Skip to content

Commit

Permalink
feat(ssr): add link asset preload header service (#3343)
Browse files Browse the repository at this point in the history
  • Loading branch information
griest024 authored Nov 13, 2024
1 parent 0af779d commit df8ce2a
Show file tree
Hide file tree
Showing 33 changed files with 653 additions and 0 deletions.
37 changes: 37 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,43 @@
}
}
}
},
"ssr": {
"projectType": "library",
"root": "libs/ssr",
"sourceRoot": "libs/ssr",
"prefix": "@daffodil",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "libs/ssr/tsconfig.lib.json",
"project": "libs/ssr/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "libs/ssr/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "libs/ssr/test.ts",
"tsConfig": "libs/ssr/tsconfig.spec.json",
"karmaConfig": "libs/ssr/karma.conf.js"
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"libs/ssr/**/*.ts",
"libs/ssr/**/*.html"
]
}
}
}
}
},
"schematics": {
Expand Down
52 changes: 52 additions & 0 deletions libs/ssr/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module.exports = {
extends: '../../.eslintrc.js',
ignorePatterns: [
'!**/*'
],
overrides: [
{
files: [
'*.ts'
],
parserOptions: {
project: [
'libs/ssr/tsconfig.lib.json',
'libs/ssr/tsconfig.spec.json'
],
createDefaultProgram: true
},
rules: {
'@angular-eslint/component-class-suffix': [
'error',
{
suffixes: [
'Component',
'Container'
]
}
],
'@angular-eslint/component-selector': [
'error',
{
type: 'lib',
prefix: 'kebab-case'
}
],
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase'
}
],
}
},
{
files: [
'*.html'
],
rules: {}
}
]
}
16 changes: 16 additions & 0 deletions libs/ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# @daffodil/ssr
`@daffodil/ssr` contains utilities useful for dealing with and adding features to the Angular SSR.

## Installation
To install `@daffodil/ssr`, use the following commands in your terminal.

Install with npm:
```bash
npm install @daffodil/ssr --save
```

Install with yarn:

```bash
yarn add @daffodil/ssr
```
6 changes: 6 additions & 0 deletions libs/ssr/express/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"lib": {
"entryFile": "src/index.ts"
}
}
1 change: 1 addition & 0 deletions libs/ssr/express/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public_api';
1 change: 1 addition & 0 deletions libs/ssr/express/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './response/public_api';
47 changes: 47 additions & 0 deletions libs/ssr/express/src/response/class.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Response } from 'express';

import { DaffSsrExpressResponse } from './class';

describe('@daffodil/ssr/express | DaffSsrExpressResponse', () => {
let service: DaffSsrExpressResponse;
let responseSpy: jasmine.SpyObj<Response>;

beforeEach(() => {
responseSpy = jasmine.createSpyObj('Response', [
'getHeader',
'setHeader',
'appendHeader',
'status',
]);

service = new DaffSsrExpressResponse(responseSpy);
});

describe('get', () => {
it('should get headers to the express response', () => {
responseSpy.getHeader.withArgs('name').and.returnValue('value');
expect(service.get('name')).toEqual('value');
});
});

describe('set', () => {
it('should set headers to the express response', () => {
service.set('name', 'value');
expect(responseSpy.setHeader).toHaveBeenCalledWith('name', 'value');
});
});

describe('append', () => {
it('should append headers to the express response', () => {
service.append('name', 'value');
expect(responseSpy.appendHeader).toHaveBeenCalledWith('name', 'value');
});
});

describe('status', () => {
it('should set the status code on the express response', () => {
service.status(404);
expect(responseSpy.status).toHaveBeenCalledWith(404);
});
});
});
30 changes: 30 additions & 0 deletions libs/ssr/express/src/response/class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Response } from 'express';

import { DaffSsrResponse } from '@daffodil/ssr';

/**
* A response that interfaces with the express request.
*
* @inheritdoc
*/
export class DaffSsrExpressResponse implements DaffSsrResponse {
constructor(
protected response: Response,
) {}

get(header: string): Array<string> | string {
return String(this.response.getHeader(header)) || '';
}

set(name: string, value: Array<string> | string): void {
this.response.setHeader(name, value);
}

append(name: string, value: Array<string> | string): void {
this.response.appendHeader(name, value);
}

status(code: number): void {
this.response.status(code);
}
}
12 changes: 12 additions & 0 deletions libs/ssr/express/src/response/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Response } from 'express';

import { provideDaffSsrResponse } from '@daffodil/ssr';

import { DaffSsrExpressResponse } from './class';

/**
* Provides `DaffSsrExpressResponse` to `DAFF_SSR_RESPONSE`.
*/
export const provideDaffSsrExpressResponse = (response: Response) =>
provideDaffSsrResponse(new DaffSsrExpressResponse(response));

