-
Notifications
You must be signed in to change notification settings - Fork 0
/
prerender.ts
161 lines (124 loc) · 4.49 KB
/
prerender.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { existsSync, mkdir, readFileSync, writeFile, copyFileSync } from 'node:fs';
import { join } from 'node:path';
import { promisify } from 'node:util';
import 'zone.js/node';
import { MainComponent } from './src/app/main.component';
import { routes } from './src/app/app.routes';
import { ApplicationConfig, Provider } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { BEFORE_APP_SERIALIZED, provideServerRendering, renderApplication } from '@angular/platform-server';
import { DOCUMENT } from '@angular/common';
import { provideRouter } from '@angular/router';
const distFolder = join(process.cwd(), 'dist/browser');
// Copy index.html to index.original.html if not exists
let indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index.html';
if (indexHtml === 'index.html') {
copyFileSync(join(distFolder, 'index.html'), join(distFolder, 'index.original.html'));
indexHtml = 'index.original.html';
}
const documentPath = join(distFolder, indexHtml);
const document = readFileSync(documentPath).toString();
const writeFileAsync = promisify(writeFile);
const mkdirAsync = promisify(mkdir);
const cache = new Map<string, string>();
function removeAll<K extends keyof HTMLElementTagNameMap>(nodes: NodeListOf<HTMLElementTagNameMap[K]>) {
const elements = Array.from(nodes);
for (const element of elements) {
element.remove();
}
}
const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideServerRendering(),
{
provide: BEFORE_APP_SERIALIZED,
useFactory: (document: Document) => () => {
// Remove inlined "critical" styles
removeAll(document.head.querySelectorAll('style'));
// Remove noscript > link[rel="stylesheet"] elements
removeAll(document.head.querySelectorAll('noscript'));
// Remove style imports and replace them with inlined styles
const links = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]'));
for (const link of links) {
const name = link.getAttribute('href');
link.remove();
if (!name) {
console.warn('Missing href attribute on link', link);
return;
}
let css = cache.get(name);
if (!css) {
// Reading file and writing it to the final html file
css = readFileSync(join(distFolder, name))
.toString()
// Remove comments
.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '')
// Remove newlines
.replace(/\n/g, ' ')
// Remove multiple spaces
.replace(/\s{2,}/g, ' ');
cache.set(name, css);
}
document.head.appendChild(document.createElement('style')).textContent = css;
}
},
deps: [DOCUMENT],
multi: true
}
]
};
function render(url: string, platformProviders?: Provider[]) {
return renderApplication(() => bootstrapApplication(MainComponent, appConfig), {
url,
document,
platformProviders
});
}
enum Rules {
DEFAULT,
GITHUB
}
async function writeHtml(rule: Rules, path: string, html: string) {
switch (rule) {
case Rules.DEFAULT: {
// Create folder if not exists
const folder = join(distFolder, path);
if (!existsSync(folder)) {
await mkdirAsync(folder, { recursive: true });
}
await writeFileAsync(join(distFolder, path, 'index.html'), html);
break;
}
// https://stackoverflow.com/questions/33270605/github-pages-trailing-slashes
case Rules.GITHUB: {
const parts = path.split('/').filter((part) => part !== '');
// Parts is empty if path is '/'
const file = parts.pop() ?? 'index';
const base = join(...parts);
const folder = join(distFolder, base);
if (!existsSync(folder)) {
await mkdirAsync(folder, { recursive: true });
}
await writeFileAsync(join(distFolder, base, `${file}.html`), html);
break;
}
}
}
(async () => {
for (const route of routes) {
console.info('Prerendering', route.path ?? route);
if (typeof route.path !== 'string') {
console.warn('Skipping route without path', route);
continue;
}
const path = route.path === '**' ? '404' : route.path;
try {
const html = await render(path, []);
await writeHtml(Rules.GITHUB, path, html);
} catch (err) {
console.error(`Error rendering ${route}`, err);
}
}
console.info('Done');
})();