Angular 17 SSR - how to compile server files (*.mjs) to *.js

1.3k Views Asked by At

my hosting provider requires server files to be compiled to CommonJs and Angular 17 by default compiles 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?

enter image description here

default tsconfig.json file:

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "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
  }
}

4

There are 4 best solutions below

1
On BEST ANSWER

Base on this idea, I found the answer, Thanks yannier

1: Modify server.ts

import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
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 commonEngine = 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;

    commonEngine
      .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)
0
On

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

0
On

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.

1
On

Solution over AWS and Lambda

server.ts

import { APP_BASE_HREF } from '@angular/common';
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 commonEngine = 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;        
    commonEngine
      .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/comma-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