@@ -31,6 +31,7 @@ interface SourceSpec {
3131 inline : string
3232 dependencies ?: Record < string , string >
3333 yarnLock ?: string
34+ tsConfig ?: string
3435}
3536
3637interface InputSpec {
@@ -54,6 +55,95 @@ interface Manifest {
5455 }
5556}
5657
58+ interface TypeScriptConfig {
59+ extends ?: string
60+ compilerOptions ?: {
61+ baseUrl ?: string
62+ paths ?: Record < string , string [ ] >
63+ [ key : string ] : any
64+ }
65+ [ key : string ] : any
66+ }
67+
68+ /**
69+ * Loads and resolves a TypeScript configuration file, handling extends
70+ * @param configPath Path to the tsconfig.json file
71+ * @param baseDir Base directory for resolving relative paths
72+ * @returns Resolved TypeScript configuration
73+ */
74+ function loadTsConfig ( configPath : string , _baseDir ?: string ) : TypeScriptConfig | null {
75+ try {
76+ if ( ! fs . existsSync ( configPath ) ) {
77+ return null
78+ }
79+
80+ const configContent = fs . readFileSync ( configPath , { encoding : "utf8" } )
81+ const config : TypeScriptConfig = JSON . parse ( configContent )
82+ const configDir = path . dirname ( configPath )
83+
84+ // If the config extends another config, load and merge it
85+ if ( config . extends ) {
86+ const extendsPath = path . resolve ( configDir , config . extends )
87+ const baseConfig = loadTsConfig ( extendsPath , configDir )
88+
89+ if ( baseConfig ) {
90+ // Merge configurations, with current config taking precedence
91+ const mergedConfig : TypeScriptConfig = {
92+ ...baseConfig ,
93+ ...config ,
94+ compilerOptions : {
95+ ...baseConfig . compilerOptions ,
96+ ...config . compilerOptions ,
97+ } ,
98+ }
99+ return mergedConfig
100+ }
101+ }
102+
103+ return config
104+ } catch ( error ) {
105+ moduleLogger . debug ( `Failed to load TypeScript config from ${ configPath } : ${ error } ` )
106+ return null
107+ }
108+ }
109+
110+ /**
111+ * Converts TypeScript path mappings to esbuild alias format
112+ * @param tsConfig TypeScript configuration
113+ * @param baseDir Base directory for resolving relative paths
114+ * @returns esbuild alias configuration
115+ */
116+ function convertTsPathsToEsbuildAlias (
117+ tsConfig : TypeScriptConfig ,
118+ baseDir : string
119+ ) : Record < string , string > | undefined {
120+ const { compilerOptions } = tsConfig
121+ if ( ! compilerOptions ?. paths ) {
122+ return undefined
123+ }
124+
125+ const { paths } = compilerOptions
126+ const alias : Record < string , string > = { }
127+
128+ for ( const [ pattern , mappings ] of Object . entries ( paths ) ) {
129+ if ( mappings . length === 0 ) continue
130+
131+ // Take the first mapping (most common case)
132+ const mapping = mappings [ 0 ]
133+
134+ // Remove the /* suffix from pattern and mapping if present
135+ const cleanPattern = pattern . replace ( / \/ \* $ / , "" )
136+ const cleanMapping = mapping . replace ( / \/ \* $ / , "" )
137+
138+ // Resolve the mapping path relative to the base directory
139+ const resolvedMapping = path . resolve ( baseDir , cleanMapping )
140+
141+ alias [ cleanPattern ] = resolvedMapping
142+ }
143+
144+ return Object . keys ( alias ) . length > 0 ? alias : undefined
145+ }
146+
57147/**
58148 * Bundles a TypeScript file using esbuild
59149 * @param filePath Path to the TypeScript file
@@ -73,15 +163,90 @@ async function bundleTypeScript(
73163 moduleLogger . debug ( `Using temporary directory: ${ tempDir } ` )
74164
75165 try {
76- // Create temp directory
166+ // Create temp directory for output only
77167 fs . mkdirSync ( tempDir , { recursive : true } )
78168
79169 // Get original file size for logging
80170 const originalSize = fs . statSync ( filePath ) . size
81171
82- // Default esbuild options optimized for readability
172+ // Check if models/index.ts exists and prepare to auto-import it
173+ const packageRoot = process . cwd ( )
174+ const modelsIndexPath = path . join ( packageRoot , "models" , "index.ts" )
175+ const shouldAutoImportModels = fs . existsSync ( modelsIndexPath )
176+
177+ // Create virtual entry content that imports models and re-exports the function
178+ const relativeFunctionPath = path . relative ( packageRoot , filePath ) . replace ( / \\ / g, "/" )
179+ let virtualEntryContent = ""
180+
181+ if ( shouldAutoImportModels ) {
182+ virtualEntryContent += `import './models';\n`
183+ moduleLogger . debug ( `Auto-importing models from: ${ modelsIndexPath } ` )
184+ }
185+
186+ // Import and re-export the function as default
187+ virtualEntryContent += `export { default } from './${ relativeFunctionPath } ';\n`
188+
189+ moduleLogger . debug ( `Virtual entry content:\n${ virtualEntryContent } ` )
190+
191+ // Load TypeScript configuration and extract aliases
192+ const functionDir = path . dirname ( filePath )
193+ let tsConfig : TypeScriptConfig | null = null
194+ let esbuildAlias : Record < string , string > | undefined = undefined
195+
196+ // Try to load tsconfig.json from function directory first, then from current working directory
197+ const functionTsConfigPath = path . join ( functionDir , "tsconfig.json" )
198+ const cwdTsConfigPath = path . join ( process . cwd ( ) , "tsconfig.json" )
199+
200+ let configPath : string | null = null
201+ let configBaseDir : string | null = null
202+
203+ if ( fs . existsSync ( functionTsConfigPath ) ) {
204+ tsConfig = loadTsConfig ( functionTsConfigPath , functionDir )
205+ configPath = functionTsConfigPath
206+ configBaseDir = functionDir
207+ moduleLogger . debug (
208+ `Loaded TypeScript config from function directory: ${ functionTsConfigPath } `
209+ )
210+ } else if ( fs . existsSync ( cwdTsConfigPath ) ) {
211+ tsConfig = loadTsConfig ( cwdTsConfigPath , process . cwd ( ) )
212+ configPath = cwdTsConfigPath
213+ configBaseDir = process . cwd ( )
214+ moduleLogger . debug (
215+ `Loaded TypeScript config from current working directory: ${ cwdTsConfigPath } `
216+ )
217+ }
218+
219+ // Convert TypeScript path aliases to esbuild aliases
220+ if ( tsConfig && configPath && configBaseDir ) {
221+ const configDir = path . dirname ( configPath )
222+ const baseUrl = tsConfig . compilerOptions ?. baseUrl || "."
223+ const resolvedBaseDir = path . resolve ( configDir , baseUrl )
224+
225+ moduleLogger . debug ( `TypeScript config found at: ${ configPath } ` )
226+ moduleLogger . debug ( `Config directory: ${ configDir } ` )
227+ moduleLogger . debug ( `Base URL: ${ baseUrl } ` )
228+ moduleLogger . debug ( `Resolved base directory: ${ resolvedBaseDir } ` )
229+ moduleLogger . debug ( `TypeScript paths: ${ JSON . stringify ( tsConfig . compilerOptions ?. paths ) } ` )
230+
231+ esbuildAlias = convertTsPathsToEsbuildAlias ( tsConfig , resolvedBaseDir )
232+
233+ if ( esbuildAlias && Object . keys ( esbuildAlias ) . length > 0 ) {
234+ moduleLogger . info (
235+ `Applied TypeScript path aliases to esbuild: ${ JSON . stringify ( esbuildAlias ) } `
236+ )
237+ moduleLogger . info ( `Base directory for aliases: ${ resolvedBaseDir } ` )
238+ } else {
239+ moduleLogger . debug ( `No TypeScript path aliases found or converted` )
240+ }
241+ }
242+
243+ // Default esbuild options optimized for readability using stdin
83244 const defaultOptions : BuildOptions = {
84- entryPoints : [ filePath ] ,
245+ stdin : {
246+ contents : virtualEntryContent ,
247+ resolveDir : packageRoot ,
248+ sourcefile : "virtual-entry.ts" ,
249+ } ,
85250 bundle : true ,
86251 format : "esm" ,
87252 sourcemap : true ,
@@ -96,16 +261,50 @@ async function bundleTypeScript(
96261 experimentalDecorators : true ,
97262 } ,
98263 } ,
264+ // Add TypeScript path aliases if available
265+ ...( esbuildAlias && { alias : esbuildAlias } ) ,
99266 }
100267
101268 // If embedDeps is false, add plugin to keep dependencies external
102269 if ( ! embedDeps ) {
103- // Create a plugin to mark all non-relative imports as external
270+ // Create a plugin to mark all non-relative imports as external, except for aliases
104271 const externalizeNpmDepsPlugin : Plugin = {
105272 name : "externalize-npm-deps" ,
106273 setup ( build ) {
107- // Filter for all import paths that don't start with ./ or ../
274+ // Filter for imports that don't start with ./ or ../ (non-relative imports)
108275 build . onResolve ( { filter : / ^ [ ^ . / ] / } , args => {
276+ moduleLogger . debug ( `Processing import: ${ args . path } from ${ args . importer || "entry" } ` )
277+
278+ // Skip if this is an entry point (no importer means it's an entry point)
279+ if ( ! args . importer ) {
280+ moduleLogger . debug ( `Skipping entry point: ${ args . path } ` )
281+ return undefined
282+ }
283+
284+ // Skip built-in Node.js modules
285+ if ( args . path . startsWith ( "node:" ) ) {
286+ moduleLogger . debug ( `Skipping Node.js built-in: ${ args . path } ` )
287+ return undefined
288+ }
289+
290+ // Check if this matches any of our TypeScript path aliases
291+ // Be more specific about alias matching to avoid false positives with scoped packages
292+ if ( esbuildAlias ) {
293+ for ( const alias of Object . keys ( esbuildAlias ) ) {
294+ // For aliases like "@", only match if it's followed by a slash (e.g., "@/models")
295+ // This prevents matching scoped packages like "@crossplane-js/sdk"
296+ if ( alias === "@" && args . path . startsWith ( "@/" ) ) {
297+ moduleLogger . debug ( `Allowing alias resolution: ${ args . path } ` )
298+ return undefined // Let esbuild handle the alias resolution
299+ } else if ( alias !== "@" && args . path . startsWith ( alias ) ) {
300+ moduleLogger . debug ( `Allowing alias resolution: ${ args . path } ` )
301+ return undefined // Let esbuild handle the alias resolution
302+ }
303+ }
304+ }
305+
306+ // For all other imports (npm packages, scoped packages, etc.), mark as external
307+ moduleLogger . debug ( `Marking as external: ${ args . path } ` )
109308 return { path : args . path , external : true }
110309 } )
111310 } ,
@@ -124,11 +323,11 @@ async function bundleTypeScript(
124323 // Bundle with esbuild
125324 await build ( buildOptions )
126325
127- // Read the bundled code
326+ // Read the bundled code (always single entry point now)
128327 const bundledCode = fs . readFileSync ( outputFile , { encoding : "utf8" } )
328+ const bundledSize = fs . statSync ( outputFile ) . size
129329
130330 // Log bundle size information
131- const bundledSize = fs . statSync ( outputFile ) . size
132331 moduleLogger . debug ( `Bundling complete: ${ originalSize } bytes → ${ bundledSize } bytes` )
133332
134333 return bundledCode
@@ -398,6 +597,38 @@ async function compoAction(
398597 }
399598 }
400599
600+ if ( xfuncjsStep . input . spec . source . tsConfig === "__TSCONFIG__" ) {
601+ // Check for tsconfig.json in the function directory
602+ const functionTsConfigPath = path . join ( functionDir , "tsconfig.json" )
603+ const rootTsConfigPath = path . join ( cwd ( ) , "tsconfig.json" )
604+ let tsConfig : string | null = null
605+
606+ if ( fs . existsSync ( functionTsConfigPath ) ) {
607+ try {
608+ // Use tsconfig.json from the function directory
609+ tsConfig = fs . readFileSync ( functionTsConfigPath , {
610+ encoding : "utf8" ,
611+ } )
612+ moduleLogger . debug ( `Using tsconfig.json from function directory: ${ functionName } ` )
613+ } catch ( error ) {
614+ moduleLogger . error ( `Error reading tsconfig.json in function directory: ${ error } ` )
615+ }
616+ } else if ( fs . existsSync ( rootTsConfigPath ) ) {
617+ try {
618+ // Use tsconfig.json from the current working directory
619+ tsConfig = fs . readFileSync ( rootTsConfigPath , { encoding : "utf8" } )
620+ moduleLogger . debug ( `Using tsconfig.json from current working directory` )
621+ } catch ( error ) {
622+ moduleLogger . error ( `Error reading tsconfig.json in current working directory: ${ error } ` )
623+ }
624+ }
625+
626+ // Add tsconfig.json to the manifest if found
627+ if ( tsConfig ) {
628+ xfuncjsStep . input . spec . source . tsConfig = tsConfig
629+ }
630+ }
631+
401632 // Generate final output using the already loaded XRD data
402633 let finalOutput : string
403634
0 commit comments