my hosting provider requires server files to be piled to CommonJs and Angular 17 by default piles files to Module JS *.mjs, I've tried to change the tsconfig.json but it changes the scope for the whole app but I want to change it just for server files. I've been looking for some documentation how to use something like tsconfig.server.json but I don't know how to later split it in angular.json file.
How to convert *.mjs files to *.js for SSR in Angular 17?
default tsconfig.json file:
/* To learn more about this file see: . */
{
"pileOnSave": false,
"pilerOptions": {
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
my hosting provider requires server files to be piled to CommonJs and Angular 17 by default piles files to Module JS *.mjs, I've tried to change the tsconfig.json but it changes the scope for the whole app but I want to change it just for server files. I've been looking for some documentation how to use something like tsconfig.server.json but I don't know how to later split it in angular.json file.
How to convert *.mjs files to *.js for SSR in Angular 17?
default tsconfig.json file:
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"pileOnSave": false,
"pilerOptions": {
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
Share
Improve this question
edited Jan 18, 2024 at 20:56
Robbert
6,6026 gold badges37 silver badges64 bronze badges
asked Nov 20, 2023 at 16:33
Freestyle09Freestyle09
5,53810 gold badges58 silver badges92 bronze badges
5
- "I've tried to change the tsconfig.json but it changes the scope for the whole app" Why is this a problem? The client code is bundled. – jabaa Commented Nov 20, 2023 at 16:40
- The build fails, that's what I meant: "An unhandled exception occurred: Cannot read properties of null (reading 'bootstrap')" – Freestyle09 Commented Nov 20, 2023 at 16:47
- 1 Got the exact same problem. all generated files changed to mjs. i'd be more than interested in the answer. – millenion Commented Dec 2, 2023 at 2:45
-
Did you try editing your
tsconfig.server.json
by removingtarget
and setting"module": "CommonJS"
? The builder should generate a single js filr – Pieterjan Commented Jan 18, 2024 at 21:06 -
@Pieterjan there is no more
tsconfig.server.json
– ezhupa99 Commented Mar 3, 2024 at 6:00
4 Answers
Reset to default 4Base on this idea, I found the answer, Thanks yannier
1: Modify server.ts
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/mon';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const monEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(browserDistFolder, {
maxAge: '1y',
})
);
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
monEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
export * from './src/main.server';
2: Create a js file named main.js
async function run() {
try {
// Import the app from the ES module
const server = await import("./server/server.mjs");
const app = await server.app();
const port = process.env["PORT"] || 4000;
// Start up the Node server
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
} catch (error) {
console.error("Failed to import app:", error);
}
}
run();
3: Run main.js
using node main.js
or if you use iis you can run it by iisnode module:
web.config
sample for run the project in iis
<configuration>
<system.web>
<httpRuntime enableVersionHeader="true" />
</system.web>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000"/>
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="DENY" />
<add name="X-XSS-Protection" value="1; mode=block" />
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
<webSocket enabled="false" />
<handlers>
<!-- Indicates that the main.js file is a node.js site to be handled by the iisnode module -->
<add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
</handlers>
<rewrite>
<rules>
<!-- <rule name="HTTP to HTTPS redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule> -->
<!-- Do not interfere with requests for node-inspector debugging -->
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
<match url="^main.js\/debug[\/]?" />
</rule>
<!-- All other URLs are mapped to the node.js site entry point -->
<rule name="DynamicContent">
<match url="^(?!.*login).*$"></match>
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/>
</conditions>
<action type="Rewrite" url="main.js"/>
</rule>
</rules>
<!-- <outboundRules>
<rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
<conditions>
<add input="{HTTPS}" pattern="on" ignoreCase="true" />
</conditions>
<action type="Rewrite" value="max-age=31536000" />
</rule>
</outboundRules> -->
</rewrite>
<!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
<security>
<requestFiltering>
<hiddenSegments>
<remove segment="bin"/>
</hiddenSegments>
</requestFiltering>
</security>
<!-- Make sure error responses are left untouched -->
<httpErrors existingResponse="PassThrough" />
<!-- Restart the server if any of these files change -->
<iisnode watchedFiles="web.config;*.js;browser/*.*" nodeProcessCommandLine="C:\Program Files\nodejs\node.exe" />
</system.webServer>
</configuration>
- Output path result:
- browser (folder)
- server (folder)
- main.js
- web.config (in iis)
I found a workaround (yet not a solution how to change it though), if your provider requires *.js files to run Angular apps you can simply create specific file like e.g. app.js and then inside this file write:
import ('<path-to-your-dist-mjs-file>.mjs');
And that's it, it'll load your Angular app from that file. It works for my MyDevil hosting provider with Passenger. I hope it helps.
Solution over AWS and Lambda
server.ts
import { APP_BASE_HREF } from '@angular/mon';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import bootstrap from './src/main.server';
// The Express app is exported so that it can be used by Serverless Functions.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const monEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
// Serve static files from /browser
server.get('*.*', express.static(browserDistFolder, {
maxAge: '1y'
}));
// All regular routes use the Angular engine
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
monEngine
.render({
bootstrap: bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => {
res.send(html)
})
.catch((err) => {
next(err)
});
});
return server;
}
export * from './src/main.server';
lambda.js
const awsServerlessExpress = require('aws-serverless-express');
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware');
const binaryMimeTypes = [
"application/javascript",
"application/json",
"application/octet-stream",
"application/xml",
"image/jpeg",
"image/png",
"image/webp",
"image/gif",
"text/ma-separated-values",
"text/css",
"text/html",
"text/javascript",
"text/plain",
"text/text",
"text/xml",
"image/x-icon",
"image/svg+xml",
"application/x-font-ttf",
"font/ttf",
"font/otf",
"font/woff",
"font/woff2"
];
module.exports.handler = async (event, context) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
try {
// Import the app from the ES module
const server = await import('./dist/app-name/server/server.mjs');
const app = await server.app();
app.use(awsServerlessExpressMiddleware.eventContext());
// Create a server with the specified MIME types
const serverAws = awsServerlessExpress.createServer(app, null, binaryMimeTypes);
if (!app) {
console.error('Server is not initialized');
return;
} else {
return awsServerlessExpress.proxy(serverAws, event, context, 'PROMISE').promise;
}
} catch (error) {
console.error('Failed to import app:', error);
}
};
serverless.yml
service: service-name
frameworkVersion: '3'
plugins:
- serverless-apigw-binary
provider:
name: aws
runtime: nodejs20.x
memorySize: 192
timeout: 10
region: us-east-1
apiGateway:
shouldStartNameWithService: true
package:
excludeDevDependencies: true
exclude:
- ./**
- '!node_modules/@vendia/**'
include:
- "node_modules/aws-serverless-express/**"
- "node_modules/binary-case/**"
- "node_modules/type-is/**"
- "node_modules/media-typer/**"
- "node_modules/mime-types/**"
- "node_modules/mime-db/**"
- "node_modules/@angular/ssr"
- "node_modules/@codegenie/**"
- "dist/**"
- "lambda.js"
functions:
server:
handler: lambda.handler
events:
- http: ANY /{proxy+}
- http: ANY /
resources:
- ${file(resources.yml)}
Also in angular.json you shoud change this line
"prerender": false, //<-Change to false
You should use the browser-esbuild
builder included in the @angular-devkit/build-angular
NPM package since Angular now uses the application builder by default