Skip to content

Conversation

@DaxServer
Copy link
Owner

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Public API now accepts a single options object and supports inputs via file path or inline source code, with improved relative import handling.
    • Generated code consolidates TypeBox imports into a single statement.
  • Documentation

    • Architecture and usage docs overhauled to describe explicit input handling and the options-based workflow, with updated examples and guidance.
  • Tests

    • Added comprehensive tests for input handling, path resolution, and error cases.
  • Chores

    • Formatting script now uses Prettier cache.
    • Removed an unused dependency.

Walkthrough

Refactors generateCode to accept a GenerateCodeOptions object (filePath or sourceCode, callerFile, project, exportEverything). Adds src/input-handler.ts to produce a ts-morph SourceFile from those inputs. Updates docs, tests, and utilities to the new options-based API; consolidates TypeBox import and adjusts package.json format script and dependencies.

Changes

Cohort / File(s) Summary
Architecture docs
ARCHITECTURE.md
Reorganized around an explicit Input Handling System and InputOptions, updated public API examples to options-based generateCode, renamed temp output to output.ts, removed TSConfig-centric handling, expanded input processing/usage sections.
Build/config
package.json
format script changed to use prettier --cache --write .; removed tsconfck by deleting the dependencies block.
Input handling module & tests
src/input-handler.ts, tests/ts-morph/input-handler.test.ts
New InputOptions interface and createSourceFileFromInput(options) to validate input, resolve file paths, detect relative imports, and create or load a ts-morph SourceFile; extensive tests for resolution, validation, and relative-import semantics.
Codegen API refactor
src/ts-morph-codegen.ts
Added GenerateCodeOptions (extends InputOptions + exportEverything?); generateCode now accepts options, uses createSourceFileFromInput, creates a fresh output Project/source (output.ts), always injects import { Type, type Static }, and threads exportEverything through parsers.
Tests/utilities updated
tests/integration/wikibase/wikibase.ts, tests/ts-morph/utils.ts
Updated calls to generateCode to pass options (sourceCode, filePath, callerFile, project, exportEverything); consolidated TypeBox import into a single combined import line.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant GC as generateCode
  participant IH as input-handler
  participant InProj as ts-morph Project (input)
  participant OutProj as ts-morph Project (output)

  Caller->>GC: generateCode({ filePath | sourceCode, callerFile?, exportEverything? })
  GC->>IH: createSourceFileFromInput(options)
  IH->>InProj: ensure Project (provided or new)
  alt sourceCode path
    IH->>InProj: createSourceFile('temp.ts' or virtual path, sourceCode)
    IH->>IH: validate relative imports vs callerFile
  else filePath path
    IH->>IH: resolveFilePath(filePath, callerFile)
    IH->>InProj: addSourceFileAtPath(resolvedPath)
  end
  IH-->>GC: SourceFile (input)

  GC->>OutProj: new Project().createSourceFile('output.ts')
  GC->>OutProj: add import { Type, type Static } from "@sinclair/typebox"
  GC->>OutProj: parse input (imports → enums → type aliases → interfaces → functions)
  GC->>OutProj: emit TypeBox types (respecting exportEverything)
  GC-->>Caller: return output.ts full text (string)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sourcecode-file-inputs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (7)
package.json (1)

30-30: Prettier cache: good change; consider adding a check script for CI.

Add a non-mutating check to fail CI on unformatted code.

   "scripts": {
-    "format": "prettier --cache --write .",
+    "format": "prettier --cache --write .",
+    "format:check": "prettier --cache --check .",
     "typecheck": "tsc --noEmit",
     "lint": "eslint"
   },
tests/ts-morph/input-handler.test.ts (2)

25-66: Broaden relative-import detection test coverage.

Add cases for export ... from and dynamic import() to lock behavior (currently only import ... from is covered).

   describe('hasRelativeImports detection', () => {
+    test('detects export-from with relative path', () => {
+      const code = `export { x } from './x'`
+      expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow(
+        'Relative imports are not supported when providing code as string'
+      )
+    })
+
+    test('detects dynamic import with relative path', () => {
+      const code = `async function load(){ await import('./x') }`
+      expect(() => createSourceFileFromInput({ sourceCode: code })).toThrow(
+        'Relative imports are not supported when providing code as string'
+      )
+    })
   })

205-281: Add a test ensuring relative imports resolve when callerFile is provided.

Currently we only assert that code creation is allowed; also verify module-specifier resolution context is the caller’s directory.

   describe('createSourceFileFromInput function', () => {
+    test('resolves relative imports using callerFile directory', () => {
+      const callerDir = join(tempDir, 'caller')
+      mkdirSync(callerDir, { recursive: true })
+      // Create a relative dependency in the caller’s directory
+      writeFileSync(join(callerDir, 'dep.ts'), 'export const n = 1')
+      const code = `import { n } from './dep'; export const m = n`
+      const sourceFile = createSourceFileFromInput({
+        sourceCode: code,
+        callerFile: join(callerDir, 'entry.ts'),
+      })
+      // Sanity checks
+      expect(sourceFile.getImportDeclarations()[0]?.getModuleSpecifierValue()).toBe('./dep')
+    })
   })
