mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
CLI: Support framework-agnostic route handler
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@fumadocs/cli': patch
|
||||
---
|
||||
|
||||
Support framework-agnostic route handler
|
||||
@@ -1,3 +1,5 @@
|
||||
export const runtime = 'edge';
|
||||
import { handler } from '@/lib/inkeep/route';
|
||||
|
||||
export { POST } from '@/lib/inkeep/server';
|
||||
export function POST(req: Request) {
|
||||
return handler.handler(req, {});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { z } from 'zod';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { Markdown } from '../markdown';
|
||||
import { Presence } from '@radix-ui/react-presence';
|
||||
import type { InkeepUIMessage } from '@/lib/inkeep/server';
|
||||
import type { InkeepUIMessage } from '@/lib/inkeep/route';
|
||||
|
||||
const Context = createContext<{
|
||||
open: boolean;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useChat, type UseChatHelpers } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport, type Tool, type UIToolInvocation } from 'ai';
|
||||
import { Markdown } from '../markdown';
|
||||
import { Presence } from '@radix-ui/react-presence';
|
||||
import type { ChatUIMessage, SearchTool } from '@/lib/openrouter/server';
|
||||
import type { ChatUIMessage, SearchTool } from '@/lib/openrouter/route';
|
||||
|
||||
const Context = createContext<{
|
||||
open: boolean;
|
||||
|
||||
@@ -100,9 +100,9 @@ export const registry: Registry = {
|
||||
target: '<dir>/ai/search.tsx',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: 'lib/openrouter/server.ts',
|
||||
target: 'app/api/chat/route.ts',
|
||||
type: 'route-handler',
|
||||
route: 'api/chat',
|
||||
path: 'lib/openrouter/route.ts',
|
||||
},
|
||||
],
|
||||
dependencies: {
|
||||
@@ -130,9 +130,9 @@ export const registry: Registry = {
|
||||
target: '<dir>/ai/search.tsx',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: 'lib/inkeep/server.ts',
|
||||
target: 'app/api/chat/route.ts',
|
||||
type: 'route-handler',
|
||||
route: 'api/chat',
|
||||
path: 'lib/inkeep/route.ts',
|
||||
},
|
||||
{
|
||||
type: 'lib',
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ProvideLinksToolSchema } from '@/lib/inkeep/inkeep-qa-schema';
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
import { $routeHandler } from '@fumadocs/cli/registry/macros/route-handler';
|
||||
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
|
||||
|
||||
export type InkeepUIMessage = UIMessage<
|
||||
never,
|
||||
{
|
||||
client: {
|
||||
location: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const openai = createOpenAICompatible({
|
||||
name: 'inkeep',
|
||||
apiKey: process.env.INKEEP_API_KEY,
|
||||
baseURL: 'https://api.inkeep.com/v1',
|
||||
});
|
||||
|
||||
export const handler = $routeHandler(
|
||||
{
|
||||
methods: ['POST'],
|
||||
params: [],
|
||||
},
|
||||
async (req) => {
|
||||
const reqJson = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai('inkeep-qa-sonnet-4'),
|
||||
tools: {
|
||||
provideLinks: {
|
||||
inputSchema: ProvideLinksToolSchema,
|
||||
},
|
||||
},
|
||||
messages: await convertToModelMessages<InkeepUIMessage>(reqJson.messages, {
|
||||
ignoreIncompleteToolCalls: true,
|
||||
convertDataPart(part) {
|
||||
if (part.type === 'data-client')
|
||||
return {
|
||||
type: 'text',
|
||||
text: `[Client Context: ${JSON.stringify(part.data)}]`,
|
||||
};
|
||||
},
|
||||
}),
|
||||
toolChoice: 'auto',
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
},
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ProvideLinksToolSchema } from '@/lib/inkeep/inkeep-qa-schema';
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
import { convertToModelMessages, streamText, UIMessage } from 'ai';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export type InkeepUIMessage = UIMessage<
|
||||
never,
|
||||
{
|
||||
client: {
|
||||
location: string;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
const openai = createOpenAICompatible({
|
||||
name: 'inkeep',
|
||||
apiKey: process.env.INKEEP_API_KEY,
|
||||
baseURL: 'https://api.inkeep.com/v1',
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const reqJson = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai('inkeep-qa-sonnet-4'),
|
||||
tools: {
|
||||
provideLinks: {
|
||||
inputSchema: ProvideLinksToolSchema,
|
||||
},
|
||||
},
|
||||
messages: await convertToModelMessages<InkeepUIMessage>(reqJson.messages, {
|
||||
ignoreIncompleteToolCalls: true,
|
||||
convertDataPart(part) {
|
||||
if (part.type === 'data-client')
|
||||
return {
|
||||
type: 'text',
|
||||
text: `[Client Context: ${JSON.stringify(part.data)}]`,
|
||||
};
|
||||
},
|
||||
}),
|
||||
toolChoice: 'auto',
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { convertToModelMessages, stepCountIs, streamText, tool, type UIMessage }
|
||||
import { z } from 'zod';
|
||||
import { source } from '@/lib/source';
|
||||
import { Document, type DocumentData } from 'flexsearch';
|
||||
import { $routeHandler } from '@fumadocs/cli/registry/macros/route-handler';
|
||||
|
||||
interface CustomDocument extends DocumentData {
|
||||
url: string;
|
||||
@@ -72,32 +73,38 @@ const systemPrompt = [
|
||||
'If you cannot find the answer in search results, say you do not know and suggest a better search query.',
|
||||
].join('\n');
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const reqJson = await req.json();
|
||||
export const handler = $routeHandler(
|
||||
{
|
||||
methods: ['POST'],
|
||||
params: [],
|
||||
},
|
||||
async (req) => {
|
||||
const reqJson = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet'),
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: {
|
||||
search: searchTool,
|
||||
},
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...(await convertToModelMessages<ChatUIMessage>(reqJson.messages ?? [], {
|
||||
convertDataPart(part) {
|
||||
if (part.type === 'data-client')
|
||||
return {
|
||||
type: 'text',
|
||||
text: `[Client Context: ${JSON.stringify(part.data)}]`,
|
||||
};
|
||||
},
|
||||
})),
|
||||
],
|
||||
toolChoice: 'auto',
|
||||
});
|
||||
const result = streamText({
|
||||
model: openrouter.chat(process.env.OPENROUTER_MODEL ?? 'anthropic/claude-3.5-sonnet'),
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: {
|
||||
search: searchTool,
|
||||
},
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...(await convertToModelMessages<ChatUIMessage>(reqJson.messages ?? [], {
|
||||
convertDataPart(part) {
|
||||
if (part.type === 'data-client')
|
||||
return {
|
||||
type: 'text',
|
||||
text: `[Client Context: ${JSON.stringify(part.data)}]`,
|
||||
};
|
||||
},
|
||||
})),
|
||||
],
|
||||
toolChoice: 'auto',
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
return result.toUIMessageStreamResponse();
|
||||
},
|
||||
);
|
||||
|
||||
export type SearchTool = typeof searchTool;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"./config": "./dist/config.js",
|
||||
"./registry/client": "./dist/registry/client.js",
|
||||
"./registry/installer": "./dist/registry/installer/index.js",
|
||||
"./registry/macros/route-handler": "./dist/registry/macros/route-handler.js",
|
||||
"./registry/schema": "./dist/registry/schema.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
CompiledComponent,
|
||||
CompiledFile,
|
||||
httpSubComponent,
|
||||
NamespaceType,
|
||||
registryInfoSchema,
|
||||
} from '@/registry/schema';
|
||||
import type { z } from 'zod';
|
||||
@@ -13,6 +12,8 @@ import { ResolverFactory } from 'oxc-resolver';
|
||||
import MagicString from 'magic-string';
|
||||
import { transformSpecifiers } from '@/utils/ast';
|
||||
import { isRelative } from '@/utils/fs';
|
||||
import type { DistributiveOmit } from '@/types';
|
||||
import { encodeImport } from '@/registry/protocols/import';
|
||||
|
||||
export type OnResolve = (
|
||||
reference: SourceReference,
|
||||
@@ -25,11 +26,9 @@ export interface CompiledRegistry {
|
||||
info: z.output<typeof registryInfoSchema>;
|
||||
}
|
||||
|
||||
export interface ComponentFile {
|
||||
type: NamespaceType;
|
||||
export type ComponentFile = DistributiveOmit<CompiledFile, 'content'> & {
|
||||
path: string;
|
||||
target?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Component {
|
||||
name: string;
|
||||
@@ -276,14 +275,6 @@ export class ComponentCompiler {
|
||||
this.registry = compiler.raw;
|
||||
}
|
||||
|
||||
private toImportPath(file: ComponentFile): string {
|
||||
let filePath = file.target ?? file.path;
|
||||
|
||||
if (filePath.startsWith('./')) filePath = filePath.slice(2);
|
||||
|
||||
return `@/${filePath.replaceAll(path.sep, '/')}`;
|
||||
}
|
||||
|
||||
async build(): Promise<CompiledComponent> {
|
||||
const files = (
|
||||
await Promise.all(this.component.files.map((file) => this.onBuildFile(file)))
|
||||
@@ -330,7 +321,7 @@ export class ComponentCompiler {
|
||||
const refFile = this.registry.onUnknownFile?.(reference.file);
|
||||
if (refFile) {
|
||||
queue.push(refFile);
|
||||
return this.toImportPath(refFile);
|
||||
return encodeImport(refFile);
|
||||
}
|
||||
|
||||
if (refFile === false) return;
|
||||
@@ -340,8 +331,7 @@ export class ComponentCompiler {
|
||||
|
||||
if (reference.type === 'sub-component') {
|
||||
const resolved = reference.resolved;
|
||||
if (resolved.component.name === this.component.name)
|
||||
return this.toImportPath(resolved.file);
|
||||
if (resolved.component.name === this.component.name) return encodeImport(resolved.file);
|
||||
|
||||
if (resolved.type === 'remote') {
|
||||
this.subComponents.set(`${resolved.registryName}:${resolved.component.name}`, {
|
||||
@@ -353,7 +343,7 @@ export class ComponentCompiler {
|
||||
this.subComponents.set(resolved.component.name, resolved.component.name);
|
||||
}
|
||||
|
||||
return this.toImportPath(resolved.file);
|
||||
return encodeImport(resolved.file);
|
||||
}
|
||||
|
||||
const dep = resolver.getDepInfo(reference.dep);
|
||||
@@ -387,13 +377,12 @@ export class ComponentCompiler {
|
||||
const astType = astTypes[path.extname(file.path)];
|
||||
const content = (await fs.readFile(sourceFilePath)).toString();
|
||||
|
||||
if (!astType)
|
||||
if (!astType) {
|
||||
return {
|
||||
...file,
|
||||
content,
|
||||
path: file.path,
|
||||
type: file.type,
|
||||
target: file.target,
|
||||
};
|
||||
}
|
||||
|
||||
const resolver = this.compiler.resolver;
|
||||
|
||||
@@ -449,10 +438,8 @@ export class ComponentCompiler {
|
||||
});
|
||||
|
||||
return {
|
||||
...file,
|
||||
content: s.toString(),
|
||||
type: file.type,
|
||||
path: file.path,
|
||||
target: file.target,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+34
-20
@@ -1,8 +1,11 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import { isSrc } from '@/utils/is-src';
|
||||
import { z } from 'zod';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
|
||||
export function createConfigSchema(isSrc: boolean) {
|
||||
const frameworks = ['next', 'waku', 'react-router', 'tanstack-start'] as const;
|
||||
export type Framework = (typeof frameworks)[number];
|
||||
|
||||
export function createConfigSchema() {
|
||||
const defaultAliases = {
|
||||
uiDir: './components/ui',
|
||||
componentsDir: './components',
|
||||
@@ -12,14 +15,7 @@ export function createConfigSchema(isSrc: boolean) {
|
||||
};
|
||||
|
||||
return z.object({
|
||||
$schema: z
|
||||
.string()
|
||||
.default(
|
||||
isSrc
|
||||
? 'node_modules/@fumadocs/cli/dist/schema/src.json'
|
||||
: 'node_modules/@fumadocs/cli/dist/schema/default.json',
|
||||
)
|
||||
.optional(),
|
||||
$schema: z.string().default('node_modules/@fumadocs/cli/dist/schema.json').optional(),
|
||||
aliases: z
|
||||
.object({
|
||||
uiDir: z.string().default(defaultAliases.uiDir),
|
||||
@@ -30,8 +26,11 @@ export function createConfigSchema(isSrc: boolean) {
|
||||
})
|
||||
.default(defaultAliases),
|
||||
|
||||
baseDir: z.string().default(isSrc ? 'src' : ''),
|
||||
baseDir: z.string().default(() => (existsSync('./src') ? 'src' : '')),
|
||||
uiLibrary: z.enum(['radix-ui', 'base-ui']).default('radix-ui'),
|
||||
framework: z.literal(frameworks).default(() => {
|
||||
return detectFrameworkFromPackageJson() ?? 'next';
|
||||
}),
|
||||
|
||||
commands: z
|
||||
.object({
|
||||
@@ -44,6 +43,25 @@ export function createConfigSchema(isSrc: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
function detectFrameworkFromPackageJson(pkgPath = './package.json'): Framework | undefined {
|
||||
try {
|
||||
const pkgRaw = readFileSync(pkgPath, 'utf-8');
|
||||
const pkg = JSON.parse(pkgRaw);
|
||||
|
||||
const deps = {
|
||||
...pkg.dependencies,
|
||||
...pkg.devDependencies,
|
||||
};
|
||||
|
||||
if (deps['next']) return 'next';
|
||||
if (deps['waku']) return 'waku';
|
||||
if (deps['react-router'] || deps['react-router-dom']) return 'react-router';
|
||||
if (deps['@tanstack/react-start']) return 'tanstack-start';
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigSchema = ReturnType<typeof createConfigSchema>;
|
||||
|
||||
export type ConfigInput = z.input<ConfigSchema>;
|
||||
@@ -54,8 +72,7 @@ export async function createOrLoadConfig(file = './cli.json'): Promise<LoadedCon
|
||||
if (inited) return inited;
|
||||
|
||||
const content = (await fs.readFile(file)).toString();
|
||||
const src = await isSrc();
|
||||
const configSchema = createConfigSchema(src);
|
||||
const configSchema = createConfigSchema();
|
||||
|
||||
return configSchema.parse(JSON.parse(content));
|
||||
}
|
||||
@@ -65,10 +82,7 @@ export async function createOrLoadConfig(file = './cli.json'): Promise<LoadedCon
|
||||
*
|
||||
* @returns the created config, `undefined` if not created
|
||||
*/
|
||||
export async function initConfig(
|
||||
file = './cli.json',
|
||||
src?: boolean,
|
||||
): Promise<LoadedConfig | undefined> {
|
||||
export async function initConfig(file = './cli.json'): Promise<LoadedConfig | undefined> {
|
||||
if (
|
||||
await fs
|
||||
.stat(file)
|
||||
@@ -78,11 +92,11 @@ export async function initConfig(
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultConfig = await getDefaultConfig(src);
|
||||
const defaultConfig = await getDefaultConfig();
|
||||
await fs.writeFile(file, JSON.stringify(defaultConfig, null, 2));
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
export async function getDefaultConfig(src?: boolean) {
|
||||
return createConfigSchema(src ?? (await isSrc())).parse({} satisfies ConfigInput);
|
||||
export async function getDefaultConfig() {
|
||||
return createConfigSchema().parse({} satisfies ConfigInput);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import { DependencyManager } from '@/registry/installer/dep-manager';
|
||||
import { createCache } from '@/utils/cache';
|
||||
import { parse, type ParseResult } from 'oxc-parser';
|
||||
import MagicString from 'magic-string';
|
||||
import { decodeImport, encodeImport } from '../protocols/import';
|
||||
import type { Awaitable } from '@/types';
|
||||
import { buildRouteHandlerFile } from '@/registry/macros/route-handler.build';
|
||||
import { resolveRouteFilePath } from '@/utils/framework';
|
||||
|
||||
interface PluginContext {
|
||||
installer: ComponentInstaller;
|
||||
@@ -22,8 +26,8 @@ interface TransformContext extends PluginContext, InstallContext {
|
||||
}
|
||||
|
||||
interface InstallContext {
|
||||
pathToFile: Map<string, File>;
|
||||
io: IOInterface;
|
||||
importLookup: Map<string, File>;
|
||||
/** full variables of the current component. */
|
||||
$variables: Record<string, unknown>;
|
||||
/** the last item is always the current component. */
|
||||
@@ -35,8 +39,6 @@ interface DownloadedComponent extends Component {
|
||||
$registry: RegistryClient;
|
||||
}
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface ComponentInstallerPlugin {
|
||||
/**
|
||||
* transform file before writing (before default transformation)
|
||||
@@ -176,14 +178,16 @@ export class ComponentInstaller {
|
||||
|
||||
scan(downloaded);
|
||||
|
||||
const pathToFile = new Map<string, File>();
|
||||
const importLookup = new Map<string, File>();
|
||||
for (const comp of allComponents) {
|
||||
for (const file of comp.files) pathToFile.set(file.target ?? file.path, file);
|
||||
for (const file of comp.files) {
|
||||
importLookup.set(encodeImport(file), file);
|
||||
}
|
||||
}
|
||||
|
||||
const info = await downloaded.$registry.fetchRegistryInfo();
|
||||
await this.installComponent(downloaded, {
|
||||
pathToFile,
|
||||
importLookup,
|
||||
io,
|
||||
$variables: { ...info.env, ...downloaded.variables },
|
||||
stack: [downloaded],
|
||||
@@ -272,25 +276,33 @@ export class ComponentInstaller {
|
||||
await plugin.beforeTransform?.(transformCtx);
|
||||
}
|
||||
|
||||
// transform alias
|
||||
const prefix = '@/';
|
||||
if (file.type === 'route-handler') {
|
||||
buildRouteHandlerFile(
|
||||
file.route,
|
||||
filePath,
|
||||
this.rootClient.config.framework,
|
||||
parsed.program,
|
||||
s,
|
||||
);
|
||||
}
|
||||
|
||||
transformSpecifiers(parsed.program, s, (specifier) => {
|
||||
for (const [k, v] of Object.entries(ctx.$variables)) {
|
||||
if (typeof v === 'string') specifier = specifier.replaceAll(`<${k}>`, v);
|
||||
}
|
||||
if (ctx.importLookup.has(specifier)) {
|
||||
let outputPath = this.resolveOutputPath(ctx.importLookup.get(specifier)!);
|
||||
|
||||
if (specifier.startsWith(prefix)) {
|
||||
const lookup = specifier.substring(prefix.length);
|
||||
const target = ctx.pathToFile.get(lookup);
|
||||
|
||||
if (target) {
|
||||
specifier = toImportSpecifier(filePath, this.resolveOutputPath(target));
|
||||
} else {
|
||||
ctx.io.onWarn(`cannot find the referenced file of ${specifier}`);
|
||||
for (const [k, v] of Object.entries(ctx.$variables)) {
|
||||
if (typeof v === 'string') outputPath = outputPath.replaceAll(`<${k}>`, v);
|
||||
}
|
||||
|
||||
return toImportSpecifier(filePath, outputPath);
|
||||
}
|
||||
|
||||
const decoded = decodeImport(specifier);
|
||||
if ('raw' in decoded) {
|
||||
return decoded.raw;
|
||||
}
|
||||
|
||||
ctx.io.onWarn(`cannot find the referenced file of ${specifier}`);
|
||||
return specifier;
|
||||
});
|
||||
|
||||
@@ -303,6 +315,11 @@ export class ComponentInstaller {
|
||||
|
||||
private resolveOutputPath(file: File): string {
|
||||
const config = this.rootClient.config;
|
||||
if (file.type === 'route-handler') {
|
||||
const rel = resolveRouteFilePath(file.route, config.framework, 'ts');
|
||||
return path.resolve(this.cwd, config.baseDir, rel);
|
||||
}
|
||||
|
||||
const dir = (
|
||||
{
|
||||
components: config.aliases.componentsDir,
|
||||
@@ -310,7 +327,6 @@ export class ComponentInstaller {
|
||||
css: config.aliases.cssDir,
|
||||
lib: config.aliases.libDir,
|
||||
layout: config.aliases.layoutDir,
|
||||
route: './',
|
||||
} as const
|
||||
)[file.type];
|
||||
if (file.target) {
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
import path from 'node:path';
|
||||
import MagicString from 'magic-string';
|
||||
import { Visitor } from 'oxc-parser';
|
||||
import type {
|
||||
ArrowFunctionExpression,
|
||||
CallExpression,
|
||||
Expression,
|
||||
Function as AstFunction,
|
||||
ImportDeclaration,
|
||||
ImportDeclarationSpecifier,
|
||||
ModuleExportName,
|
||||
ObjectExpression,
|
||||
ParamPattern,
|
||||
Program,
|
||||
Statement,
|
||||
} from '@oxc-project/types';
|
||||
import type { Framework } from '@/config';
|
||||
import type { RouteHandlerHttpMethod, StaticInfo } from './route-handler';
|
||||
|
||||
const reactRouterLoaderMethods = new Set(['GET', 'HEAD', 'OPTIONS']);
|
||||
const reactRouterActionMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||
|
||||
export type ParsedRouteInfo = StaticInfo<string, string | undefined>;
|
||||
|
||||
function moduleExportNameString(name: ModuleExportName): string {
|
||||
if (name.type === 'Identifier') return name.name;
|
||||
if (name.type === 'Literal') return String(name.value);
|
||||
return '';
|
||||
}
|
||||
|
||||
function getImportedBinding(
|
||||
spec: ImportDeclarationSpecifier,
|
||||
): { imported: string; local: string } | null {
|
||||
if (spec.type === 'ImportSpecifier') {
|
||||
return {
|
||||
imported: moduleExportNameString(spec.imported),
|
||||
local: spec.local.name,
|
||||
};
|
||||
}
|
||||
if (spec.type === 'ImportDefaultSpecifier') {
|
||||
return { imported: 'default', local: spec.local.name };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectMacroBindings(
|
||||
program: Program,
|
||||
): { importDecls: ImportDeclaration[]; locals: Set<string> } | null {
|
||||
const locals = new Set<string>();
|
||||
const importDecls: ImportDeclaration[] = [];
|
||||
const seenDecl = new Set<ImportDeclaration>();
|
||||
|
||||
new Visitor({
|
||||
ImportDeclaration(node: ImportDeclaration) {
|
||||
for (const spec of node.specifiers) {
|
||||
const b = getImportedBinding(spec);
|
||||
if (!b) continue;
|
||||
if (b.imported === '$routeHandler' || b.imported === 'default') {
|
||||
locals.add(b.local);
|
||||
if (!seenDecl.has(node)) {
|
||||
seenDecl.add(node);
|
||||
importDecls.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}).visit(program);
|
||||
|
||||
if (locals.size === 0) return null;
|
||||
return { importDecls, locals };
|
||||
}
|
||||
|
||||
function collectRouteHandlerCalls(program: Program, locals: Set<string>): CallExpression[] {
|
||||
const calls: CallExpression[] = [];
|
||||
new Visitor({
|
||||
CallExpression(node: CallExpression) {
|
||||
if (node.callee.type !== 'Identifier') return;
|
||||
if (!locals.has(node.callee.name)) return;
|
||||
calls.push(node);
|
||||
},
|
||||
}).visit(program);
|
||||
return calls;
|
||||
}
|
||||
|
||||
function isSameCall(init: Expression | null | undefined, call: CallExpression): boolean {
|
||||
if (!init || init.type !== 'CallExpression') return false;
|
||||
return init.start === call.start && init.end === call.end;
|
||||
}
|
||||
|
||||
function findStatementSpanForCall(
|
||||
program: Program,
|
||||
call: CallExpression,
|
||||
): { start: number; end: number } | null {
|
||||
for (const stmt of program.body) {
|
||||
const span = statementSpanIfContainsCall(stmt, call);
|
||||
if (span) return span;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function statementSpanIfContainsCall(
|
||||
stmt: Statement,
|
||||
call: CallExpression,
|
||||
): { start: number; end: number } | null {
|
||||
if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'VariableDeclaration') {
|
||||
for (const d of stmt.declaration.declarations) {
|
||||
if (isSameCall(d.init, call)) return { start: stmt.start, end: stmt.end };
|
||||
}
|
||||
}
|
||||
if (stmt.type === 'VariableDeclaration') {
|
||||
for (const d of stmt.declarations) {
|
||||
if (isSameCall(d.init, call)) return { start: stmt.start, end: stmt.end };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function objectPropertyKeyName(key: ObjectExpression['properties'][number]): string | null {
|
||||
if (key.type !== 'Property') return null;
|
||||
const k = key.key;
|
||||
if (k.type === 'Identifier') return k.name;
|
||||
if (k.type === 'Literal' && typeof k.value === 'string') return k.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `methods`, `params`, and optional `catchAll` from the `$routeHandler` info object literal (AST).
|
||||
*/
|
||||
export function parseRouteInfoFromAst(info: ObjectExpression): ParsedRouteInfo {
|
||||
let methods: string[] | undefined;
|
||||
let params: string[] | undefined;
|
||||
let catchAll: string | undefined;
|
||||
|
||||
for (const prop of info.properties) {
|
||||
if (prop.type !== 'Property') continue;
|
||||
const name = objectPropertyKeyName(prop);
|
||||
if (name === 'methods') {
|
||||
if (prop.value.type !== 'ArrayExpression') {
|
||||
throw new Error(
|
||||
'route-handler.build: `methods` in $routeHandler info must be an array literal',
|
||||
);
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const el of prop.value.elements) {
|
||||
if (el == null) continue;
|
||||
if (el.type === 'Literal' && typeof el.value === 'string') {
|
||||
out.push(el.value);
|
||||
continue;
|
||||
}
|
||||
throw new Error(
|
||||
'route-handler.build: `methods` must be string literals (e.g. methods: ["GET", "POST"])',
|
||||
);
|
||||
}
|
||||
methods = out;
|
||||
}
|
||||
if (name === 'params') {
|
||||
if (prop.value.type !== 'ArrayExpression') {
|
||||
throw new Error('route-handler.build: `params` must be an array literal');
|
||||
}
|
||||
const out: string[] = [];
|
||||
for (const el of prop.value.elements) {
|
||||
if (el == null) continue;
|
||||
if (el.type === 'Literal' && typeof el.value === 'string') {
|
||||
out.push(el.value);
|
||||
continue;
|
||||
}
|
||||
throw new Error('route-handler.build: `params` must be string literals');
|
||||
}
|
||||
params = out;
|
||||
}
|
||||
if (name === 'catchAll') {
|
||||
if (prop.value.type === 'Literal' && typeof prop.value.value === 'string') {
|
||||
catchAll = prop.value.value;
|
||||
continue;
|
||||
}
|
||||
throw new Error('route-handler.build: `catchAll` must be a string literal');
|
||||
}
|
||||
}
|
||||
|
||||
if (!methods?.length) {
|
||||
throw new Error(
|
||||
'route-handler.build: $routeHandler info must include a non-empty `methods` array',
|
||||
);
|
||||
}
|
||||
if (!params) {
|
||||
throw new Error(
|
||||
'route-handler.build: $routeHandler info must include a `params` array literal',
|
||||
);
|
||||
}
|
||||
|
||||
return { methods: methods as RouteHandlerHttpMethod[], params, catchAll };
|
||||
}
|
||||
|
||||
function needsRouteParams(info: ParsedRouteInfo): boolean {
|
||||
return info.params.length > 0 || Boolean(info.catchAll);
|
||||
}
|
||||
|
||||
function encodeKey(key: string): string {
|
||||
if (key.includes('-')) {
|
||||
return JSON.stringify(key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function bindingNameFromParam(p: ParamPattern): string | null {
|
||||
if (p.type === 'Identifier') return p.name;
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseHandlerFromAst(
|
||||
s: MagicString,
|
||||
expr: Expression,
|
||||
): { requestName: string; paramsName: string | null; bodyText: string } {
|
||||
const isFn = expr.type === 'FunctionExpression';
|
||||
const isArrow = expr.type === 'ArrowFunctionExpression';
|
||||
if (!isFn && !isArrow) {
|
||||
throw new Error(
|
||||
'route-handler.build: second argument to $routeHandler must be a function or async arrow function',
|
||||
);
|
||||
}
|
||||
const fn = expr as ArrowFunctionExpression | (AstFunction & { type: 'FunctionExpression' });
|
||||
if (!fn.async) {
|
||||
throw new Error('route-handler.build: route handler must be async');
|
||||
}
|
||||
if (isFn && (!fn.body || fn.body.type !== 'BlockStatement')) {
|
||||
throw new Error('route-handler.build: function handler must use a block body');
|
||||
}
|
||||
|
||||
const p0 = fn.params[0];
|
||||
const p1 = fn.params[1];
|
||||
if (fn.params.length > 2) {
|
||||
throw new Error('route-handler.build: route handler must have at most two parameters');
|
||||
}
|
||||
|
||||
const requestName = (p0 && bindingNameFromParam(p0)) || 'request';
|
||||
const paramsName = p1 ? bindingNameFromParam(p1) : null;
|
||||
if (p1 && !paramsName) {
|
||||
throw new Error(
|
||||
'route-handler.build: unsupported parameter pattern; use a simple identifier for (request, params)',
|
||||
);
|
||||
}
|
||||
|
||||
const body = fn.body;
|
||||
let bodyText: string;
|
||||
|
||||
if (body == null) {
|
||||
throw new Error('route-handler.build: handler has no body');
|
||||
} else if (body.type === 'BlockStatement') {
|
||||
bodyText = s.slice(body.start + 1, body.end - 1).trim();
|
||||
} else if (fn.type === 'ArrowFunctionExpression') {
|
||||
const expr = s.slice(body.start, body.end);
|
||||
bodyText = `return ${expr};`;
|
||||
} else {
|
||||
throw new Error('route-handler.build: could not extract handler body');
|
||||
}
|
||||
|
||||
return { requestName, paramsName, bodyText };
|
||||
}
|
||||
|
||||
function buildParamsObjectLiteral(
|
||||
framework: Framework,
|
||||
info: ParsedRouteInfo,
|
||||
paramsIdentifier: string,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
for (const k of info.params) {
|
||||
parts.push(`${encodeKey(k)}: ${paramsIdentifier}.${k}`);
|
||||
}
|
||||
|
||||
if (info.catchAll) {
|
||||
let value: string;
|
||||
switch (framework) {
|
||||
case 'react-router':
|
||||
value = `${paramsIdentifier}['*']`;
|
||||
break;
|
||||
case 'tanstack-start':
|
||||
value = `${paramsIdentifier}._splat`;
|
||||
break;
|
||||
default:
|
||||
value = `${paramsIdentifier}.${info.catchAll}`;
|
||||
}
|
||||
|
||||
parts.push(`${encodeKey(info.catchAll)}: ${value}`);
|
||||
}
|
||||
|
||||
return `{\n${indentBlock(parts.join(',\n'), 2)}\n}`;
|
||||
}
|
||||
|
||||
function requestAliasPrefix(framework: Framework, handler: { requestName: string }): string {
|
||||
if (framework === 'tanstack-start') {
|
||||
if (handler.requestName === 'ctx') {
|
||||
throw new Error(
|
||||
'route-handler.build: name the request parameter something other than `ctx` for TanStack file routes',
|
||||
);
|
||||
}
|
||||
return `const ${handler.requestName} = ctx.request;\n`;
|
||||
}
|
||||
if (framework === 'react-router') {
|
||||
if (handler.requestName === 'args') {
|
||||
throw new Error(
|
||||
'route-handler.build: name the request parameter something other than `args` for React Router resource routes',
|
||||
);
|
||||
}
|
||||
return `const ${handler.requestName} = args.request;\n`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildRouteSetupBlock(
|
||||
framework: Framework,
|
||||
info: ParsedRouteInfo,
|
||||
paramsBinding: string,
|
||||
): string {
|
||||
if (!needsRouteParams(info)) return '';
|
||||
let paramsIdentifier: string;
|
||||
switch (framework) {
|
||||
case 'next':
|
||||
paramsIdentifier = '(await ctx.params)';
|
||||
break;
|
||||
case 'react-router':
|
||||
paramsIdentifier = 'args.params';
|
||||
break;
|
||||
case 'tanstack-start':
|
||||
paramsIdentifier = 'ctx.params';
|
||||
break;
|
||||
case 'waku':
|
||||
paramsIdentifier = 'context.params';
|
||||
break;
|
||||
}
|
||||
|
||||
return `const ${paramsBinding} = ${buildParamsObjectLiteral(framework, info, paramsIdentifier)};\n`;
|
||||
}
|
||||
|
||||
function resolveParamsBindingName(info: ParsedRouteInfo, userSecond: string | null): string | null {
|
||||
if (!needsRouteParams(info)) return null;
|
||||
return userSecond ?? 'params';
|
||||
}
|
||||
|
||||
function registryRouteToTanStackCreateFileRoutePath(route: string): string {
|
||||
const trimmed = route.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const segments = trimmed.split('/');
|
||||
const parts = segments.map((seg) => {
|
||||
if (/^\[\[\.\.\.[^/\]]+\]\]$/.test(seg) || /^\[\.\.\.[^/\]]+\]$/.test(seg)) return '$';
|
||||
const m = /^\[([^/\]]+)\]$/.exec(seg);
|
||||
if (m) return `$${m[1]}`;
|
||||
return seg;
|
||||
});
|
||||
return `/${parts.join('/')}`;
|
||||
}
|
||||
|
||||
/** URL path for `RouteContext` / `ApiContext` string literals (leading slash, App Router–style segments). */
|
||||
function registryRouteToUrlPath(route: string): string {
|
||||
const t = route.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
return t ? `/${t}` : '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* React Router typegen: `./+types/<segments>` relative to the route file (see `+types` next to `routes/`).
|
||||
*/
|
||||
function computeReactRouterTypesSpecifier(routeFilePath: string): string {
|
||||
const normalized = routeFilePath.replace(/\\/g, '/');
|
||||
const parts = normalized.split('/');
|
||||
const routesIdx = parts.lastIndexOf('routes');
|
||||
const ext = path.extname(routeFilePath);
|
||||
const base = path.basename(routeFilePath, ext);
|
||||
if (routesIdx >= 0) {
|
||||
const dirSegments = parts.slice(routesIdx + 1, -1);
|
||||
return `./+types/${[...dirSegments, base].join('.')}`;
|
||||
}
|
||||
return `./+types/${base}`;
|
||||
}
|
||||
|
||||
function frameworkGeneratedImports(framework: Framework, routeFilePath: string): string {
|
||||
const lines: string[] = [];
|
||||
if (framework === 'tanstack-start') {
|
||||
lines.push(`import { createFileRoute } from '@tanstack/react-router';`);
|
||||
}
|
||||
if (framework === 'waku') {
|
||||
lines.push(`import type { ApiContext } from 'waku/router';`);
|
||||
}
|
||||
if (framework === 'react-router') {
|
||||
lines.push(`import type { Route } from '${computeReactRouterTypesSpecifier(routeFilePath)}';`);
|
||||
}
|
||||
return lines.length ? `${lines.join('\n')}\n` : '';
|
||||
}
|
||||
|
||||
function indentBlock(text: string, spaces: number): string {
|
||||
const pad = ' '.repeat(spaces);
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => (line.length ? pad + line : line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function generateDeclaration(
|
||||
framework: Framework,
|
||||
route: string,
|
||||
parsedInfo: ParsedRouteInfo,
|
||||
handler: { requestName: string; paramsName: string | null; bodyText: string },
|
||||
): string {
|
||||
const paramsBinding = resolveParamsBindingName(parsedInfo, handler.paramsName);
|
||||
const alias = requestAliasPrefix(framework, handler);
|
||||
const setupBlock =
|
||||
paramsBinding != null ? buildRouteSetupBlock(framework, parsedInfo, paramsBinding) : '';
|
||||
const inner = alias + setupBlock + handler.bodyText;
|
||||
const bodyIndented = indentBlock(inner, 2);
|
||||
const urlPath = registryRouteToUrlPath(route);
|
||||
const urlPathLiteral = JSON.stringify(urlPath);
|
||||
|
||||
switch (framework) {
|
||||
case 'next': {
|
||||
/** `RouteContext` is provided by Next.js typed routes / `next typegen` (global). */
|
||||
const ctxType = `RouteContext<${urlPathLiteral}>`;
|
||||
return parsedInfo.methods
|
||||
.map(
|
||||
(m) =>
|
||||
`export async function ${m}(${handler.requestName}: Request, ctx: ${ctxType}) {\n${bodyIndented}\n}`,
|
||||
)
|
||||
.join('\n\n');
|
||||
}
|
||||
case 'waku': {
|
||||
const ctxType = `ApiContext<${urlPathLiteral}>`;
|
||||
return parsedInfo.methods
|
||||
.map(
|
||||
(m) =>
|
||||
`export async function ${m}(${handler.requestName}: Request, context: ${ctxType}) {\n${bodyIndented}\n}`,
|
||||
)
|
||||
.join('\n\n');
|
||||
}
|
||||
case 'tanstack-start': {
|
||||
const fileRoutePath = JSON.stringify(registryRouteToTanStackCreateFileRoutePath(route));
|
||||
const entries = parsedInfo.methods
|
||||
.map((m) => ` ${m}: async (ctx) => {\n${indentBlock(inner, 8)}\n },`)
|
||||
.join('\n');
|
||||
return `export const Route = createFileRoute(${fileRoutePath})({\n server: {\n handlers: {\n${entries}\n },\n },\n});\n`;
|
||||
}
|
||||
case 'react-router': {
|
||||
const includeLoader = parsedInfo.methods.some((x) => reactRouterLoaderMethods.has(x));
|
||||
const includeAction = parsedInfo.methods.some((x) => reactRouterActionMethods.has(x));
|
||||
if (!includeLoader && !includeAction) {
|
||||
throw new Error(
|
||||
'route-handler.build: react-router needs at least one method mapped to loader (GET/HEAD/OPTIONS) or action (POST/PUT/PATCH/DELETE)',
|
||||
);
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (includeLoader) {
|
||||
parts.push(`export async function loader(args: Route.LoaderArgs) {\n${bodyIndented}\n}`);
|
||||
}
|
||||
if (includeAction) {
|
||||
parts.push(`export async function action(args: Route.ActionArgs) {\n${bodyIndented}\n}`);
|
||||
}
|
||||
return `${parts.join('\n\n')}\n`;
|
||||
}
|
||||
default: {
|
||||
const _exhaustive: never = framework;
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeMacroImport(s: MagicString, importDecl: ImportDeclaration): void {
|
||||
const start = importDecl.start;
|
||||
let end = importDecl.end;
|
||||
if (s.original[end] === '\n') end += 1;
|
||||
s.remove(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites a module that calls `$routeHandler` into framework-native route code.
|
||||
*
|
||||
* Output does not import `@fumadocs/cli`.
|
||||
*
|
||||
* Uses framework typegen where applicable: global `RouteContext` (Next typed routes), `ApiContext` (Waku),
|
||||
* inferred handler `ctx` (TanStack `createFileRoute`), `Route.LoaderArgs` / `Route.ActionArgs` (React Router `+types`).
|
||||
*/
|
||||
export function buildRouteHandlerFile(
|
||||
route: string,
|
||||
routeFilePath: string,
|
||||
framework: Framework,
|
||||
program: Program,
|
||||
s: MagicString,
|
||||
) {
|
||||
const macro = collectMacroBindings(program);
|
||||
if (!macro) return;
|
||||
|
||||
const calls = collectRouteHandlerCalls(program, macro.locals);
|
||||
if (calls.length === 0) return;
|
||||
if (calls.length > 1) {
|
||||
throw new Error('route-handler.build: expected exactly one $routeHandler(...) call per file');
|
||||
}
|
||||
|
||||
const call = calls[0]!;
|
||||
if (call.arguments.length !== 2) {
|
||||
throw new Error('route-handler.build: $routeHandler must be called with (info, handler)');
|
||||
}
|
||||
|
||||
const arg0 = call.arguments[0] as Expression;
|
||||
const arg1 = call.arguments[1] as Expression;
|
||||
if (arg0.type !== 'ObjectExpression') {
|
||||
throw new Error(
|
||||
'route-handler.build: first argument to $routeHandler must be an object literal',
|
||||
);
|
||||
}
|
||||
|
||||
const parsedInfo = parseRouteInfoFromAst(arg0);
|
||||
const handler = parseHandlerFromAst(s, arg1);
|
||||
|
||||
const stmtSpan = findStatementSpanForCall(program, call);
|
||||
if (!stmtSpan) {
|
||||
throw new Error(
|
||||
'route-handler.build: $routeHandler(...) must be the initializer of a const (optionally exported)',
|
||||
);
|
||||
}
|
||||
|
||||
const extraImports = frameworkGeneratedImports(framework, routeFilePath);
|
||||
if (extraImports) {
|
||||
const insertPos = findInsertPositionForNewImports(program, macro.importDecls);
|
||||
if (insertPos === 0) {
|
||||
s.prepend(extraImports);
|
||||
} else {
|
||||
s.appendLeft(insertPos, `\n${extraImports}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const decl of [...macro.importDecls].sort((a, b) => b.start - a.start)) {
|
||||
removeMacroImport(s, decl);
|
||||
}
|
||||
s.overwrite(
|
||||
stmtSpan.start,
|
||||
stmtSpan.end,
|
||||
generateDeclaration(framework, route, parsedInfo, handler),
|
||||
);
|
||||
}
|
||||
|
||||
function findInsertPositionForNewImports(
|
||||
program: Program,
|
||||
skipImports: readonly ImportDeclaration[],
|
||||
): number {
|
||||
const skip = new Set(skipImports);
|
||||
const others = program.body.filter(
|
||||
(stmt): stmt is ImportDeclaration => stmt.type === 'ImportDeclaration' && !skip.has(stmt),
|
||||
);
|
||||
if (others.length === 0) return 0;
|
||||
return others[others.length - 1]!.end;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Awaitable } from '@/types';
|
||||
|
||||
const routeHandlerHttpMethods = [
|
||||
'GET',
|
||||
'HEAD',
|
||||
'POST',
|
||||
'PUT',
|
||||
'PATCH',
|
||||
'DELETE',
|
||||
'OPTIONS',
|
||||
] as const;
|
||||
|
||||
export type RouteHandlerHttpMethod = (typeof routeHandlerHttpMethods)[number];
|
||||
|
||||
export interface StaticInfo<Params extends string, CatchAll extends string | undefined> {
|
||||
params: Params[];
|
||||
catchAll?: CatchAll;
|
||||
/**
|
||||
* HTTP verbs this route implements.
|
||||
*/
|
||||
methods: readonly RouteHandlerHttpMethod[];
|
||||
}
|
||||
|
||||
export type RouteHandler<Params extends string, CatchAll extends string | undefined> = (
|
||||
request: Request,
|
||||
params: RouteHandlerParams<Params, CatchAll>,
|
||||
) => Awaitable<Response>;
|
||||
|
||||
export type RouteHandlerParams<Params extends string, CatchAll extends string | undefined> = Record<
|
||||
Params,
|
||||
string
|
||||
> &
|
||||
(CatchAll extends string ? Record<CatchAll, string[] | undefined> : unknown);
|
||||
|
||||
export function $routeHandler<
|
||||
Params extends string,
|
||||
CatchAll extends string | undefined = undefined,
|
||||
>(info: StaticInfo<Params, CatchAll>, handler: RouteHandler<NoInfer<Params>, NoInfer<CatchAll>>) {
|
||||
return {
|
||||
info,
|
||||
handler,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ComponentFile } from '@/build';
|
||||
import type { CompiledFile } from '../schema';
|
||||
|
||||
export type ImportInfo =
|
||||
| {
|
||||
raw: string;
|
||||
}
|
||||
| {
|
||||
target: string;
|
||||
}
|
||||
| {
|
||||
route: string;
|
||||
};
|
||||
|
||||
export function encodeImport(file: ComponentFile | CompiledFile): string {
|
||||
if (file.type === 'route-handler') return `route-handler:${file.route}`;
|
||||
return `:${file.target ?? file.path}`;
|
||||
}
|
||||
|
||||
export function decodeImport(s: string): ImportInfo {
|
||||
if (s.startsWith(':'))
|
||||
return {
|
||||
target: s.slice(1),
|
||||
};
|
||||
|
||||
if (s.startsWith('route-handler:'))
|
||||
return {
|
||||
route: s.slice('route-handler:'.length),
|
||||
};
|
||||
|
||||
return { raw: s };
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export type NamespaceType = (typeof namespaces)[number];
|
||||
export type CompiledFile = z.input<typeof fileSchema>;
|
||||
export type CompiledComponent = z.input<typeof componentSchema>;
|
||||
export type CompiledRegistryInfo = z.input<typeof registryInfoSchema>;
|
||||
@@ -8,21 +7,27 @@ export type DownloadedRegistryInfo = z.output<typeof registryInfoSchema>;
|
||||
export type File = z.output<typeof fileSchema>;
|
||||
export type Component = z.output<typeof componentSchema>;
|
||||
|
||||
export const namespaces = ['components', 'lib', 'css', 'route', 'ui', 'layout'] as const;
|
||||
|
||||
export const indexSchema = z.object({
|
||||
name: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const fileSchema = z.object({
|
||||
type: z.literal(namespaces),
|
||||
path: z.string(),
|
||||
target: z.string().optional(),
|
||||
export const routeFileSchema = z.object({
|
||||
type: z.literal('route-handler'),
|
||||
route: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
export const fileSchema = z
|
||||
.object({
|
||||
type: z.literal(['components', 'lib', 'css', 'ui', 'layout']),
|
||||
path: z.string(),
|
||||
target: z.string().optional(),
|
||||
content: z.string(),
|
||||
})
|
||||
.or(routeFileSchema);
|
||||
|
||||
export const httpSubComponent = z.object({
|
||||
type: z.literal('http'),
|
||||
baseUrl: z.string(),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
||||
export type Awaitable<T> = T | Promise<T>;
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Framework } from '@/config';
|
||||
|
||||
/**
|
||||
* Resolves the route file path (relative to the CLI `baseDir`) from an App Router–style route string.
|
||||
*
|
||||
* @param route - The route path, e.g., "api/auth/[[...slug]]".
|
||||
* @param framework - Value of `framework` in CLI config.
|
||||
* @param extension - Optional file extension, default is "ts".
|
||||
* @returns Relative path incl. framework route root, e.g. "app/api/foo/route.ts" (next), "routes/api.foo.$.ts" (tanstack-start), or "routes/api/foo/$.ts" (react-router).
|
||||
*/
|
||||
export function resolveRouteFilePath(
|
||||
route: string,
|
||||
framework: Framework,
|
||||
extension: string = 'ts',
|
||||
): string {
|
||||
route = route.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
|
||||
switch (framework) {
|
||||
case 'next': {
|
||||
return `app/${route}/route.${extension}`;
|
||||
}
|
||||
case 'tanstack-start': {
|
||||
let flat = route
|
||||
.replace(/\[\[\.\.\.[^/\]]+\]\]/g, '$')
|
||||
.replace(/\[\.\.\.[^/\]]+\]/g, '$')
|
||||
.replace(/\[([^/\]]+)\]/g, (_, p1) => `$${p1}`);
|
||||
flat = flat.replaceAll('/', '.');
|
||||
if (flat.startsWith('.')) flat = flat.slice(1);
|
||||
return `routes/${flat}.${extension}`;
|
||||
}
|
||||
case 'react-router': {
|
||||
const rrPath = route
|
||||
.split('/')
|
||||
.map((seg) => {
|
||||
if (/^\[\[\.\.\.[^/\]]+\]\]$/.test(seg) || /^\[\.\.\.[^/\]]+\]$/.test(seg)) return '$';
|
||||
return seg.replace(/^\[([^/\]]+)\]$/, (_, v) => `$${v}`);
|
||||
})
|
||||
.join('/');
|
||||
return `routes/${rrPath}.${extension}`;
|
||||
}
|
||||
case 'waku': {
|
||||
const underApi = route
|
||||
.replace(/\[\[\.\.\.([^\]]+)\]\]/g, '[...$1]')
|
||||
.replace(/\[\.\.\.([^\]]+)\]/g, '[...$1]');
|
||||
return `pages/_api/${underApi}.${extension}`;
|
||||
}
|
||||
default: {
|
||||
const _exhaustive: never = framework;
|
||||
return _exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { exists } from '@/utils/fs';
|
||||
|
||||
export async function isSrc(): Promise<boolean> {
|
||||
return exists('./src');
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import type { Framework } from '@/config';
|
||||
import { resolveRouteFilePath } from '@/utils/framework';
|
||||
|
||||
test('next: app/ + app router route.ts path (incl. optional catch-all)', () => {
|
||||
expect(resolveRouteFilePath('api/auth/[[...slug]]', 'next')).toBe(
|
||||
'app/api/auth/[[...slug]]/route.ts',
|
||||
);
|
||||
expect(resolveRouteFilePath('api/foo/[...slug]', 'next')).toBe('app/api/foo/[...slug]/route.ts');
|
||||
expect(resolveRouteFilePath('/api/foo/', 'next')).toBe('app/api/foo/route.ts');
|
||||
});
|
||||
|
||||
test('next: custom extension', () => {
|
||||
expect(resolveRouteFilePath('api/foo', 'next', 'tsx')).toBe('app/api/foo/route.tsx');
|
||||
});
|
||||
|
||||
test('tanstack-start: routes/ + flattened $ / splat filenames', () => {
|
||||
const fw = 'tanstack-start' as const satisfies Framework;
|
||||
expect(resolveRouteFilePath('api/handler', fw)).toBe('routes/api.handler.ts');
|
||||
expect(resolveRouteFilePath('/api/foo/', fw)).toBe('routes/api.foo.ts');
|
||||
expect(resolveRouteFilePath('api/posts/[id]', fw)).toBe('routes/api.posts.$id.ts');
|
||||
expect(resolveRouteFilePath('api/foo/[[...slug]]', fw)).toBe('routes/api.foo.$.ts');
|
||||
expect(resolveRouteFilePath('api/foo/[...slug]', fw)).toBe('routes/api.foo.$.ts');
|
||||
expect(resolveRouteFilePath('posts/[slug]/edit', fw)).toBe('routes/posts.$slug.edit.ts');
|
||||
});
|
||||
|
||||
test('react-router: routes/ + nested path ($ dynamic, $ splat)', () => {
|
||||
const fw = 'react-router' as const satisfies Framework;
|
||||
expect(resolveRouteFilePath('api/handler', fw)).toBe('routes/api/handler.ts');
|
||||
expect(resolveRouteFilePath('/api/foo/', fw)).toBe('routes/api/foo.ts');
|
||||
expect(resolveRouteFilePath('api/posts/[id]', fw)).toBe('routes/api/posts/$id.ts');
|
||||
expect(resolveRouteFilePath('api/foo/[[...slug]]', fw)).toBe('routes/api/foo/$.ts');
|
||||
expect(resolveRouteFilePath('api/foo/[...slug]', fw)).toBe('routes/api/foo/$.ts');
|
||||
expect(resolveRouteFilePath('posts/[slug]/edit', fw)).toBe('routes/posts/$slug/edit.ts');
|
||||
});
|
||||
|
||||
test('waku: pages/_api/ + [...] catch-all normalization', () => {
|
||||
expect(resolveRouteFilePath('api/handler', 'waku')).toBe('pages/_api/api/handler.ts');
|
||||
expect(resolveRouteFilePath('api/foo/[[...slug]]', 'waku')).toBe(
|
||||
'pages/_api/api/foo/[...slug].ts',
|
||||
);
|
||||
expect(resolveRouteFilePath('api/foo/[...slug]', 'waku')).toBe('pages/_api/api/foo/[...slug].ts');
|
||||
expect(resolveRouteFilePath('api/items/[id]', 'waku')).toBe('pages/_api/api/items/[id].ts');
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import type { Framework } from '@/config';
|
||||
import { buildRouteHandlerFile } from '@/registry/macros/route-handler.build';
|
||||
import MagicString from 'magic-string';
|
||||
import path from 'node:path';
|
||||
import { parseSync } from 'oxc-parser';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
export interface BuildRouteHandlerFileOptions {
|
||||
route: string;
|
||||
/** Resolved absolute path for the emitted route module (used for `lang` / parse filename). */
|
||||
routeFilePath: string;
|
||||
framework: Framework;
|
||||
/** Source text (registry component output or app source) containing a `$routeHandler` call. */
|
||||
compiledContent: string;
|
||||
}
|
||||
|
||||
export function buildRouteHandlerFromString(options: BuildRouteHandlerFileOptions): string {
|
||||
const { route, routeFilePath, framework, compiledContent } = options;
|
||||
|
||||
const lang = path.extname(routeFilePath) === '.tsx' ? 'tsx' : 'ts';
|
||||
const result = parseSync(routeFilePath, compiledContent, { lang, astType: 'ts' });
|
||||
if (result.errors.length > 0) {
|
||||
throw new Error(
|
||||
`route-handler.build: failed to parse ${routeFilePath}:\n${result.errors.map((e) => e.message).join('\n')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const program = result.program;
|
||||
const s = new MagicString(options.compiledContent);
|
||||
buildRouteHandlerFile(route, routeFilePath, framework, program, s);
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
const sample = `import { $routeHandler } from '@fumadocs/cli/registry/macros/route-handler';
|
||||
|
||||
export const handler = $routeHandler(
|
||||
{ methods: ['GET', 'POST'], params: ['id'] },
|
||||
async (req, p) => Response.json({ id: p.id }),
|
||||
);
|
||||
`;
|
||||
|
||||
test('next: native verb exports, inlined body, params from ctx', () => {
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/posts/[id]',
|
||||
routeFilePath: '/proj/app/api/posts/[id]/route.ts',
|
||||
framework: 'next',
|
||||
compiledContent: sample,
|
||||
});
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
"
|
||||
export async function GET(req: Request, ctx: RouteContext<"/api/posts/[id]">) {
|
||||
const p = {
|
||||
id: (await ctx.params).id
|
||||
};
|
||||
return Response.json({ id: p.id });
|
||||
}
|
||||
|
||||
export async function POST(req: Request, ctx: RouteContext<"/api/posts/[id]">) {
|
||||
const p = {
|
||||
id: (await ctx.params).id
|
||||
};
|
||||
return Response.json({ id: p.id });
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test('next: inlines dynamic + params object when route has segments', () => {
|
||||
const src = `import { $routeHandler } from '@fumadocs/cli/registry/macros/route-handler';
|
||||
export const h = $routeHandler(
|
||||
{ methods: ['GET'], params: ['id'], catchAll: 'rest' },
|
||||
async (request, p) => Response.json({ request, p }),
|
||||
);
|
||||
`;
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/items/[id]/[...rest]',
|
||||
routeFilePath: '/proj/app/api/items/[id]/[...rest]/route.ts',
|
||||
framework: 'next',
|
||||
compiledContent: src,
|
||||
});
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
"export async function GET(request: Request, ctx: RouteContext<"/api/items/[id]/[...rest]">) {
|
||||
const p = {
|
||||
id: (await ctx.params).id,
|
||||
rest: (await ctx.params).rest
|
||||
};
|
||||
return Response.json({ request, p });
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test('tanstack-start: createFileRoute + per-verb handlers with ctx.request alias', () => {
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/posts/[id]',
|
||||
routeFilePath: '/proj/routes/api.posts.$id.ts',
|
||||
framework: 'tanstack-start',
|
||||
compiledContent: sample,
|
||||
});
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
"import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute("/api/posts/$id")({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async (ctx) => {
|
||||
const req = ctx.request;
|
||||
const p = {
|
||||
id: ctx.params.id
|
||||
};
|
||||
return Response.json({ id: p.id });
|
||||
},
|
||||
POST: async (ctx) => {
|
||||
const req = ctx.request;
|
||||
const p = {
|
||||
id: ctx.params.id
|
||||
};
|
||||
return Response.json({ id: p.id });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test('react-router: async loader + action, args.request alias', () => {
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/posts/[id]',
|
||||
routeFilePath: '/proj/routes/api/posts/$id.ts',
|
||||
framework: 'react-router',
|
||||
compiledContent: sample,
|
||||
});
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
"import type { Route } from './+types/api.posts.$id';
|
||||
|
||||
export async function loader(args: Route.LoaderArgs) {
|
||||
const req = args.request;
|
||||
const p = {
|
||||
id: args.params.id
|
||||
};
|
||||
return Response.json({ id: p.id });
|
||||
}
|
||||
|
||||
export async function action(args: Route.ActionArgs) {
|
||||
const req = args.request;
|
||||
const p = {
|
||||
id: args.params.id
|
||||
};
|
||||
return Response.json({ id: p.id });
|
||||
}
|
||||
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test('react-router: GET-only omits action', () => {
|
||||
const getOnly = `import { $routeHandler } from '@fumadocs/cli/registry/macros/route-handler';
|
||||
|
||||
export const handler = $routeHandler(
|
||||
{ methods: ['GET'], params: [] },
|
||||
async () => new Response('ok'),
|
||||
);
|
||||
`;
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/x',
|
||||
routeFilePath: '/proj/routes/api/x.ts',
|
||||
framework: 'react-router',
|
||||
compiledContent: getOnly,
|
||||
});
|
||||
expect(out).toMatchInlineSnapshot(`
|
||||
"import type { Route } from './+types/api.x';
|
||||
|
||||
export async function loader(args: Route.LoaderArgs) {
|
||||
const request = args.request;
|
||||
return new Response('ok');
|
||||
}
|
||||
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
test('no-op when there is no $routeHandler import', () => {
|
||||
const src = `export const x = 1;
|
||||
`;
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/x',
|
||||
routeFilePath: '/proj/app/api/x/route.ts',
|
||||
framework: 'next',
|
||||
compiledContent: src,
|
||||
});
|
||||
expect(out).toBe(src);
|
||||
});
|
||||
|
||||
test('no-op when $routeHandler is imported but never called', () => {
|
||||
const src = `import { $routeHandler } from '@fumadocs/cli/registry/macros/route-handler';
|
||||
|
||||
export const setup = true;
|
||||
`;
|
||||
const out = buildRouteHandlerFromString({
|
||||
route: 'api/x',
|
||||
routeFilePath: '/proj/app/api/x/route.ts',
|
||||
framework: 'next',
|
||||
compiledContent: src,
|
||||
});
|
||||
expect(out).toBe(src);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
'./src/{index,config}.ts',
|
||||
'./src/registry/{client,schema}.ts',
|
||||
'./src/registry/installer/index.ts',
|
||||
'./src/registry/macros/route-handler.ts',
|
||||
'./src/build/index.ts',
|
||||
],
|
||||
format: 'esm',
|
||||
@@ -25,17 +26,9 @@ export default defineConfig({
|
||||
console.log('JSON schema generated');
|
||||
await fs.mkdir('dist/schema', { recursive: true });
|
||||
await fs.writeFile(
|
||||
'dist/schema/src.json',
|
||||
'dist/schema.json',
|
||||
JSON.stringify(
|
||||
z.toJSONSchema(createConfigSchema(true), {
|
||||
io: 'input',
|
||||
}),
|
||||
),
|
||||
);
|
||||
await fs.writeFile(
|
||||
'dist/schema/default.json',
|
||||
JSON.stringify(
|
||||
z.toJSONSchema(createConfigSchema(false), {
|
||||
z.toJSONSchema(createConfigSchema(), {
|
||||
io: 'input',
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ export function resolveExternal(
|
||||
if (ref.type === 'file') {
|
||||
const file = path.relative(toRegistryDir, ref.file).replaceAll(path.sep, '/');
|
||||
|
||||
if (/^contexts\//.test(file) || /^utils\/use-/.test(file)) {
|
||||
if (file.startsWith('contexts/') || file.startsWith('utils/use-')) {
|
||||
return {
|
||||
dep: toPackageName,
|
||||
type: 'dependency',
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"extends": "tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext"
|
||||
"target": "esnext",
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "eslint.config.mjs"]
|
||||
}
|
||||
|
||||
Generated
+27
-24
@@ -145,7 +145,7 @@ importers:
|
||||
version: 12.38.0([email protected]([email protected]))([email protected])
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6([email protected]([email protected]))([email protected])
|
||||
@@ -251,7 +251,7 @@ importers:
|
||||
version: 4.0.3
|
||||
mcp-handler:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@modelcontextprotocol/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 1.1.0(@modelcontextprotocol/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
next-validate-link:
|
||||
specifier: ^1.6.4
|
||||
version: 1.6.4
|
||||
@@ -345,7 +345,7 @@ importers:
|
||||
version: 1.7.0([email protected])
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -394,7 +394,7 @@ importers:
|
||||
version: link:../../packages/radix-ui
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -440,7 +440,7 @@ importers:
|
||||
version: link:../../packages/radix-ui
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -477,7 +477,7 @@ importers:
|
||||
version: 5.2.17(@types/[email protected])([email protected])
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -526,7 +526,7 @@ importers:
|
||||
version: 1.7.0([email protected])
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -575,7 +575,7 @@ importers:
|
||||
version: link:../../packages/radix-ui
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -624,7 +624,7 @@ importers:
|
||||
version: 1.7.0([email protected])
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -679,7 +679,7 @@ importers:
|
||||
version: link:../../packages/radix-ui
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -725,7 +725,7 @@ importers:
|
||||
version: link:../../packages/radix-ui
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -777,7 +777,7 @@ importers:
|
||||
version: link:../../packages/radix-ui
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -1577,6 +1577,9 @@ importers:
|
||||
specifier: 1.0.0-alpha.6
|
||||
version: 1.0.0-alpha.6(@types/[email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected])([email protected]([email protected])))([email protected])([email protected])([email protected])([email protected])
|
||||
devDependencies:
|
||||
'@fumadocs/cli':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/cli
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
@@ -1682,7 +1685,7 @@ importers:
|
||||
version: 12.38.0([email protected]([email protected]))([email protected])
|
||||
next:
|
||||
specifier: 16.x.x
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6([email protected]([email protected]))([email protected])
|
||||
@@ -1978,7 +1981,7 @@ importers:
|
||||
version: 1.7.0([email protected])
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react-router:
|
||||
specifier: ^7.13.2
|
||||
version: 7.13.2([email protected]([email protected]))([email protected])
|
||||
@@ -2276,7 +2279,7 @@ importers:
|
||||
version: 3.1.0
|
||||
next:
|
||||
specifier: 16.2.1
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4
|
||||
@@ -2794,7 +2797,7 @@ importers:
|
||||
version: 12.38.0([email protected]([email protected]))([email protected])
|
||||
next:
|
||||
specifier: 16.x.x
|
||||
version: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
version: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6([email protected]([email protected]))([email protected])
|
||||
@@ -7647,6 +7650,7 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA==}
|
||||
cpu: [arm64, x64]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@@ -13136,7 +13140,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@content-collections/core': 0.14.3([email protected])
|
||||
'@content-collections/integrations': 0.5.0(@content-collections/[email protected]([email protected]))
|
||||
next: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
next: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
|
||||
'@dimforge/[email protected]': {}
|
||||
|
||||
@@ -18457,7 +18461,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/mdx': 2.0.13
|
||||
next: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
next: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
react: 19.2.4
|
||||
vite: 8.0.3(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
vue: 3.5.31([email protected])
|
||||
@@ -19270,14 +19274,14 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected](@modelcontextprotocol/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])):
|
||||
[email protected](@modelcontextprotocol/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])):
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk': 1.28.0([email protected])
|
||||
chalk: 5.6.2
|
||||
commander: 11.1.0
|
||||
redis: 4.7.1
|
||||
optionalDependencies:
|
||||
next: 16.2.1(@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
next: 16.2.1(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected])
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -20019,7 +20023,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]):
|
||||
[email protected](@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@next/env': 16.2.1
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -20028,7 +20032,7 @@ snapshots:
|
||||
postcss: 8.4.31
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4([email protected])
|
||||
styled-jsx: 5.1.6(@babel/[email protected])([email protected])([email protected])
|
||||
styled-jsx: 5.1.6([email protected])([email protected])
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.2.1
|
||||
'@next/swc-darwin-x64': 16.2.1
|
||||
@@ -21661,12 +21665,11 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
|
||||
[email protected](@babel/[email protected])([email protected])([email protected]):
|
||||
[email protected]([email protected])([email protected]):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.4
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.0
|
||||
babel-plugin-macros: 3.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
Reference in New Issue
Block a user