diff --git a/.changeset/funky-monkeys-juggle.md b/.changeset/funky-monkeys-juggle.md new file mode 100644 index 0000000..7afc1e2 --- /dev/null +++ b/.changeset/funky-monkeys-juggle.md @@ -0,0 +1,5 @@ +--- +"docflow": patch +--- + +resolve absolute import paths in extracted type signatures diff --git a/packages/cli/src/core/parser/source/extract-signature.ts b/packages/cli/src/core/parser/source/extract-signature.ts index 22f9732..eba3b43 100644 --- a/packages/cli/src/core/parser/source/extract-signature.ts +++ b/packages/cli/src/core/parser/source/extract-signature.ts @@ -6,8 +6,21 @@ import { FunctionDeclaration, TypeAliasDeclaration, EnumDeclaration, + TypeFormatFlags, } from "ts-morph"; +/** + * Prevents `typeof import("/absolute/path").Foo` for symbols not directly imported in the current scope. + * `UseAliasDefinedOutsideCurrentScope` resolves out-of-scope symbols by their alias name instead. + * + * @see {@link https://github.com/dsherret/ts-morph/issues/453} + */ +const TYPE_FORMAT_FLAGS = + TypeFormatFlags.NoTruncation | + TypeFormatFlags.WriteTypeArgumentsOfSignature | + TypeFormatFlags.UseTypeOfFunction | + TypeFormatFlags.UseAliasDefinedOutsideCurrentScope; + export function extractSignature(declaration: Node): string | undefined { if (Node.isFunctionDeclaration(declaration)) { return formatFunctionSignature(declaration); @@ -51,7 +64,7 @@ function formatFunctionSignature(node: FunctionDeclaration): string { .getParameters() .map(p => p.getText()) .join(", "); - const returnType = node.getReturnType().getText(node); + const returnType = node.getReturnType().getText(node, TYPE_FORMAT_FLAGS); return `function ${name}${typeParams ? `<${typeParams}>` : ""}(${params}): ${returnType};`; } @@ -75,7 +88,7 @@ function formatArrowFunctionSignature(node: VariableDeclaration): string { .getParameters() .map(p => p.getText()) .join(", "); - const returnType = initializer.getReturnType().getText(node); + const returnType = initializer.getReturnType().getText(node, TYPE_FORMAT_FLAGS); return `${declarationKind} ${name}: ${typeParams ? `<${typeParams}>` : ""}(${params}) => ${returnType};`; } @@ -85,7 +98,7 @@ function formatVariableSignature(node: VariableDeclaration): string { const name = node.getName(); const variableStatement = node.getVariableStatement(); const declarationKind = variableStatement?.getDeclarationKind() ?? "const"; - const type = node.getType().getText(node); + const type = node.getType().getText(node, TYPE_FORMAT_FLAGS); return `${declarationKind} ${name}: ${type};`; } diff --git a/packages/cli/src/tests/core/parser/source/extract-signature.spec.ts b/packages/cli/src/tests/core/parser/source/extract-signature.spec.ts index 9bbce0f..e3a3f32 100644 --- a/packages/cli/src/tests/core/parser/source/extract-signature.spec.ts +++ b/packages/cli/src/tests/core/parser/source/extract-signature.spec.ts @@ -147,6 +147,22 @@ describe("extractSignature", () => { expect(signature).toContain("/** The database URL. */"); }); + it("should not include absolute file paths in nested object variable types", async () => { + const tsConfigPath = getTsConfigPath(workspace.root, "packages/core"); + const project = getTsProject(tsConfigPath); + + const toolbarFile = project.getSourceFiles().find(sf => sf.getFilePath().includes("toolbar.ts")); + + const toolbar = toolbarFile?.getVariableDeclaration("Toolbar"); + expect(toolbar).toBeDefined(); + + const signature = extractSignature(toolbar!); + expect(signature).not.toMatch(/import\("[^"]*\/[^"]*"\)/); + expect(signature).toContain("ToolbarMenu"); + expect(signature).toContain("CopyAction"); + expect(signature).toContain("PasteAction"); + }); + it("should not include absolute file paths in return types", async () => { const tsConfigPath = getTsConfigPath(workspace.root, "packages/core"); const project = getTsProject(tsConfigPath); diff --git a/packages/cli/src/tests/utils/package-creators.ts b/packages/cli/src/tests/utils/package-creators.ts index a74f171..9ad0fc7 100644 --- a/packages/cli/src/tests/utils/package-creators.ts +++ b/packages/cli/src/tests/utils/package-creators.ts @@ -52,7 +52,8 @@ export function getDefaultConfig(): UserConfig { export * from './math.js'; export * from './string.js'; -export * from './classes.js';` +export * from './classes.js'; +export * from './toolbar.js';` ); await workspace.write( @@ -149,6 +150,52 @@ export function toLower(str: string): string { export const greet = (name: string) => \`Hello, \${name}!\`;` ); + // Nested object with barrel-exported typeof references (DevTool pattern) + // ToolbarMenu is directly imported → in scope → resolved by name + // Actions is imported as barrel object, CopyAction/PasteAction inside are NOT directly imported + await workspace.write( + "packages/core/src/toolbar/copy-action.ts", + `export class CopyAction { + execute(): void {} +}` + ); + + await workspace.write( + "packages/core/src/toolbar/paste-action.ts", + `export class PasteAction { + execute(): void {} +}` + ); + + await workspace.write( + "packages/core/src/toolbar/actions.ts", + `import { CopyAction } from './copy-action'; +import { PasteAction } from './paste-action'; + +export const Actions = { + Copy: CopyAction, + Paste: PasteAction, +};` + ); + + await workspace.write( + "packages/core/src/toolbar/toolbar-menu.ts", + `export class ToolbarMenu { + open(): void {} +}` + ); + + await workspace.write( + "packages/core/src/toolbar.ts", + `import { ToolbarMenu } from './toolbar/toolbar-menu'; +import { Actions } from './toolbar/actions'; + +export const Toolbar = { + Menu: ToolbarMenu, + Actions, +};` + ); + await workspace.write( "packages/core/src/classes.ts", `import { UserConfig } from '@libs/types';