src/ts-morph-codegen.ts (3)

19-24: API redesign via destructured options: consider a thin backward-compat wrapper

This is a breaking change for any callers passing a SourceFile. If you want a smoother migration, add an optional shim that detects a SourceFile and delegates to the new path. Low effort, high DX value.

Here’s a minimal wrapper you could place near the new function (outside this hunk) to keep old usages limping along while you deprecate:

// Back-compat shim (deprecated)
export async function generateCode(
  sourceFileOrOptions: any,
  legacy?: { exportEverything?: boolean } = { exportEverything: false }
): Promise<string> {
  if (sourceFileOrOptions && typeof sourceFileOrOptions.getFullText === 'function') {
    // Called with SourceFile (legacy)
    const sourceFile = sourceFileOrOptions
    return generateCodeNew({
      sourceCode: sourceFile.getFullText(),
      callerFile: sourceFile.getFilePath(),
      project: sourceFile.getProject(),
      exportEverything: legacy.exportEverything ?? false,
    })
  }
  // Called with new options
  return generateCodeNew(sourceFileOrOptions)
}
// Rename the new impl to avoid name clash in this example
export const generateCodeNew = async (opts: GenerateCodeOptions): Promise<string> => { /* ... */ }

31-33: Avoid hardcoded output filename; reuse manipulation settings from input project

Creating a brand-new Project is fine, but you lose any customized manipulationSettings or formatting alignment with the input project. Also, hardcoding 'output.ts' makes it harder to differentiate multi-file generations.

Apply this diff to make the output file name configurable and inherit manipulation settings:

 export interface GenerateCodeOptions extends InputOptions {
   exportEverything?: boolean
+  outputFileName?: string
 }
 
