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

4

6

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
  }
}

Melanson answered 20/11, 2023 at 16:33 Comment(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.Shake
The build fails, that's what I meant: "An unhandled exception occurred: Cannot read properties of null (reading 'bootstrap')"Melanson
Got the exact same problem. all generated files changed to mjs. i'd be more than interested in the answer.Mosemoseley
Did you try editing your tsconfig.server.json by removing target and setting "module": "CommonJS"? The builder should generate a single js filrLaurice
@Laurice there is no more tsconfig.server.jsonForan
C
4

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)
Care answered 28/1 at 14:21 Comment(2)
Disable prerender and works perfectlyGrimaldo
Can you explain a bit more to make things generic so one can understand according to project's situation.Cirri
M
0

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.

Melanson answered 8/12, 2023 at 13:10 Comment(0)
D
0

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
Daddy answered 18/1 at 19:14 Comment(1)
This is not how to convert files to *.js extensionMelanson
U
-1

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

Unreflective answered 20/11, 2023 at 17:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.