Skip to content

Commit

Permalink
feat(@nguniversal/express-engine): add standalone support to ng add
Browse files Browse the repository at this point in the history
This commit add support to run `ng add @nguniversal/express-engine` in a standalone application.
  • Loading branch information
alan-agius4 committed Apr 3, 2023
1 parent 0b8a4e1 commit 3af1451
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 42 deletions.
Expand Up @@ -3,10 +3,9 @@ import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';

import { AppServerModule } from './src/<%= stripTsExtension(main) %>';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import <% if (isStandalone) { %>bootstrap<% } else { %>{ AppServerModule }<% } %> from './src/<%= stripTsExtension(main) %>;
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
Expand All @@ -16,7 +15,7 @@ export function app(): express.Express {

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
<% if (isStandalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule<% } %>
}));

server.set('view engine', 'html');
Expand Down
71 changes: 55 additions & 16 deletions modules/express-engine/schematics/install/index.spec.ts
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import { tags } from '@angular-devkit/core';
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';

Expand All @@ -27,34 +28,30 @@ describe('Universal Schematic', () => {
});

it('should add dependency: @nguniversal/express-engine', async () => {
const tree = await schematicRunner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const filePath = '/package.json';
const contents = tree.readContent(filePath);
expect(contents).toMatch(/"@nguniversal\/express-engine": "/);
});

it('should add dependency: express', async () => {
const tree = await schematicRunner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const filePath = '/package.json';
const contents = tree.readContent(filePath);
expect(contents).toMatch(/"express": "/);
});

it('should install npm dependencies', async () => {
await schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise();
await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);
expect(schematicRunner.tasks.length).toBe(1);
expect(schematicRunner.tasks[0].name).toBe('node-package');
expect((schematicRunner.tasks[0].options as { command: string }).command).toBe('install');
});

it(`should update 'tsconfig.server.json' files with Express main file`, async () => {
const tree = await schematicRunner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const { files } = JSON.parse(
tree
Expand All @@ -69,20 +66,62 @@ describe('Universal Schematic', () => {
});

it(`should add export to main file in 'server.ts'`, async () => {
const tree = await schematicRunner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const content = tree.readContent('/projects/test-app/server.ts');
expect(content).toContain(`export * from './src/main.server'`);
});

it(`should add correct value to 'distFolder'`, async () => {
const tree = await schematicRunner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const content = tree.readContent('/projects/test-app/server.ts');
expect(content).toContain(`const distFolder = join(process.cwd(), 'dist/test-app/browser');`);
});

it(`should import 'AppServerModule' from 'main.server.ts'`, async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const filePath = '/projects/test-app/server.ts';
const content = tree.readContent(filePath);
expect(content).toContain(`import { AppServerModule } from './src/main.server;`);
});

it(`should pass 'AppServerModule' in the bootstrap parameter.`, async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const filePath = '/projects/test-app/server.ts';
const content = tree.readContent(filePath);
expect(tags.oneLine`${content}`).toContain(tags.oneLine`
ngExpressEngine({
bootstrap: AppServerModule
}));
`);
});

describe('standalone application', () => {
beforeEach(async () => {
appTree = await createTestApp({ standalone: true });
});

it(`should add default import to 'main.server.ts'`, async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const filePath = '/projects/test-app/server.ts';
const content = tree.readContent(filePath);
expect(content).toContain(`import bootstrap from './src/main.server;`);
});

it(`should pass 'AppServerModule' in the bootstrap parameter.`, async () => {
const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree);

const filePath = '/projects/test-app/server.ts';
const content = tree.readContent(filePath);
expect(tags.oneLine`${content}`).toContain(tags.oneLine`
ngExpressEngine({
bootstrap
}));
`);
});
});
});
19 changes: 16 additions & 3 deletions modules/express-engine/schematics/install/index.ts
Expand Up @@ -23,6 +23,9 @@ import {
import { Schema as UniversalOptions } from '@schematics/angular/universal/schema';
import { DependencyType, addDependency, updateWorkspace } from '@schematics/angular/utility';
import { JSONFile } from '@schematics/angular/utility/json-file';
import { isStandaloneApp } from '@schematics/angular/utility/ng-ast-utils';
import { targetBuildNotFoundError } from '@schematics/angular/utility/project-targets';
import { BrowserBuilderOptions } from '@schematics/angular/utility/workspace-models';
import * as ts from 'typescript';

import {
Expand Down Expand Up @@ -276,7 +279,7 @@ function addDependencies(): Rule {
};
}

function addServerFile(options: UniversalOptions): Rule {
function addServerFile(options: UniversalOptions, isStandalone: boolean): Rule {
return async (host) => {
const project = await getProject(host, options.project);
const browserDistDirectory = await getOutputPath(host, options.project, 'build');
Expand All @@ -288,6 +291,7 @@ function addServerFile(options: UniversalOptions): Rule {
...options,
stripTsExtension,
browserDistDirectory,
isStandalone,
}),
move(project.root),
]),
Expand All @@ -302,6 +306,15 @@ export default function (options: AddUniversalOptions): Rule {
...options,
skipInstall: true,
};
const clientBuildTarget = project.targets.get('build');
if (!clientBuildTarget) {
throw targetBuildNotFoundError();
}

const clientBuildOptions = (clientBuildTarget.options ||
{}) as unknown as BrowserBuilderOptions;

const isStandalone = isStandaloneApp(host, clientBuildOptions.main);

delete universalOptions.serverFileName;
delete universalOptions.serverPort;
Expand All @@ -313,8 +326,8 @@ export default function (options: AddUniversalOptions): Rule {
addScriptsRule(options),
updateServerTsConfigRule(options),
updateWorkspaceConfigRule(options),
routingInitialNavigationRule(options),
addServerFile(options),
isStandalone ? noop() : routingInitialNavigationRule(options),
addServerFile(options, isStandalone),
addDependencies(),
]);
};
Expand Down
32 changes: 14 additions & 18 deletions modules/express-engine/schematics/testing/test-app.ts
Expand Up @@ -15,23 +15,19 @@ export const collectionPath = require.resolve('../collection.json');
export async function createTestApp(appOptions = {}): Promise<UnitTestTree> {
const baseRunner = new SchematicTestRunner('universal-schematics', collectionPath);

const tree = await baseRunner
.runExternalSchematicAsync('@schematics/angular', 'workspace', {
name: 'workspace',
version: '6.0.0',
newProjectRoot: 'projects',
})
.toPromise();
const tree = await baseRunner.runExternalSchematic('@schematics/angular', 'workspace', {
name: 'workspace',
version: '6.0.0',
newProjectRoot: 'projects',
});

return baseRunner
.runExternalSchematicAsync(
'@schematics/angular',
'application',
{
...appOptions,
name: 'test-app',
},
tree,
)
.toPromise();
return baseRunner.runExternalSchematic(
'@schematics/angular',
'application',
{
...appOptions,
name: 'test-app',
},
tree,
);
}

0 comments on commit 3af1451

Please sign in to comment.