Skip to content

Commit

Permalink
feat: add SSR support for dev/build (#182)
Browse files Browse the repository at this point in the history
Closes #21
  • Loading branch information
brandonroberts authored Dec 17, 2022
1 parent 687f61e commit 965ed61
Show file tree
Hide file tree
Showing 21 changed files with 403 additions and 50 deletions.
3 changes: 3 additions & 0 deletions apps/analog-app/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async function errorHandler(error, event) {
event.res.end('error' + error.toString());
}
7 changes: 6 additions & 1 deletion apps/analog-app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
"targets": {
"build": {
"executor": "@nrwl/vite:build",
"outputs": ["{options.outputPath}"],
"outputs": [
"{options.outputPath}",
"dist/apps/analog-app/.nitro",
"dist/apps/analog-app/ssr",
"dist/apps/analog-app/server"
],
"options": {
"configFile": "apps/analog-app/vite.config.ts",
"outputPath": "dist/apps/analog-app/client"
Expand Down
10 changes: 7 additions & 3 deletions apps/analog-app/src/app/routes/(home).ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { NgForOf, NgIf } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { RouterLinkWithHref } from '@angular/router';
import { catchError, of } from 'rxjs';
import { ProductAlertsComponent } from '../product-alerts/product-alerts.component';
import { Product } from '../products';

Expand Down Expand Up @@ -54,9 +55,12 @@ export default class ProductListComponent {
http = inject(HttpClient);

ngOnInit() {
this.http.get<Product[]>('/api/v1/products').subscribe((products) => {
this.products = products;
});
this.http
.get<Product[]>('/api/v1/products')
.pipe(catchError(() => of([])))
.subscribe((products) => {
this.products = products;
});
}

share() {
Expand Down
16 changes: 10 additions & 6 deletions apps/analog-app/src/app/routes/products.[productId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { injectActivatedRoute } from '@analogjs/router';
import { Product } from '../products';
import { CartService } from '../cart.service';
import { HttpClient } from '@angular/common/http';
import { catchError, of } from 'rxjs';

@Component({
selector: 'app-product-details',
Expand Down Expand Up @@ -33,12 +34,15 @@ export default class ProductDetailsComponent implements OnInit {
const routeParams = this.route.parent!.snapshot!.paramMap;
const productIdFromRoute = Number(routeParams.get('productId'));

this.http.get<Product[]>('/api/v1/products').subscribe((products) => {
// Find the product that correspond with the id provided in route.
this.product = products.find(
(product) => product.id === productIdFromRoute
);
});
this.http
.get<Product[]>('/api/v1/products')
.pipe(catchError(() => of([])))
.subscribe((products) => {
// Find the product that correspond with the id provided in route.
this.product = products.find(
(product) => product.id === productIdFromRoute
);
});
}

addToCart(product: Product) {
Expand Down
28 changes: 28 additions & 0 deletions apps/analog-app/src/main.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'zone.js/node';
import {
renderApplication,
ɵSERVER_CONTEXT as SERVER_CONTEXT,
} from '@angular/platform-server';
import { provideFileRouter } from '@analogjs/router';
import { withEnabledBlockingInitialNavigation } from '@angular/router';

import { AppComponent } from './app/app.component';
import { enableProdMode } from '@angular/core';

if (import.meta.env.PROD) {
enableProdMode();
}

export default async function render(url: string, document: string) {
const html = await renderApplication(AppComponent, {
appId: 'analog-app',
document,
url,
providers: [
provideFileRouter(withEnabledBlockingInitialNavigation()),
{ provide: SERVER_CONTEXT, useValue: 'ssr-analog' },
],
});

return html;
}
5 changes: 2 additions & 3 deletions apps/analog-app/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import 'zone.js';
import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { provideHttpClient } from '@angular/common/http';
import { provideFileRouter } from '@analogjs/router';

import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
providers: [provideFileRouter(), importProvidersFrom(HttpClientModule)],
providers: [provideFileRouter(), provideHttpClient()],
}).catch((err) => console.error(err));
2 changes: 1 addition & 1 deletion apps/analog-app/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"target": "ES2022",
"useDefineForClassFields": false
},
"files": ["src/main.ts"],
"files": ["src/main.ts", "src/main.server.ts"],
"include": ["src/**/*.d.ts", "src/app/routes/**/*.ts"],
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
17 changes: 11 additions & 6 deletions apps/analog-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import { defineConfig, Plugin, splitVendorChunkPlugin } from 'vite';
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
return {
publicDir: 'src/public',
optimizeDeps: {
include: ['@angular/common', '@angular/forms'],
},
build: {
target: ['es2020'],
},
resolve: {
mainFields: ['module'],
},
plugins: [
analog({
ssr: true,
ssrBuildDir: '../../dist/apps/analog-app/ssr',
entryServer: 'apps/analog-app/src/main.server.ts',
vite: {
inlineStylesExtension: 'scss',
tsconfig:
Expand All @@ -26,11 +27,15 @@ export default defineConfig(({ mode }) => {
: 'apps/analog-app/tsconfig.app.json',
},
nitro: {
rootDir: `apps/analog-app/src`,
rootDir: 'apps/analog-app',
output: {
dir: `../../../../dist/apps/analog-app/server`,
dir: '../../../dist/apps/analog-app/server',
},
buildDir: `../../../dist/apps/analog-app/.nitro`,
publicAssets: [{ dir: `../../../dist/apps/analog-app/client` }],
serverAssets: [
{ baseName: 'public', dir: `./dist/apps/analog-app/client` },
],
buildDir: '../../dist/apps/analog-app/.nitro',
},
}),
visualizer() as Plugin,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "nx run-many --target build --all",
"build:vite-ci": "npm run build -- --exclude analog-app,docs-app",
"test:vite-ci": "npm run test",
"e2e": "nx run-many --target e2e --all --parallel=1",
"e2e": "nx run-many --target e2e --all --parallel=1 --exclude analog-app-e2e-playwright",
"test": "nx run-many --target test --all",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"contributors:add": "all-contributors add",
Expand Down
26 changes: 26 additions & 0 deletions packages/platform/src/lib/build-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { loadEsmModule } from '@angular-devkit/build-angular/src/utils/load-esm';
import { NitroConfig } from 'nitropack';

import { Options } from './options';

export async function buildServer(
options?: Options,
nitroConfig?: NitroConfig
) {
const { createNitro, build, prepare, copyPublicAssets, prerender } =
await loadEsmModule<typeof import('nitropack')>('nitropack');

const nitro = await createNitro({
dev: false,
...nitroConfig,
});
await prepare(nitro);
await copyPublicAssets(nitro);
await prerender(nitro);

if (!options?.prerender) {
await build(nitro);
}

await nitro.close();
}
4 changes: 4 additions & 0 deletions packages/platform/src/lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import type { PluginOptions } from '@analogjs/vite-plugin-angular';
import { NitroConfig } from 'nitropack';

export interface Options {
ssr?: boolean;
ssrBuildDir?: string;
prerender?: boolean;
entryServer?: string;
vite?: PluginOptions;
nitro?: NitroConfig;
}
12 changes: 9 additions & 3 deletions packages/platform/src/lib/platform-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import angular from '@analogjs/vite-plugin-angular';
import { Options } from './options';
import { viteNitroPlugin } from './vite-nitro-plugin';
import { routerPlugin } from './router-plugin';
import { devServerPlugin } from './ssr/dev-server-plugin';
import { ssrBuildPlugin } from './ssr/ssr-build-plugin';

export function platformPlugin(opts?: Options): Plugin[] {
export function platformPlugin(opts: Options = {}): Plugin[] {
return [
viteNitroPlugin(opts?.nitro),
...angular(opts?.vite),
viteNitroPlugin(opts, opts?.nitro),
(opts.ssr ? ssrBuildPlugin() : false) as Plugin,
...routerPlugin(),
(opts.ssr
? devServerPlugin({ entryServer: opts.entryServer })
: false) as Plugin,
...angular(opts?.vite),
];
}
8 changes: 6 additions & 2 deletions packages/platform/src/lib/router-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import type { Plugin } from 'vite';
export function routerPlugin(): Plugin[] {
return [
{
name: 'analogjs-router-config',
name: 'analogjs-router-plugin',
config() {
return {
ssr: {
noExternal: ['@analogjs/router', '@angular/**'],
},
optimizeDeps: {
exclude: ['@analogjs/router'],
include: ['rxjs'],
exclude: ['@analogjs/router', '@angular/platform-server'],
},
};
},
Expand Down
7 changes: 7 additions & 0 deletions packages/platform/src/lib/runtime/api-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { eventHandler } from 'h3';

export default eventHandler(async (event) => {
if (event.req.url?.startsWith('/api')) {
return $fetch(`${event.req.url?.replace('/api', '')}`);
}
});
15 changes: 15 additions & 0 deletions packages/platform/src/lib/runtime/renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { eventHandler } from 'h3';
// @ts-ignore
import { useStorage } from '#imports';

export default eventHandler(async (event) => {
// @ts-ignore
const render = (await import('#build/../ssr/main.server.mjs'))['default'];

// @ts-ignore
const template = await useStorage().getItem(`/assets/public:index.html`);

const html = await render(event.req.url, template);

return html;
});
16 changes: 16 additions & 0 deletions packages/platform/src/lib/ssr/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { build, mergeConfig, UserConfig } from 'vite';
import { Options } from '../options';

export async function buildSSRApp(config: UserConfig, options?: Options) {
const ssrBuildConfig = mergeConfig(config, {
build: {
ssr: true,
rollupOptions: {
input: options?.entryServer || './src/main.server.ts',
},
outDir: options?.ssrBuildDir || './dist/ssr',
},
});

await build(ssrBuildConfig);
}
Loading

0 comments on commit 965ed61

Please sign in to comment.