-export const generateCode = async ({
+export const generateCode = async ({
   sourceCode,
   filePath,
   exportEverything = false,
   ...options
 }: GenerateCodeOptions): Promise<string> => {
   const sourceFile = createSourceFileFromInput({
     sourceCode,
     filePath,
     ...options,
   })
   const processedTypes = new Set<string>()
-  const newSourceFile = new Project().createSourceFile('output.ts', '', {
+  const outputFileName = options.outputFileName ?? 'output.ts'
+  const inputProject = sourceFile.getProject()
+  const newProject = new Project({
+    manipulationSettings: inputProject.getCompilerOptions(), // or copy specific manipulation settings if you set them
+  })
+  const newSourceFile = newProject.createSourceFile(outputFileName, '', {
     overwrite: true,
   })

Note: if you keep a separate project for output (good to avoid polluting the input project), at least align formatting/manipulation settings.


35-45: Per-specifier type import needs TS ≥ 4.5; prefer two imports for broader compatibility

{ type Static } syntax requires TS 4.5+. If consumers might be on older TS, emit two imports instead: value-only for Type and import type for Static (supported since TS 3.8). Functionality is identical and safer.

-  newSourceFile.addImportDeclaration({
-    moduleSpecifier: '@sinclair/typebox',
-    namedImports: [
-      'Type',
-      {
-        name: 'Static',
-        isTypeOnly: true,
-      },
-    ],
-  })
+  newSourceFile.addImportDeclaration({
+    moduleSpecifier: '@sinclair/typebox',
+    namedImports: ['Type'],
+  })
+  newSourceFile.addImportDeclaration({
+    moduleSpecifier: '@sinclair/typebox',
+    isTypeOnly: true,
+    namedImports: ['Static'],
+  })

Confirm your minimum supported TS version. If it’s TS 5.x across the board, your current single-import approach is fine—just document the requirement.

ARCHITECTURE.md (1)

72-73: Hyphenate “full-text” when used as a compound adjective

Very minor clarity fix.

-9.  **Output**: Finally, the full text content of the newly generated `output.ts` source file (which now contains all the TypeBox schemas and static types) is returned as a string.
+9.  **Output**: Finally, the full-text content of the newly generated `output.ts` source file (which now contains all the TypeBox schemas and static types) is returned as a string.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0c6824a and 9d142af.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • ARCHITECTURE.md (3 hunks)
  • package.json (1 hunks)
  • src/input-handler.ts (1 hunks)
  • src/ts-morph-codegen.ts (4 hunks)
  • tests/integration/wikibase/wikibase.ts (1 hunks)
  • tests/ts-morph/input-handler.test.ts (1 hunks)
  • tests/ts-morph/utils.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tests/ts-morph/input-handler.test.ts (1)
src/input-handler.ts (1)
  • createSourceFileFromInput (61-90)
tests/ts-morph/utils.ts (1)
src/ts-morph-codegen.ts (1)
  • generateCode (19-117)
tests/integration/wikibase/wikibase.ts (1)
src/ts-morph-codegen.ts (1)
  • generateCode (19-117)
src/ts-morph-codegen.ts (1)
src/input-handler.ts (2)
  • InputOptions (5-10)
  • createSourceFileFromInput (61-90)
🪛 GitHub Check: CodeQL
src/input-handler.ts

[failure] 14-14: Polynomial regular expression used on uncontrolled data
This regular expression that depends on library input may run slow on strings starting with 'import ' and with many repetitions of ' '.

🪛 markdownlint-cli2 (0.17.2)
ARCHITECTURE.md

26-26: Link fragments should be valid

(MD051, link-fragments)

🪛 LanguageTool
ARCHITECTURE.md

[uncategorized] ~72-~72: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... aliases. 9. Output: Finally, the full text content of the newly generated `output....

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[grammar] ~129-~129: There might be a mistake here.
Context: ...either file paths or source code strings 2. Path Resolution: Handles both absolute...

(QB_NEW_EN)


[grammar] ~130-~130: There might be a mistake here.
Context: ...lative file paths with proper validation 3. Relative Import Validation: Prevents r...

(QB_NEW_EN)


[grammar] ~131-~131: There might be a mistake here.
Context: ...nless a callerFile context is provided 4. Project Context Sharing: Supports pass...

(QB_NEW_EN)


[grammar] ~132-~132: There might be a mistake here.
Context: ...es to maintain import resolution context 5. Error Handling: Provides clear error m...

(QB_NEW_EN)

🔇 Additional comments (10)
tests/integration/wikibase/wikibase.ts (1)

11-15: API migration looks correct.

Passing sourceCode plus callerFile matches the new InputOptions flow; exportEverything: true preserved. No issues spotted.

tests/ts-morph/utils.ts (2)

6-6: Import string aligns with generator’s type-only import.

type Static matches the codegen’s isTypeOnly: true. Good.


21-26: Good: exercising new options shape and reusing the caller’s Project.

Passing { sourceCode, callerFile, project } will let input handling reuse AST context. No blocking issues.

src/ts-morph-codegen.ts (4)

15-17: Public options surface: add GenerateCodeOptions to package exports (if intended)

The interface looks good. If this is part of your public API, ensure it’s re-exported from the package entry (e.g., src/index.ts) to avoid forcing deep imports.


25-29: Input creation LGTM

Good use of createSourceFileFromInput to normalize filePath vs sourceCode flows, and forwarding the rest options for validation. Clear and robust.


47-52: Shared printer and parser options look solid

Good single ts.createPrinter() instance per run and clear propagation of exportEverything into parsers.


1-4: Confirm TS path alias configuration and runtime support

  • tsconfig.json includes a paths mapping for internal modules:
    "paths": {
      "@daxserver/validation-schema-codegen/*": ["./src/*"],
      "@test-fixtures/*": ["./tests/*"]
    }
  • A search yielded no relative imports of input-handler, parsers, or utils; all internal imports use the package alias.
  • To avoid dual module instances at runtime, ensure your bundler (e.g., Rollup, Webpack) and your test runner (e.g., Bun) are configured to resolve the same alias mapping.

Review comment (updated):

Importing from your own package alias (@daxserver/validation-schema-codegen/...) is safe given the tsconfig.json paths mapping, but you must confirm that your build and test environments also honor this alias. If they don’t, switch these imports to relative paths (e.g., ./input-handler) to prevent duplicate module graphs or resolution surprises.

ARCHITECTURE.md (3)

58-59: Doc states “separate import declarations,” but code emits a single consolidated import

Current implementation adds a single import { Type, type Static } from "@sinclair/typebox" (or two imports if you adopt the compatibility suggestion). Update the wording to avoid confusion.

-2.  **Initialization**: A new in-memory `SourceFile` (`output.ts`) is created to build the generated code. Essential imports for TypeBox (`Type`, `Static`) are added as separate import declarations for better compatibility.
+2.  **Initialization**: A new in-memory `SourceFile` (`output.ts`) is created to build the generated code. A TypeBox import is added (value import for `Type` and type-only import for `Static`).

60-66: Flow still references ImportParser and omits InterfaceParser

Implementation no longer constructs an ImportParser; import-related type ordering is handled via DependencyCollector + TypeAliasParser. Also, InterfaceParser is instantiated in code but missing from the list here. Align the doc with reality.

Suggested text:

  • Step 3: “Parser Instantiation: Instances of EnumParser, TypeAliasParser, InterfaceParser, and FunctionDeclarationParser are created…”
  • Step 4: Replace Import Processing paragraph with: “Dependencies are resolved and ordered using DependencyCollector, which traverses import declarations and local types to compute a topological order for type aliases.”

110-126: InputOptions section reads well

The interface and explanations align with the implementation in src/input-handler.ts. Clear and helpful.

Comment on lines 12 to 15
const hasRelativeImports = (code: string): boolean => {
const relativeImportRegex = /import\s+.*?from\s+['"]\.[^'"]*['"]/g
return relativeImportRegex.test(code)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Regex can be vulnerable to performance issues and misses other relative-import forms. Replace with a line-anchored, multi-form detector.

The current /import\s+.*?from\s+['"]\.[^'"]*['"]/g risks catastrophic backtracking on crafted inputs and ignores export ... from and dynamic import(). Use a linear, line-anchored pattern without g, or parse with ts-morph.

-const hasRelativeImports = (code: string): boolean => {
-  const relativeImportRegex = /import\s+.*?from\s+['"]\.[^'"]*['"]/g
-  return relativeImportRegex.test(code)
-}
+const hasRelativeImports = (code: string): boolean => {
+  // Covers:
+  // - import ... from './x'
+  // - export ... from './x'
+  // - dynamic import('./x')
+  // Anchored to line starts; avoids greedy dot and global flag.
+  const re =
+    /^(?:\s*import\s+(?:[^'"]+?\s+from\s*)?['"]\.\.?\/[^'"]+['"]\s*;?|\s*export\s+[^'"]*from\s*['"]\.\.?\/[^'"]+['"]\s*;?|\s*import\(\s*['"]\.\.?\/[^'"]+['"]\s*\))/m
+  return re.test(code)
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hasRelativeImports = (code: string): boolean => {
const relativeImportRegex = /import\s+.*?from\s+['"]\.[^'"]*['"]/g
return relativeImportRegex.test(code)
}
const hasRelativeImports = (code: string): boolean => {
// Covers:
// - import ... from './x'
// - export ... from './x'
// - dynamic import('./x')
// Anchored to line starts; avoids greedy dot and global flag.
const re =
/^(?:\s*import\s+(?:[^'"]+?\s+from\s*)?['"]\.\.?\/[^'"]+['"]\s*;?|\s*export\s+[^'"]*from\s*['"]\.\.?\/[^'"]+['"]\s*;?|\s*import\(\s*['"]\.\.?\/[^'"]+['"]\s*\))/m
return re.test(code)
}
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 14-14: Polynomial regular expression used on uncontrolled data
This regular expression that depends on library input may run slow on strings starting with 'import ' and with many repetitions of ' '.

🤖 Prompt for AI Agents
In src/input-handler.ts around lines 12 to 15, the current regex risks
catastrophic backtracking and misses other relative-import forms; replace it
with a line-anchored, non-global detector that covers imports, exports and
dynamic imports or use a TypeScript parser. Specifically, remove the /g flag,
anchor the pattern to line starts (use multiline mode) and match the common
forms: import ... from './...', export ... from './...' and import('./...') with
a single linear regex, or better, switch to ts-morph/TypeScript AST to reliably
detect relative module specifiers.

Comment on lines 61 to 77
export const createSourceFileFromInput = (options: InputOptions): SourceFile => {
validateInputOptions(options)

const project = options.project || new Project()
const { filePath, sourceCode, callerFile } = options

if (sourceCode) {
// If callerFile is provided, it means this code came from an existing SourceFile
// and relative imports should be allowed
if (hasRelativeImports(sourceCode) && !callerFile) {
throw new Error(
'Relative imports are not supported when providing code as string. Only package imports from node_modules are allowed. Relative imports will be implemented in the future.',
)
}

return project.createSourceFile('temp.ts', sourceCode)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

When sourceCode is provided, use callerFile’s directory for the virtual file path to enable correct relative-resolution—without colliding with an existing file.

Currently we create 'temp.ts' at the project root, which breaks relative import resolution. Also avoid clobbering an existing SourceFile at callerFile if the caller passes the same Project.

-  if (sourceCode) {
+  if (sourceCode) {
     // If callerFile is provided, it means this code came from an existing SourceFile
     // and relative imports should be allowed
     if (hasRelativeImports(sourceCode) && !callerFile) {
       throw new Error(
         'Relative imports are not supported when providing code as string. Only package imports from node_modules are allowed. Relative imports will be implemented in the future.',
       )
     }
-
-    return project.createSourceFile('temp.ts', sourceCode)
+    const virtualPath = callerFile
+      ? resolve(dirname(callerFile), '__virtual__.ts')
+      : 'temp.ts'
+    return project.createSourceFile(virtualPath, sourceCode, { overwrite: true })
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/input-handler.ts around lines 61–77, when sourceCode is provided we
currently create a flat 'temp.ts' at the project root which breaks
relative-import resolution and can clobber an existing SourceFile in the same
Project; instead compute the virtual file path inside the callerFile's directory
(e.g. dirname(callerFile) + a unique virtual filename) so relative imports
resolve against the caller's folder, and ensure you do not reuse a path already
present in the Project (generate a unique name or append a counter/UUID if a
SourceFile at that path exists) before calling project.createSourceFile so you
neither collide with callerFile nor leave the file at project root.

Comment on lines +65 to 79
if (exportEverything) {
// When exporting everything, maintain original order
orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
options.exportEverything,
exportEverything,
)
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
} else {
// When not exporting everything, add local types first so filtering can detect their dependencies
dependencyCollector.addLocalTypes(localTypeAliases, sourceFile)
orderedDependencies = dependencyCollector.collectFromImports(
importDeclarations,
options.exportEverything,
exportEverything,
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Dependency ordering branches: behavior check

The “export everything” vs “selective export” paths differ in when local types are added/processed, which is intentional. Please confirm tests cover both branches (including mixed local/imported dependencies and shadowed names) so ordering remains deterministic.

You can sanity-scan tests to ensure both paths are exercised:

Also applies to: 88-99


🏁 Script executed:

#!/bin/bash
rg -nP "exportEverything\s*:\s*true|exportEverything\s*:\s*false|generateCode\(\s*{[^}]*exportEverything" -C2

Length of output: 713


Add tests for the exportEverything: false code path

It looks like all existing calls to generateCode({…, exportEverything: true }) exercise only the “export everything” branch. We need coverage of the selective-export branch (when exportEverything is false) to verify that local types are correctly ordered before imports, including scenarios with mixed local/imported dependencies and shadowed names.

Please add tests that:

  • Invoke generateCode({ sourceCode: /* … */, exportEverything: false, callerFile: /* … */ }).
  • Cover combinations of local type aliases and imported types.
  • Assert that the resulting dependency order matches the intended sequence (locals first, then imports).

Suggested locations to extend or add tests:

  • tests/integration/wikibase/wikibase.ts – currently only exercises exportEverything: true.
  • A new unit-test file under tests/unit/ts-morph-codegen/ focusing on both branches and edge cases.

Comment on lines 99 to 170
describe('resolveFilePath function', () => {
test('resolves absolute path that exists', () => {
writeFileSync(testFilePath, 'export type Test = string')

const sourceFile = createSourceFileFromInput({ filePath: testFilePath })

expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
})

test('throws error for absolute path that does not exist', () => {
const nonExistentPath = join(tempDir, 'non-existent.ts')

expect(() => createSourceFileFromInput({ filePath: nonExistentPath })).toThrow(
`Absolute path does not exist: ${nonExistentPath}`
)
})

test('resolves relative path from current working directory', () => {
const relativePath = 'temp-test-input-handler/test.ts'
writeFileSync(testFilePath, 'export type Test = string')

const sourceFile = createSourceFileFromInput({ filePath: relativePath })

expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
})

test('resolves relative path from callerFile directory', () => {
const callerFile = join(tempDir, 'caller.ts')
const relativeFile = join(tempDir, 'relative.ts')
writeFileSync(relativeFile, 'export type Test = string')

const sourceFile = createSourceFileFromInput({
filePath: './relative.ts',
callerFile
})

expect(sourceFile.getFilePath()).toBe(relativeFile as StandardizedFilePath)
})

test('throws error when relative path cannot be resolved', () => {
expect(() => createSourceFileFromInput({ filePath: './non-existent.ts' })).toThrow(
'Could not resolve path: ./non-existent.ts'
)
})

test('throws error when multiple resolutions found', () => {
// Create file in current directory
const currentDirFile = join(process.cwd(), 'duplicate.ts')
writeFileSync(currentDirFile, 'export type Test = string')

// Create file in caller directory
const callerDir = join(tempDir, 'caller')
mkdirSync(callerDir, { recursive: true })
const callerFile = join(callerDir, 'caller.ts')
const callerDirFile = join(callerDir, 'duplicate.ts')
writeFileSync(callerDirFile, 'export type Test = string')

expect(() => createSourceFileFromInput({
filePath: './duplicate.ts',
callerFile
})).toThrow('Multiple resolutions found for path: ./duplicate.ts')

// Cleanup
rmSync(currentDirFile, { force: true })
})

test('throws error when path is not a file', () => {
expect(() => createSourceFileFromInput({ filePath: tempDir })).toThrow(
`Path is not a file: ${tempDir}`
)
})
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Edge case: duplicate candidate paths can false-positive “multiple resolutions.”

If dirname(callerFile) === process.cwd(), the same path can be added twice and trigger the “multiple resolutions” error. Add a test to reproduce and dedupe candidates in implementation (see src/input-handler.ts comment).

   describe('resolveFilePath function', () => {
+    test('does not report multiple resolutions when callerDir equals CWD', () => {
+      // Create file in CWD and use a callerFile within the same dir
+      const sameDirFile = join(process.cwd(), 'same-dir.ts')
+      writeFileSync(sameDirFile, 'export type T = 1')
+      try {
+        const sourceFile = createSourceFileFromInput({
+          filePath: './same-dir.ts',
+          callerFile: join(process.cwd(), 'caller.ts'),
+        })
+        expect(sourceFile.getFilePath()).toBe(sameDirFile as StandardizedFilePath)
+      } finally {
+        rmSync(sameDirFile, { force: true })
+      }
+    })
   })

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Locate the input-handler implementation file
file=$(fd input-handler.ts | head -n1)
echo "Implementation file found: $file"

# Show context around createSourceFileFromInput
echo "----- createSourceFileFromInput definition -----"
rg -nP 'function\s+createSourceFileFromInput\b' -t ts -C5 "$file" || true

# Show context around resolveFilePath
echo "----- resolveFilePath helper -----"
rg -nP 'function\s+resolveFilePath\b' -t ts -C5 "$file" || true

# Search for any deduplication logic (e.g., Set usage or uniq)
echo "----- dedupe logic search -----"
rg -nP '\\bnew\s+Set\\b|\\.filter\\(|\\.uniq\\(' -t ts -C5 "$file" || true

Length of output: 774


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Print the first 200 lines of the input-handler implementation to locate relevant functions
echo "----- Beginning of src/input-handler.ts -----"
sed -n '1,200p' src/input-handler.ts

# Search for createSourceFileFromInput usages to find its definition
echo "----- Searching for createSourceFileFromInput definition -----"
grep -nR "createSourceFileFromInput" -n src/input-handler.ts

# Search for resolveFilePath to view its implementation
echo "----- Searching for resolveFilePath implementation -----"
grep -nR "resolveFilePath" -n src/input-handler.ts

# Look for any explicit deduplication logic (Set, uniq, filter) in this file
echo "----- Checking for deduplication patterns -----"
grep -nE "new +Set|\\.uniq|\\.filter" -n src/input-handler.ts || echo "No dedupe patterns found"

Length of output: 3641


Add deduplication in resolveFilePath and include CWD edge-case test

To prevent false-positive “multiple resolutions” when callerFile is in the current working directory, we need to:

• In src/input-handler.ts, dedupe possiblePaths before filtering for existence.
• In tests/ts-morph/input-handler.test.ts, add a test that verifies resolution succeeds (instead of throwing) when callerDir === process.cwd().

Locations and suggested diffs:

  • src/input-handler.ts (inside resolveFilePath):
   // After pushing callerDir and CWD paths:
   possiblePaths.push(resolve(process.cwd(), input))

+  // Remove duplicate entries to avoid false-positive multiple resolution errors
+  const uniquePaths = Array.from(new Set(possiblePaths))
-
-  const existingPaths = possiblePaths.filter((path) => existsSync(path))
+  const existingPaths = uniquePaths.filter((path) => existsSync(path))

   if (existingPaths.length === 0) {
     throw new Error(`Could not resolve path: ${input}. Tried: ${possiblePaths.join(', ')}`)
   }
  • tests/ts-morph/input-handler.test.ts (inside the describe('resolveFilePath function', …) block, after existing tests):
   test('throws error when path is not a file', () => {
     expect(() => createSourceFileFromInput({ filePath: tempDir })).toThrow(
       `Path is not a file: ${tempDir}`
     )
   })

+  test('does not report multiple resolutions when callerDir equals CWD', () => {
+    // Create file in CWD and use a callerFile within the same dir
+    const sameDirFile = join(process.cwd(), 'same-dir.ts')
+    writeFileSync(sameDirFile, 'export type T = 1')
+    try {
+      const sourceFile = createSourceFileFromInput({
+        filePath: './same-dir.ts',
+        callerFile: join(process.cwd(), 'caller.ts'),
+      })
+      expect(sourceFile.getFilePath()).toBe(sameDirFile as StandardizedFilePath)
+    } finally {
+      rmSync(sameDirFile, { force: true })
+    }
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe('resolveFilePath function', () => {
test('resolves absolute path that exists', () => {
writeFileSync(testFilePath, 'export type Test = string')
const sourceFile = createSourceFileFromInput({ filePath: testFilePath })
expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
})
test('throws error for absolute path that does not exist', () => {
const nonExistentPath = join(tempDir, 'non-existent.ts')
expect(() => createSourceFileFromInput({ filePath: nonExistentPath })).toThrow(
`Absolute path does not exist: ${nonExistentPath}`
)
})
test('resolves relative path from current working directory', () => {
const relativePath = 'temp-test-input-handler/test.ts'
writeFileSync(testFilePath, 'export type Test = string')
const sourceFile = createSourceFileFromInput({ filePath: relativePath })
expect(sourceFile.getFilePath()).toBe(testFilePath as StandardizedFilePath)
})
test('resolves relative path from callerFile directory', () => {
const callerFile = join(tempDir, 'caller.ts')
const relativeFile = join(tempDir, 'relative.ts')
writeFileSync(relativeFile, 'export type Test = string')
const sourceFile = createSourceFileFromInput({
filePath: './relative.ts',
callerFile
})
expect(sourceFile.getFilePath()).toBe(relativeFile as StandardizedFilePath)
})
test('throws error when relative path cannot be resolved', () => {
expect(() => createSourceFileFromInput({ filePath: './non-existent.ts' })).toThrow(
'Could not resolve path: ./non-existent.ts'
)
})
test('throws error when multiple resolutions found', () => {
// Create file in current directory
const currentDirFile = join(process.cwd(), 'duplicate.ts')
writeFileSync(currentDirFile, 'export type Test = string')
// Create file in caller directory
const callerDir = join(tempDir, 'caller')
mkdirSync(callerDir, { recursive: true })
const callerFile = join(callerDir, 'caller.ts')
const callerDirFile = join(callerDir, 'duplicate.ts')
writeFileSync(callerDirFile, 'export type Test = string')
expect(() => createSourceFileFromInput({
filePath: './duplicate.ts',
callerFile
})).toThrow('Multiple resolutions found for path: ./duplicate.ts')
// Cleanup
rmSync(currentDirFile, { force: true })
})
test('throws error when path is not a file', () => {
expect(() => createSourceFileFromInput({ filePath: tempDir })).toThrow(
`Path is not a file: ${tempDir}`
)
})
})
test('throws error when path is not a file', () => {
expect(() => createSourceFileFromInput({ filePath: tempDir })).toThrow(
`Path is not a file: ${tempDir}`
)
})
test('does not report multiple resolutions when callerDir equals CWD', () => {
// Create file in CWD and use a callerFile within the same dir
const sameDirFile = join(process.cwd(), 'same-dir.ts')
writeFileSync(sameDirFile, 'export type T = 1')
try {
const sourceFile = createSourceFileFromInput({
filePath: './same-dir.ts',
callerFile: join(process.cwd(), 'caller.ts'),
})
expect(sourceFile.getFilePath()).toBe(sameDirFile as StandardizedFilePath)
} finally {
rmSync(sameDirFile, { force: true })
}
})
})
🤖 Prompt for AI Agents
tests/ts-morph/input-handler.test.ts around lines 99-170: the review asks to add
deduplication in resolveFilePath and a CWD edge-case test; update
src/input-handler.ts so resolveFilePath dedupes possiblePaths (e.g., replace
possiblePaths with a Set or Array.from(new Set(possiblePaths))) before checking
fs.existsSync, then update tests/ts-morph/input-handler.test.ts by adding a test
after the existing cases that creates a callerFile whose directory is
process.cwd() (write any needed duplicate file only once), calls
createSourceFileFromInput with that callerFile and a relative path, and asserts
it resolves (does not throw) to the intended file path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
ARCHITECTURE.md (2)

140-143: Good fix: examples now await the async generateCode.

These updates prevent copy-paste surprises returning a Promise instead of a string.

Also applies to: 149-154, 159-161


26-26: TOC anchor typo resolved.

“ImportParser Optimizations” anchor now matches the section header.

🧹 Nitpick comments (4)
src/input-handler.ts (3)

12-18: Broaden relative import detection to include export-from and dynamic import().

Current logic only examines standard import declarations. It will miss:

  • export { X } from './x'
  • export * from './x'
  • dynamic import('./x')

Consider extending detection to export declarations with module specifiers and scanning for dynamic imports. With ts-morph:

  • sourceFile.getExportDeclarations().some(d => d.getModuleSpecifierValue()?.startsWith('.') === true)
  • Find CallExpression nodes where identifier is import and the first arg is a string starting with ./ or ../.

39-41: Minor: dedupe “Tried” paths in error message for clarity.

You already dedupe for existence checks; mirror that in the message to avoid duplicate entries when callerDir === CWD.

-    throw new Error(`Could not resolve path: ${input}. Tried: ${possiblePaths.join(', ')}`)
+    const tried = Array.from(new Set(possiblePaths)).join(', ')
+    throw new Error(`Could not resolve path: ${input}. Tried: ${tried}`)

69-71: Project lifecycle note (optional).

If callers pass a shared Project, the temp file fix above keeps it clean. If you often create a new Project here, consider whether consumers need compiler options/tsconfig alignment; otherwise, defaults are fine.

ARCHITECTURE.md (1)

68-69: Tiny wording polish (optional).

“the full text content of the newly generated …” can be simplified.

-9.  **Output**: Finally, the full text content of the newly generated `output.ts` source file (which now contains all the TypeBox schemas and static types) is returned as a string.
+9.  **Output**: Finally, the full text of the newly generated `output.ts` source file (which now contains all the TypeBox schemas and static types) is returned as a string.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9d142af and b122cf2.

📒 Files selected for processing (3)
  • ARCHITECTURE.md (5 hunks)
  • src/input-handler.ts (1 hunks)
  • tests/ts-morph/input-handler.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/ts-morph/input-handler.test.ts
🧰 Additional context used
🪛 LanguageTool
ARCHITECTURE.md

[uncategorized] ~68-~68: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... aliases. 9. Output: Finally, the full text content of the newly generated `output....

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[grammar] ~125-~125: There might be a mistake here.
Context: ...either file paths or source code strings 2. Path Resolution: Handles both absolute...

(QB_NEW_EN)


[grammar] ~126-~126: There might be a mistake here.
Context: ...lative file paths with proper validation 3. Relative Import Validation: Prevents r...

(QB_NEW_EN)


[grammar] ~127-~127: There might be a mistake here.
Context: ...nless a callerFile context is provided 4. Project Context Sharing: Supports pass...

(QB_NEW_EN)


[grammar] ~128-~128: There might be a mistake here.
Context: ...es to maintain import resolution context 5. Error Handling: Provides clear error m...

(QB_NEW_EN)

Comment on lines +43 to +48
if (existingPaths.length > 1) {
throw new Error(
`Multiple resolutions found for path: ${input}. '` +
`Found: ${existingPaths.join(', ')}. ' +
'Please provide a more specific path.`,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix broken template literal concatenation (syntax error).

The error message construction mixes template strings and quotes; this won’t compile. Use a single template literal.

-  if (existingPaths.length > 1) {
-    throw new Error(
-      `Multiple resolutions found for path: ${input}. '` +
-        `Found: ${existingPaths.join(', ')}. ' +
-        'Please provide a more specific path.`,
-    )
-  }
+  if (existingPaths.length > 1) {
+    throw new Error(
+      `Multiple resolutions found for path: ${input}. Found: ${existingPaths.join(
+        ', ',
+      )}. Please provide a more specific path.`,
+    )
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (existingPaths.length > 1) {
throw new Error(
`Multiple resolutions found for path: ${input}. '` +
`Found: ${existingPaths.join(', ')}. ' +
'Please provide a more specific path.`,
)
if (existingPaths.length > 1) {
throw new Error(
`Multiple resolutions found for path: ${input}. Found: ${existingPaths.join(
', ',
)}. Please provide a more specific path.`,
)
}
🤖 Prompt for AI Agents
In src/input-handler.ts around lines 43 to 48, the error message construction
mixes template literals and string concatenation incorrectly causing a syntax
error; replace the broken concatenation with a single template literal that
includes input and existingPaths.join(', ') so the message reads as one
interpolated string (e.g., use a single backtick-wrapped template literal
combining the descriptive text and the joined paths) and remove the stray quote
characters and plus operators.

Comment on lines +76 to +78
const sourceFile = project.createSourceFile('temp.ts', sourceCode)

if (!callerFile && hasRelativeImports(sourceFile)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t leak a “temp” SourceFile; make probing idempotent and collision-free.

Two issues:

  • Line 76: creating 'temp.ts' without { overwrite: true } will throw on repeated calls with a shared Project.
  • Lines 86–89: you create a second SourceFile and keep the first in the Project, polluting it and risking name collisions across invocations.

Pattern: create a short-lived probe file to inspect imports, then delete it, then create a single virtual file at a non-colliding path.

-    const sourceFile = project.createSourceFile('temp.ts', sourceCode)
+    // Create a temporary file only to inspect imports, then remove it.
+    const probe = project.createSourceFile('__probe__.ts', sourceCode, { overwrite: true })
 
-    if (!callerFile && hasRelativeImports(sourceFile)) {
+    if (!callerFile && hasRelativeImports(probe)) {
       throw new Error(
         'Relative imports are not supported when providing code as string. ' +
           'Only package imports from node_modules are allowed. ' +
           'Relative imports will be implemented in the future.',
       )
     }
 
-    const virtualPath = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'
-    
-    return project.createSourceFile(virtualPath, sourceCode, { overwrite: true })
+    // Remove probe to keep the Project clean and support repeated calls.
+    probe.delete()
+
+    // Choose a virtual path near the caller to support relative resolution,
+    // and avoid collisions if a file with that name is already in the Project.
+    const base = callerFile ? resolve(dirname(callerFile), '__virtual__.ts') : 'temp.ts'
+    let virtualPath = base
+    let i = 1
+    while (project.getSourceFile(virtualPath)) {
+      virtualPath = base.replace(/\.ts$/, `.${i++}.ts`)
+    }
+    return project.createSourceFile(virtualPath, sourceCode, { overwrite: true })

If you prefer, we can extract the “pick non-colliding virtual path” into a small helper placed above for reuse.

Also applies to: 86-89

🤖 Prompt for AI Agents
In src/input-handler.ts around lines 76–78 and 86–89, the current probing
creates a 'temp.ts' SourceFile without overwrite and later creates a second
SourceFile, leaking files into the shared Project and causing collisions on
repeated calls; change probing to (1) pick a non-colliding virtual path (e.g.,
helper getNonCollidingVirtualPath that appends a counter/uuid to a base name or
checks project.getSourceFile) and use that single path for the probe, (2) create
the SourceFile with a unique name (or `{ overwrite: true }` if intentionally
reusing a fixed probe name), (3) perform import probing against that single
file, and (4) immediately remove the probe from the project
(project.removeSourceFile or equivalent) so the Project is not polluted and the
operation is idempotent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants