The nestjs-ember-static
package enhances the Nest framework to serve static Single Page Applications (SPAs) built with Ember. Drawing inspiration from @nestjs/serve-static, this module stands out by offering tailored features for Ember applications.
Why choose this over @nestjs/serve-static? Primarily for its support of live reloading and configuration injection. Designed to facilitate the integration of EmberJS applications within a NestJS mono repository, this module ensures that both front-end and back-end can be accessed through the same port. During development, it preserves the Ember application's live reloading capabilities. Moreover, it provides a feature where the back-end can inject configuration metadata directly into the generated index.html
. This functionality makes it ideal for developers seeking a seamless development experience with combined Ember and NestJS applications.
$ pnpm install nestjs-ember-static
To incorporate the EmberModule
into your Nest application, you can begin by editing your app.module.ts
file. Below are detailed setups using both the EmberModule.forRoot
and EmberModule.forRootAsync
methods, along with an additional section on injecting custom configurations.
For simpler setups where asynchronous operations and dynamic configurations are not required, you can use the forRoot
method directly:
import { Module } from '@nestjs/common';
import { join } from 'path';
import { EmberModule } from 'nestjs-ember-static';
@Module({
imports: [
EmberModule.forRoot({
rootPath: join(__dirname, '../frontend/dist'), // Adjust the path as necessary
}),
],
})
export class AppModule {}
This configuration will statically serve your Ember SPA from the specified directory.
For more complex scenarios that require dynamic path resolution or other asynchronous setup procedures, you can utilize the forRootAsync
method:
import { Module } from '@nestjs/common';
import { join } from 'path';
import { EmberModule } from 'nestjs-ember-static';
import { ConfigService } from '@nestjs/config'; // Ensure ConfigService is imported if not already
@Module({
imports: [
EmberModule.forRootAsync({
useFactory: async (configService: ConfigService): Promise<EmberModuleOptions> => {
const env = configService.get('env', { infer: true });
return {
rootPath:
env['name'] === 'development'
? join(env['appRoot'], '../frontend/dist') // Development path
: join(env['appRoot'], 'client'), // Production path
};
},
inject: [ConfigService], // Inject ConfigService to use it in the factory function
}),
],
})
export class AppModule {}
For advanced configuration, such as dynamically setting properties or modifying the application title, you can add extra providers within the module. This enhances the module's flexibility and functionality, especially during development:
extraProviders: [
{
provide: EMBER_INJECTION_HELPERS,
useFactory: (configService: ConfigService) => [
new EmberConfigInjectionHelper('ember-app/config/environment', configService),
new TitleInjectionHelper('Ember Application'),
],
inject: [ConfigService],
},
];
By following these configurations, you can effectively integrate and serve your Ember SPA within a NestJS application, leveraging both static and dynamic setup options.
To implement custom inject helpers for dynamic configuration, you need to define them by extending an abstract class. They get passes the rootElement from the html document (see node-html-parser for how to mutate that).
export abstract class AbstractInjectionHelper {
public abstract process(htmlElement: HTMLElement): void;
}
Here's an example of how you can create your own inject helpers:
export class TitleInjectionHelper extends AbstractInjectionHelper {
constructor(private title: string) {
super();
}
public process(htmlElement: HTMLElement): void {
htmlElement.querySelector('title')!.textContent = this.title;
}
}`
Note: The InjectionHelpers
given in this library are meant to be implemented by you. These are provided as examples to illustrate how you might structure your own helpers for configuration injection.
The EmberModule supports route guards to control access to your Ember SPA.
To add a route guard, provide a guard into the EmberModule via the extraProviders:
import { Module } from '@nestjs/common';
import { join } from 'path';
import { EMBER_MODULE_GUARD, EmberModule } from 'nestjs-ember-static';
import { ConfigService } from '@nestjs/config';
import { MyCustomGuard } from './guards/my-custom.guard';
@Module({
imports: [
EmberModule.forRootAsync({
extraProviders: [
{
provide: EMBER_INJECTION_HELPERS,
useFactory: (configService: ConfigService) => [
new EmberClientConfigInjector('@3dlayermaker/device-controller-frontend/config/environment', configService),
],
inject: [ConfigService],
},
{
provide: EMBER_MODULE_GUARD,
useClass: ControllerApiAuthGuard,
},
],
}),
],
})
export class AppModule {}
The guard will be applied to all routes handled by the EmberModule. The guard should implement the CanActivate interface, and you can control access by implementing the canActivate method.
While developing, I typically run both the NestJS application and the EmberJS application simultaneously. My package.json
includes a script like:
"start:dev": "concurrently --kill-others 'cd apps/frontend && pnpm run start' 'cd apps/backend && pnpm run start:debug'"
In production, I build the EmberJS app and store it in the client directory of the NestJS application. This setup allows me to use a dynamic rootPath
as shown in the example above. In development mode (when the environment variable NODE_ENV
is set to 'development'), any request to /ember-cli-live-reload.js
on the NestJS application is forwarded to the same route on the running Ember application. In production, this route will return a 404.
The nestjs-ember-static
module supports both Express and Fastify as backing frameworks. This flexibility allows you to choose the framework that best suits your application's needs and preferences. Whether you are using the default Express or have switched to Fastify for improved performance, the nestjs-ember-static
module integrates seamlessly with both, ensuring consistent functionality and easy setup.
However, it's important to note that Fastify has some limitations. For more details on these limitations, refer to the @nestjs/serve-static
module documentation. This will help you understand any potential constraints and how they might impact your application when using Fastify as the backing framework.
The EmberModule
utilizes the same options as @nestjs/serve-static
with the addition of the following:
Property | Type | Description |
---|---|---|
liveReloadPort |
number | The port the /ember-cli-live-reload.js resource is served on by the Ember app in development. Default: 4200
|
These options provide additional flexibility and control when serving your Ember SPA with the NestJS framework.
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please read more here.
- Author - Bas Kamer
As this is based on serve-static, this module is also licensed under the MIT license.