22// Licensed under the MIT License.
33
44using System . Text . Json ;
5+ using Microsoft . Agents . A365 . DevTools . Cli . Exceptions ;
56using Microsoft . Agents . A365 . DevTools . Cli . Models ;
67using Microsoft . Agents . A365 . DevTools . Cli . Services . Helpers ;
78using Microsoft . Extensions . Logging ;
@@ -77,7 +78,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
7778 var packageJsonPath = Path . Combine ( projectDir , "package.json" ) ;
7879 if ( ! File . Exists ( packageJsonPath ) )
7980 {
80- throw new FileNotFoundException ( "package.json not found in project directory" ) ;
81+ throw new NodeProjectNotFoundException ( projectDir ) ;
8182 }
8283
8384 // Install dependencies
@@ -89,7 +90,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
8990 installResult = await _helper . ExecuteWithOutputAsync ( "npm" , "install" , projectDir , verbose ) ;
9091 if ( ! installResult . Success )
9192 {
92- throw new Exception ( $ "npm install failed: { installResult . StandardError } " ) ;
93+ throw new NodeDependencyInstallException ( projectDir , installResult . StandardError ) ;
9394 }
9495 }
9596
@@ -103,7 +104,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
103104 var buildResult = await _helper . ExecuteWithOutputAsync ( "npm" , "run build" , projectDir , verbose ) ;
104105 if ( ! buildResult . Success )
105106 {
106- throw new Exception ( $ "npm run build failed: { buildResult . StandardError } " ) ;
107+ throw new NodeBuildFailedException ( projectDir , buildResult . StandardError ) ;
107108 }
108109 }
109110 else
@@ -145,7 +146,7 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
145146 CopyDirectory ( srcDir , Path . Combine ( publishPath , "src" ) ) ;
146147 }
147148
148- // Copy server files (.js files in root)
149+ // Copy server/entry files (.js/.ts files in root)
149150 foreach ( var jsFile in Directory . GetFiles ( projectDir , "*.js" ) )
150151 {
151152 File . Copy ( jsFile , Path . Combine ( publishPath , Path . GetFileName ( jsFile ) ) ) ;
@@ -155,6 +156,17 @@ public async Task<string> BuildAsync(string projectDir, string outputPath, bool
155156 File . Copy ( tsFile , Path . Combine ( publishPath , Path . GetFileName ( tsFile ) ) ) ;
156157 }
157158
159+ var distDir = Path . Combine ( projectDir , "dist" ) ;
160+ if ( Directory . Exists ( distDir ) )
161+ {
162+ _logger . LogInformation ( "Found dist folder, copying to publish output..." ) ;
163+ CopyDirectory ( distDir , Path . Combine ( publishPath , "dist" ) ) ;
164+ }
165+ else
166+ {
167+ _logger . LogInformation ( "No dist folder found in project; relying on Oryx build (if configured) to produce runtime output." ) ;
168+ }
169+
158170 // Create .deployment file to force Oryx build during Azure deployment
159171 await CreateDeploymentFile ( publishPath ) ;
160172
@@ -166,6 +178,11 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
166178 _logger . LogInformation ( "Creating Oryx manifest for Node.js..." ) ;
167179
168180 var packageJsonPath = Path . Combine ( projectDir , "package.json" ) ;
181+ if ( ! File . Exists ( packageJsonPath ) )
182+ {
183+ throw new NodeProjectNotFoundException ( projectDir ) ;
184+ }
185+
169186 var packageJson = await File . ReadAllTextAsync ( packageJsonPath ) ;
170187
171188 // Parse package.json to detect start command and version
@@ -177,8 +194,7 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
177194 if ( root . TryGetProperty ( "engines" , out var engines ) &&
178195 engines . TryGetProperty ( "node" , out var nodeVersionProp ) )
179196 {
180- var versionString = nodeVersionProp . GetString ( ) ?? "18" ;
181- // Extract major version (e.g., "18.x" -> "18")
197+ var versionString = nodeVersionProp . GetString ( ) ?? "20" ;
182198 var match = System . Text . RegularExpressions . Regex . Match ( versionString , @"(\d+)" ) ;
183199 if ( match . Success )
184200 {
@@ -192,7 +208,13 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
192208 if ( root . TryGetProperty ( "scripts" , out var scripts ) &&
193209 scripts . TryGetProperty ( "start" , out var startScript ) )
194210 {
195- startCommand = startScript . GetString ( ) ?? startCommand ;
211+ var scriptValue = startScript . GetString ( ) ;
212+ if ( ! string . IsNullOrWhiteSpace ( scriptValue ) )
213+ {
214+ // Use the script literally, same as package.json
215+ startCommand = scriptValue ;
216+ }
217+
196218 _logger . LogInformation ( "Detected start command from package.json: {Command}" , startCommand ) ;
197219 }
198220 else if ( root . TryGetProperty ( "main" , out var mainProp ) )
@@ -203,34 +225,47 @@ public async Task<OryxManifest> CreateManifestAsync(string projectDir, string pu
203225 }
204226 else
205227 {
206- // Look for common entry point files
228+ // Look for common entry point files under publish path
207229 var commonEntryPoints = new [ ] { "server.js" , "app.js" , "index.js" , "main.js" } ;
208230 foreach ( var entryPoint in commonEntryPoints )
209231 {
210232 if ( File . Exists ( Path . Combine ( publishPath , entryPoint ) ) )
211233 {
212234 startCommand = $ "node { entryPoint } ";
213- _logger . LogInformation ( "Detected entry point: {Command}" , startCommand ) ;
235+ _logger . LogInformation ( "Detected entry point in publish folder : {Command}" , startCommand ) ;
214236 break ;
215237 }
216238 }
217239 }
218240
219- var buildCommand = "npm run build" ;
220- var hasBuildScript = scripts . TryGetProperty ( "build" , out var buildScript ) ;
221- if ( hasBuildScript )
241+ // Detect build command – strictly Node
242+ string buildCommand = "" ;
243+ bool buildRequired = false ;
244+
245+ if ( root . TryGetProperty ( "scripts" , out var scriptsForBuild ) &&
246+ scriptsForBuild . TryGetProperty ( "build" , out var buildScript ) )
247+ {
248+ var buildValue = buildScript . GetString ( ) ;
249+ if ( ! string . IsNullOrWhiteSpace ( buildValue ) )
250+ {
251+ // We always call through npm so it picks up the script from package.json
252+ buildCommand = "npm run build" ;
253+ buildRequired = true ;
254+ _logger . LogInformation ( "Detected build script; using Oryx build command: {Command}" , buildCommand ) ;
255+ }
256+ }
257+ else
222258 {
223- buildCommand = buildScript . GetString ( ) ?? buildCommand ;
224- _logger . LogInformation ( "Detected build command from package.json: {Command}" , buildCommand ) ;
259+ _logger . LogInformation ( "No build script found; Oryx will only run npm install." ) ;
225260 }
226261
227262 return new OryxManifest
228263 {
229264 Platform = "nodejs" ,
230265 Version = nodeVersion ,
231266 Command = startCommand ,
232- BuildCommand = hasBuildScript ? buildCommand : "" ,
233- BuildRequired = hasBuildScript
267+ BuildCommand = buildCommand ,
268+ BuildRequired = buildRequired
234269 } ;
235270 }
236271
0 commit comments