2 changes: 2 additions & 0 deletions libs/ssr/express/src/response/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './provider';
export * from './class';
4 changes: 4 additions & 0 deletions libs/ssr/guides/platforms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Platforms
There are various server platforms that can be used with angular SSR. The following list enumerates the platforms that Daffodil supports and the corresponding guide for setting up that platform.

- [Express](/libs/ssr/guides/platforms/express.md)
28 changes: 28 additions & 0 deletions libs/ssr/guides/platforms/express.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Express
Express is the default and recommended Angular SSR platform.

To get started, provide the response token in your common engine render:

`server.ts`
```ts
import { provideDaffSsrExpressResponse } from '@daffodil/ssr/express'

// the rest of the file from https://angular.dev/guide/ssr#configure-server-side-rendering is omitted

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: req.baseUrl},
provideDaffSsrExpressResponse(res),
],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
```
61 changes: 61 additions & 0 deletions libs/ssr/guides/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Usage

## Setting the Response Status Code
Inject the `DAFF_SSR_RESPONSE` and call `status` to set the status code on the SSR document response.

app.component.ts
```ts
import { DaffSsrResponse, DAFF_SSR_RESPONSE } from '@daffodil/ssr'

@Component()
class AppComponent {
constructor(
@Inject(DAFF_SSR_RESPONSE) private response: DaffSsrResponse
) {
this.response.status(404);
}
}
```

## Adding a Response Header
Inject the `DAFF_SSR_RESPONSE` and call `append` to add a header to the SSR document response.

app.component.ts
```ts
import { DaffSsrResponse, DAFF_SSR_RESPONSE } from '@daffodil/ssr'

@Component()
class AppComponent {
constructor(
@Inject(DAFF_SSR_RESPONSE) private response: DaffSsrResponse
) {
this.response.append('Link', '<https://www.mydomain.com>; rel=preconnect');
}
}
```

## Asset Preloading
Preloading certain important assets can boost the initial render of a page. By including [`Link` headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) on the SSR document response, the browser can load these assets while parsing the HTML.

Daffodil provides a `DaffSsrHeaderLinkAssetPreloader` service to assist this process. The following example demonstrates how to preload an asset that is needed for the initial render of a page.

```ts
import {
DaffSsrHeaderLinkAssetPreloader,
DaffSsrHeadersLinkPreloadAssetKind,
DaffSsrHeadersLinkPreloadAssetPriority,
} from '@daffodil/ssr'

@Component()
class AppComponent {
constructor(
private assetPreloadService: DaffSsrHeaderLinkAssetPreloader
) {
this.assetPreloadService.addHeader(
'/asset/logo.png',
DaffSsrHeadersLinkPreloadAssetKind.IMAGE,
DaffSsrHeadersLinkPreloadAssetPriority.HIGH,
);
}
}
```
10 changes: 10 additions & 0 deletions libs/ssr/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
baseConfiguration = require('../../tools/karma/karma.conf');

module.exports = function (config) {
baseConfiguration(config);
config.set({
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../coverage/libs/ssr'),
},
});
};
7 changes: 7 additions & 0 deletions libs/ssr/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ssr",
"lib": {
"entryFile": "src/index.ts"
}
}
7 changes: 7 additions & 0 deletions libs/ssr/ng-package.prod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/ssr",
"lib": {
"entryFile": "src/index.ts"
}
}
39 changes: 39 additions & 0 deletions libs/ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@daffodil/ssr",
"nx": {
"targets": {
"build": {
"outputs": ["{workspaceRoot}/dist/ssr"]
}
}
},
"version": "0.0.0-PLACEHOLDER",
"description": "Adds features and provides utils for the Angular ssr.",
"repository": {
"type": "git",
"url": "https://github.com/graycoreio/daffodil"
},
"author": "Graycore LLC",
"license": "MIT",
"bugs": {
"url": "https://github.com/graycoreio/daffodil/issues"
},
"scripts": {
"build": "ng build ssr --configuration production",
"lint": "cd ../.. && ng lint ssr",
"lint:fix": "npm run lint -- --fix",
"test": "ng test ssr --watch=false --browsers=ChromeHeadless",
"publish": "cd ../../dist/ssr && npm publish --access=public"
},
"peerDependencies": {
"@angular/common": "0.0.0-PLACEHOLDER",
"@angular/core": "0.0.0-PLACEHOLDER",
"@daffodil/core": "0.0.0-PLACEHOLDER",
"@angular/ssr": "0.0.0-PLACEHOLDER",
"rxjs": "0.0.0-PLACEHOLDER",
"express": "0.0.0-PLACEHOLDER"
},
"devDependencies": {
"@daffodil/core": "0.0.0-PLACEHOLDER"
}
}
Loading

0 comments on commit df8ce2a

Please sign in to comment.