-
Notifications
You must be signed in to change notification settings - Fork 0
feat: accept source code or path instead of ts-morph SourceFile #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||||||||||||||||||||
| import { existsSync, statSync } from 'fs' | ||||||||||||||||||||||||||||
| import { dirname, isAbsolute, resolve } from 'path' | ||||||||||||||||||||||||||||
| import { Project, SourceFile } from 'ts-morph' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export interface InputOptions { | ||||||||||||||||||||||||||||
| filePath?: string | ||||||||||||||||||||||||||||
| sourceCode?: string | ||||||||||||||||||||||||||||
| callerFile?: string | ||||||||||||||||||||||||||||
| project?: Project | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const hasRelativeImports = (sourceFile: SourceFile): boolean => { | ||||||||||||||||||||||||||||
| const hasRelativeImports = sourceFile | ||||||||||||||||||||||||||||
| .getImportDeclarations() | ||||||||||||||||||||||||||||
| .some((importDeclaration) => importDeclaration.isModuleSpecifierRelative()) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return hasRelativeImports | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const resolveFilePath = (input: string, callerFile?: string): string => { | ||||||||||||||||||||||||||||
| if (isAbsolute(input)) { | ||||||||||||||||||||||||||||
| if (!existsSync(input)) { | ||||||||||||||||||||||||||||
| throw new Error(`Absolute path does not exist: ${input}`) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return input | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const possiblePaths: string[] = [] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (callerFile) { | ||||||||||||||||||||||||||||
| const callerDir = dirname(callerFile) | ||||||||||||||||||||||||||||
| possiblePaths.push(resolve(callerDir, input)) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| possiblePaths.push(resolve(process.cwd(), input)) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const existingPaths = Array.from(new Set(possiblePaths)).filter((path) => existsSync(path)) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (existingPaths.length === 0) { | ||||||||||||||||||||||||||||
| throw new Error(`Could not resolve path: ${input}. Tried: ${possiblePaths.join(', ')}`) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (existingPaths.length > 1) { | ||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||
| `Multiple resolutions found for path: ${input}. '` + | ||||||||||||||||||||||||||||
| `Found: ${existingPaths.join(', ')}. ' + | ||||||||||||||||||||||||||||
| 'Please provide a more specific path.`, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return existingPaths[0]! | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const validateInputOptions = (options: InputOptions): void => { | ||||||||||||||||||||||||||||
| const { filePath, sourceCode } = options | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!filePath && !sourceCode) { | ||||||||||||||||||||||||||||
| throw new Error('Either filePath or sourceCode must be provided') | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (filePath && sourceCode) { | ||||||||||||||||||||||||||||
| throw new Error('Only one of filePath or sourceCode can be provided, not both') | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const sourceFile = project.createSourceFile('temp.ts', sourceCode) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!callerFile && hasRelativeImports(sourceFile)) { | ||||||||||||||||||||||||||||
|
Comment on lines
+76
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 |
||||||||||||||||||||||||||||
| 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 }) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (filePath) { | ||||||||||||||||||||||||||||
| const resolvedPath = resolveFilePath(filePath, callerFile) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!statSync(resolvedPath).isFile()) { | ||||||||||||||||||||||||||||
| throw new Error(`Path is not a file: ${resolvedPath}`) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return project.addSourceFileAtPath(resolvedPath) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| throw new Error('Invalid input options') | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.