@@ -5,6 +5,7 @@ const { parse, join } = require('node:path')
5
5
const { satisfies } = require ( 'semver' )
6
6
7
7
const getAngularJson = require ( './getAngularJson' )
8
+ const { getEngineBasedOnKnownSignatures } = require ( './serverTsSignature' )
8
9
const { getProject } = require ( './setUpEdgeFunction' )
9
10
10
11
// eslint-disable-next-line no-inline-comments
@@ -13,20 +14,28 @@ import { render } from '@netlify/angular-runtime/common-engine'
13
14
14
15
const commonEngine = new CommonEngine()
15
16
16
- export default async function HttpHandler (request: Request, context: any): Promise<Response> {
17
+ export async function netlifyCommonEngineHandler (request: Request, context: any): Promise<Response> {
17
18
return await render(commonEngine)
18
19
}
19
20
`
20
21
21
22
// eslint-disable-next-line no-inline-comments
22
23
const NetlifyServerTsAppEngine = /* typescript */ `import { AngularAppEngine, createRequestHandler } from '@angular/ssr'
24
+ import { getContext } from '@netlify/angular-runtime/context'
25
+
23
26
const angularAppEngine = new AngularAppEngine()
24
27
25
- // @ts-expect-error - createRequestHandler expects a function with single Request argument and doesn't allow context argument
26
- export const reqHandler = createRequestHandler(async (request: Request, context: any) => {
28
+ export async function netlifyAppEngineHandler(request: Request): Promise<Response> {
29
+ const context = getContext()
30
+
27
31
const result = await angularAppEngine.handle(request, context)
28
32
return result || new Response('Not found', { status: 404 })
29
- })
33
+ }
34
+
35
+ /**
36
+ * The request handler used by the Angular CLI (dev-server and during build).
37
+ */
38
+ export const reqHandler = createRequestHandler(netlifyAppEngineHandler)
30
39
`
31
40
32
41
let needSwapping = false
@@ -38,30 +47,44 @@ let serverModuleBackupLocation
38
47
* @param {string } serverModuleContents
39
48
* @returns {'AppEngine' | 'CommonEngine' | undefined }
40
49
*/
41
- const getUsedEngine = function ( serverModuleContents ) {
42
- if ( serverModuleContents . includes ( 'AngularAppEngine' ) || serverModuleContents . includes ( 'AngularNodeAppEngine' ) ) {
50
+ const guessUsedEngine = function ( serverModuleContents ) {
51
+ const containsAppEngineKeywords =
52
+ serverModuleContents . includes ( 'AngularAppEngine' ) || serverModuleContents . includes ( 'AngularNodeAppEngine' )
53
+ const containsCommonEngineKeywords = serverModuleContents . includes ( 'CommonEngine' )
54
+
55
+ if ( containsAppEngineKeywords && containsCommonEngineKeywords ) {
56
+ // keywords for both engine found - we can't determine which one is used
57
+ return
58
+ }
59
+
60
+ if ( containsAppEngineKeywords ) {
43
61
return 'AppEngine'
44
62
}
45
63
46
- if ( serverModuleContents . includes ( 'CommonEngine' ) ) {
64
+ if ( containsCommonEngineKeywords ) {
47
65
return 'CommonEngine'
48
66
}
67
+
68
+ // no keywords found - we can't determine which engine is used
49
69
}
50
70
51
71
/**
52
- * For Angular@19+ we inspect user's server.ts and if it uses express, we swap it out with our own.
53
- * We also check wether CommonEngine or AppEngine is used to provide correct replacement preserving
54
- * engine of user's choice (CommonEngine is stable, but lacks support for some features, AppEngine is
55
- * Developer Preview, but has more features and is easier to integrate with - ultimately choice is up to user
56
- * as AppEngine might have breaking changes outside of major version bumps)
72
+ * For Angular@19+ we inspect user's server.ts and if it's one of known defaults that are generated when scaffolding
73
+ * new Angular app with SSR enabled - we will automatically swap it out with Netlify compatible server.ts using same Angular
74
+ * Engine. Swapping just known server.ts files ensures that we are not losing any customizations user might have made.
75
+ * In case server.ts file is not known and our checks decide that it's not Netlify compatible (we are looking for specific keywords
76
+ * that would be used for named exports) - we will fail the build and provide user with instructions on how to replace server.ts
77
+ * to make it Netlify compatible and which they can apply request handling customizations to it (or just leave default in if they generally
78
+ * have default one that just missed our known defaults comparison potentially due to custom formatting etc).
57
79
* @param {Object } obj
58
80
* @param {string } obj.angularVersion Angular version
59
81
* @param {string } obj.siteRoot Root directory of an app
60
82
* @param {(msg: string) => never } obj.failPlugin Function to fail the plugin
83
+ * * @param {(msg: string) => never } obj.failBuild Function to fail the build
61
84
*
62
85
* @returns {'AppEngine' | 'CommonEngine' | undefined }
63
86
*/
64
- const fixServerTs = async function ( { angularVersion, siteRoot, failPlugin } ) {
87
+ const fixServerTs = async function ( { angularVersion, siteRoot, failPlugin, failBuild } ) {
65
88
if ( ! satisfies ( angularVersion , '>=19.0.0-rc' , { includePrerelease : true } ) ) {
66
89
// for pre-19 versions, we don't need to do anything
67
90
return
@@ -76,35 +99,77 @@ const fixServerTs = async function ({ angularVersion, siteRoot, failPlugin }) {
76
99
77
100
serverModuleLocation = build ?. options ?. ssr ?. entry
78
101
if ( ! serverModuleLocation || ! existsSync ( serverModuleLocation ) ) {
79
- console . log ( 'No SSR setup.' )
80
102
return
81
103
}
82
104
83
105
// check wether project is using stable CommonEngine or Developer Preview AppEngine
84
106
const serverModuleContents = await readFile ( serverModuleLocation , 'utf8' )
85
- /** @type {'AppEngine' | 'CommonEngine' } */
86
- const usedEngine = getUsedEngine ( serverModuleContents ) ?? 'CommonEngine'
87
107
88
- // if server module uses express - it means we can't use it and instead we need to provide our own
89
- needSwapping = serverModuleContents . includes ( 'express' )
108
+ const usedEngineBasedOnKnownSignatures = getEngineBasedOnKnownSignatures ( serverModuleContents )
109
+ if ( usedEngineBasedOnKnownSignatures ) {
110
+ needSwapping = true
90
111
91
- if ( needSwapping ) {
92
- console . log ( `Swapping server.ts to use ${ usedEngine } ` )
112
+ console . log (
113
+ `Default server.ts using ${ usedEngineBasedOnKnownSignatures } found. Automatically swapping to Netlify compatible server.ts.` ,
114
+ )
93
115
94
116
const parsed = parse ( serverModuleLocation )
95
117
96
118
serverModuleBackupLocation = join ( parsed . dir , `${ parsed . name } .original${ parsed . ext } ` )
97
119
98
120
await rename ( serverModuleLocation , serverModuleBackupLocation )
99
121
100
- if ( usedEngine === 'CommonEngine' ) {
122
+ if ( usedEngineBasedOnKnownSignatures === 'CommonEngine' ) {
101
123
await writeFile ( serverModuleLocation , NetlifyServerTsCommonEngine )
102
- } else if ( usedEngine === 'AppEngine' ) {
124
+ } else if ( usedEngineBasedOnKnownSignatures === 'AppEngine' ) {
103
125
await writeFile ( serverModuleLocation , NetlifyServerTsAppEngine )
104
126
}
127
+ return usedEngineBasedOnKnownSignatures
128
+ }
129
+
130
+ // if we can't determine engine based on known signatures, let's first try to check if module is already
131
+ // Netlify compatible to determine if it can be used as is or if user intervention is required
132
+ // we will look for "netlify<Engine>Handler" which is named export that we will rely on and it's existence will
133
+ // be quite strong indicator that module is already compatible and doesn't require any changes
134
+
135
+ const isNetlifyAppEngine = serverModuleContents . includes ( 'netlifyAppEngineHandler' )
136
+ const isNetlifyCommonEngine = serverModuleContents . includes ( 'netlifyCommonEngineHandler' )
137
+
138
+ if ( isNetlifyAppEngine && isNetlifyCommonEngine ) {
139
+ // both exports found - we can't determine which engine is used
140
+ failBuild (
141
+ "server.ts seems to contain both 'netlifyAppEngineHandler' and 'netlifyCommonEngineHandler' - it should contain just one of those." ,
142
+ )
143
+ }
144
+
145
+ if ( isNetlifyAppEngine ) {
146
+ return 'AppEngine'
147
+ }
148
+
149
+ if ( isNetlifyCommonEngine ) {
150
+ return 'CommonEngine'
151
+ }
152
+
153
+ // at this point we know that user's server.ts is not Netlify compatible so user intervention is required
154
+ // we will try to inspect server.ts to determine which engine is used and provide more accurate error message
155
+ const guessedUsedEngine = guessUsedEngine ( serverModuleContents )
156
+
157
+ let errorMessage = `server.ts doesn't seem to be Netlify compatible and is not known default. Please replace it with Netlify compatible server.ts.`
158
+ if ( guessedUsedEngine ) {
159
+ const alternativeEngine = guessedUsedEngine === 'AppEngine' ? 'CommonEngine' : 'AppEngine'
160
+
161
+ errorMessage += `\n\nIt seems like you use "${ guessedUsedEngine } " - for this case your server.ts file should contain following:\n\n\`\`\`\n${
162
+ guessedUsedEngine === 'CommonEngine' ? NetlifyServerTsCommonEngine : NetlifyServerTsAppEngine
163
+ } \`\`\``
164
+ errorMessage += `\n\nIf you want to use "${ alternativeEngine } " instead - your server.ts file should contain following:\n\n\`\`\`\n${
165
+ alternativeEngine === 'CommonEngine' ? NetlifyServerTsCommonEngine : NetlifyServerTsAppEngine
166
+ } \`\`\``
167
+ } else {
168
+ errorMessage += `\n\nIf you want to use "CommonEngine" - your server.ts file should contain following:\n\n\`\`\`\n${ NetlifyServerTsCommonEngine } \`\`\``
169
+ errorMessage += `\n\nIf you want to use "AppEngine" - your server.ts file should contain following:\n\n\`\`\`\n${ NetlifyServerTsAppEngine } \`\`\``
105
170
}
106
171
107
- return usedEngine
172
+ failBuild ( errorMessage )
108
173
}
109
174
110
175
module . exports . fixServerTs = fixServerTs
0 commit comments