Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 97b690b

Browse files
committedNov 7, 2024·
fix: hopefully initial working demo deployment using CommonEngine
1 parent d1d3df3 commit 97b690b

File tree

4 files changed

+176
-74
lines changed

4 files changed

+176
-74
lines changed
 

‎demo/server.ts

+11-52
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,15 @@
1-
import { APP_BASE_HREF } from '@angular/common';
2-
import { CommonEngine } from '@angular/ssr';
3-
import express from 'express';
4-
import { fileURLToPath } from 'node:url';
5-
import { dirname, join, resolve } from 'node:path';
6-
import bootstrap from './src/main.server';
1+
import { CommonEngine } from '@angular/ssr/node'
72

8-
// The Express app is exported so that it can be used by serverless Functions.
9-
export function app(): express.Express {
10-
const server = express();
11-
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
12-
const browserDistFolder = resolve(serverDistFolder, '../browser');
13-
const indexHtml = join(serverDistFolder, 'index.server.html');
3+
const commonEngine = new CommonEngine()
144

15-
const commonEngine = new CommonEngine();
5+
export default async function HttpHandler(
6+
request: Request,
7+
context: any,
8+
commonEngineRenderArgs: any,
9+
): Promise<Response> {
10+
// customize if you want to
1611

17-
server.set('view engine', 'html');
18-
server.set('views', browserDistFolder);
19-
20-
// Example Express Rest API endpoints
21-
// server.get('/api/**', (req, res) => { });
22-
// Serve static files from /browser
23-
server.get('*.*', express.static(browserDistFolder, {
24-
maxAge: '1y'
25-
}));
26-
27-
// All regular routes use the Angular engine
28-
server.get('*', (req, res, next) => {
29-
const { protocol, originalUrl, baseUrl, headers } = req;
30-
31-
commonEngine
32-
.render({
33-
bootstrap,
34-
documentFilePath: indexHtml,
35-
url: `${protocol}://${headers.host}${originalUrl}`,
36-
publicPath: browserDistFolder,
37-
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
38-
})
39-
.then((html) => res.send(html))
40-
.catch((err) => next(err));
41-
});
42-
43-
return server;
12+
return new Response(await commonEngine.render(commonEngineRenderArgs), {
13+
headers: { 'content-type': 'text/html' },
14+
})
4415
}
45-
46-
function run(): void {
47-
const port = process.env['PORT'] || 4000;
48-
49-
// Start up the Node server
50-
const server = app();
51-
server.listen(port, () => {
52-
console.log(`Node Express server listening on http://localhost:${port}`);
53-
});
54-
}
55-
56-
run();

‎src/helpers/serverModuleHelpers.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const { existsSync } = require('node:fs')
2+
const { readFile } = require('node:fs/promises')
3+
4+
const { satisfies } = require('semver')
5+
6+
const getAngularJson = require('./getAngularJson')
7+
const { getProject } = require('./setUpEdgeFunction')
8+
9+
/**
10+
* Inspect content of server module and determine which engine is used
11+
* @param {string} serverModuleContents
12+
* @returns {'AppEngine' | 'CommonEngine' | undefined}
13+
*/
14+
const getUsedEngine = function (serverModuleContents) {
15+
if (serverModuleContents.includes('AngularAppEngine') || serverModuleContents.includes('AngularNodeAppEngine')) {
16+
return 'AppEngine'
17+
}
18+
19+
if (serverModuleContents.includes('CommonEngine')) {
20+
return 'CommonEngine'
21+
}
22+
}
23+
24+
/**
25+
* TODO: document what's happening here and update types
26+
* @param {string} angularJson
27+
* @returns {'AppEngine' | 'CommonEngine' | undefined}
28+
*/
29+
const fixServerTs = async function ({ angularVersion, siteRoot, failPlugin }) {
30+
if (!satisfies(angularVersion, '>=19.0.0-rc', { includePrerelease: true })) {
31+
// for pre-19 versions, we don't need to do anything
32+
return
33+
}
34+
35+
const angularJson = getAngularJson({ failPlugin, siteRoot })
36+
37+
const project = getProject(angularJson)
38+
const {
39+
architect: { build },
40+
} = project
41+
42+
const serverModuleLocation = build?.options?.ssr?.entry
43+
if (!serverModuleLocation || !existsSync(serverModuleLocation)) {
44+
console.log('No SSR setup.')
45+
return
46+
}
47+
48+
// check wether project is using stable CommonEngine or Developer Preview AppEngine
49+
const serverModuleContents = await readFile(serverModuleLocation, 'utf8')
50+
51+
// if server module uses express - it means we can't use it and instead we need to provide our own
52+
// alternatively we could just compare content (or hash of it) to "known" content of server.ts file
53+
// that users get when they scaffold new project and only swap if it's known content and fail with
54+
// actionable message so users know how to adjust their server.ts file to work on Netlify
55+
// with engine they opted to use
56+
const needSwapping = serverModuleContents.includes('express')
57+
58+
/** @type {'AppEngine' | 'CommonEngine'} */
59+
const usedEngine = getUsedEngine(serverModuleContents) ?? 'CommonEngine'
60+
61+
// TODO: actual swapping of server.ts content (or location in angular.json)
62+
63+
return usedEngine
64+
}
65+
66+
const revertServerTsFix = async function () {}
67+
68+
module.exports.fixServerTs = fixServerTs

‎src/helpers/setUpEdgeFunction.js

+91-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
/* eslint-disable max-lines */
12
const { Buffer } = require('node:buffer')
23
const { readdirSync, existsSync } = require('node:fs')
3-
const { writeFile, mkdir, readFile } = require('node:fs/promises')
4+
const { writeFile, mkdir, readFile, copyFile } = require('node:fs/promises')
45
const { join, relative, sep, posix } = require('node:path')
56
const process = require('node:process')
67

@@ -64,7 +65,8 @@ const getPrerenderedRoutes = async (outputDir) => {
6465
return prerenderedRoutes
6566
}
6667

67-
const setUpEdgeFunction = async ({ angularJson, constants, failBuild }) => {
68+
// eslint-disable-next-line max-lines-per-function
69+
const setUpEdgeFunction = async ({ angularJson, constants, failBuild, usedEngine }) => {
6870
const project = getProject(angularJson)
6971
const {
7072
architect: { build },
@@ -108,26 +110,93 @@ const setUpEdgeFunction = async ({ angularJson, constants, failBuild }) => {
108110
globalThis.Event = globalThis.DenoEvent
109111
`
110112

111-
const ssrFunction = `
113+
let ssrFunctionContent = ''
114+
115+
if (!usedEngine) {
116+
// eslint-disable-next-line no-inline-comments
117+
ssrFunctionContent = /* javascript */ `
118+
import { Buffer } from "node:buffer";
119+
import { renderApplication } from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/render-utils.server.mjs";
120+
import bootstrap from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/main.server.mjs";
121+
import "./fixup-event.mjs";
122+
123+
const document = Buffer.from(${JSON.stringify(
124+
Buffer.from(html, 'utf-8').toString('base64'),
125+
)}, 'base64').toString("utf-8");
126+
127+
export default async (request, context) => {
128+
const html = await renderApplication(bootstrap, {
129+
url: request.url,
130+
document,
131+
platformProviders: [{ provide: "netlify.request", useValue: request }, { provide: "netlify.context", useValue: context }],
132+
});
133+
return new Response(html, { headers: { "content-type": "text/html" } });
134+
};
135+
`
136+
} else if (usedEngine === 'CommonEngine') {
137+
const cssAssetsManifest = {}
138+
const outputBrowserDir = join(outputDir, 'browser')
139+
const cssFiles = getAllFilesIn(outputBrowserDir).filter((file) => file.endsWith('.css'))
140+
141+
for (const cssFile of cssFiles) {
142+
const content = await readFile(cssFile)
143+
cssAssetsManifest[`${relative(outputBrowserDir, cssFile)}`] = content.toString('base64')
144+
}
145+
146+
// eslint-disable-next-line no-inline-comments
147+
ssrFunctionContent = /* javascript */ `
148+
import { Buffer } from "node:buffer";
149+
import { dirname, relative, resolve } from 'node:path';
150+
import { fileURLToPath } from 'node:url';
151+
152+
import Handler from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/server.mjs";
153+
import bootstrap from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/main.server.mjs";
154+
import "./fixup-event.mjs";
155+
156+
const document = Buffer.from(${JSON.stringify(
157+
Buffer.from(html, 'utf-8').toString('base64'),
158+
)}, 'base64').toString("utf-8");
159+
160+
const cssAssetsManifest = ${JSON.stringify(cssAssetsManifest)};
161+
162+
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
163+
const browserDistFolder = resolve(serverDistFolder, 'browser');
164+
165+
// fs.readFile is not supported in Edge Functions, so this is a workaround for CSS inlining
166+
// that will intercept readFile attempt and if it's a CSS file, return the content from the manifest
167+
const originalReadFile = globalThis.Deno.readFile
168+
globalThis.Deno.readFile = (...args) => {
169+
if (args.length > 0 && typeof args[0] === 'string') {
170+
const relPath = relative(browserDistFolder, args[0])
171+
if (relPath in cssAssetsManifest) {
172+
return Promise.resolve(Buffer.from(cssAssetsManifest[relPath], 'base64'))
173+
}
174+
}
175+
176+
return originalReadFile.apply(globalThis.Deno, args)
177+
}
178+
179+
export default async (request, context) => {
180+
const commonEngineRenderArgs = {
181+
bootstrap: bootstrap,
182+
document,
183+
url: request.url,
184+
publicPath: browserDistFolder,
185+
platformProviders: [{ provide: "netlify.request", useValue: request }, { provide: "netlify.context", useValue: context }],
186+
}
187+
return await Handler(request, context, commonEngineRenderArgs);
188+
}
189+
`
190+
}
191+
192+
if (!ssrFunctionContent) {
193+
return failBuild('no ssr function body')
194+
}
195+
196+
// eslint-disable-next-line no-inline-comments
197+
const ssrFunction = /* javascript */ `
112198
import "./polyfill.mjs";
113-
import { Buffer } from "node:buffer";
114-
import { renderApplication } from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/render-utils.server.mjs";
115-
import bootstrap from "${toPosix(relative(edgeFunctionDir, serverDistRoot))}/main.server.mjs";
116-
import "./fixup-event.mjs";
117-
118-
const document = Buffer.from(${JSON.stringify(
119-
Buffer.from(html, 'utf-8').toString('base64'),
120-
)}, 'base64').toString("utf-8");
121-
122-
export default async (request, context) => {
123-
const html = await renderApplication(bootstrap, {
124-
url: request.url,
125-
document,
126-
platformProviders: [{ provide: "netlify.request", useValue: request }, { provide: "netlify.context", useValue: context }],
127-
});
128-
return new Response(html, { headers: { "content-type": "text/html" } });
129-
};
130-
199+
${ssrFunctionContent}
131200
export const config = {
132201
path: "/*",
133202
excludedPath: ${JSON.stringify(excludedPaths)},
@@ -142,3 +211,4 @@ const setUpEdgeFunction = async ({ angularJson, constants, failBuild }) => {
142211
}
143212

144213
module.exports.setUpEdgeFunction = setUpEdgeFunction
214+
/* eslint-enable max-lines */

‎src/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ const fixOutputDir = require('./helpers/fixOutputDir')
33
const getAngularJson = require('./helpers/getAngularJson')
44
const getAngularRoot = require('./helpers/getAngularRoot')
55
const getAngularVersion = require('./helpers/getAngularVersion')
6+
const { fixServerTs } = require('./helpers/serverModuleHelpers')
67
const { setUpEdgeFunction } = require('./helpers/setUpEdgeFunction')
78
const validateAngularVersion = require('./helpers/validateAngularVersion')
89

910
let isValidAngularProject = true
11+
let usedEngine
1012

1113
module.exports = {
1214
async onPreBuild({ netlifyConfig, utils, constants }) {
1315
const { failBuild, failPlugin } = utils.build
1416
const siteRoot = getAngularRoot({ failBuild, netlifyConfig })
1517
const angularVersion = await getAngularVersion(siteRoot)
1618
isValidAngularProject = validateAngularVersion(angularVersion)
19+
1720
if (!isValidAngularProject) {
1821
console.warn('Skipping build plugin.')
1922
return
@@ -31,6 +34,8 @@ module.exports = {
3134
IS_LOCAL: constants.IS_LOCAL,
3235
netlifyConfig,
3336
})
37+
38+
usedEngine = await fixServerTs({ angularVersion, siteRoot, failPlugin })
3439
},
3540
async onBuild({ utils, netlifyConfig, constants }) {
3641
if (!isValidAngularProject) {
@@ -45,8 +50,8 @@ module.exports = {
4550
await setUpEdgeFunction({
4651
angularJson,
4752
constants,
48-
netlifyConfig,
4953
failBuild,
54+
usedEngine,
5055
})
5156
},
5257
}

0 commit comments

Comments
 (0)
Please sign in to comment.