mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
CLI: Migrate to oxc for AST manipulation
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@fumadocs/cli': patch
|
||||
---
|
||||
|
||||
Migrate to oxc for AST manipulation
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Project } from 'ts-morph';
|
||||
|
||||
export function createEmptyProject(): Project {
|
||||
return new Project({
|
||||
compilerOptions: {},
|
||||
});
|
||||
}
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+1059
-408
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user