Visitar URL original
feat!: option prerender.format, mirroring angular/angular-cli#34180 (… · angular-schule/prerender-format@4d75e1b · GitHub
Skip to content

Commit 4d75e1b

Browse files
committed
feat!: option prerender.format, mirroring angular/angular-cli#34180 (0.3.0)
The "file" format is configured as `"prerender": { "format": "file" }`, the same option as in the pull request for the Angular CLI. The builder takes `format` out of the prerender option, so it also works with `"outputMode": "static"`. Routes are renamed from `<route>/index.html` to `<route>.html` without special rules for individual routes. BREAKING CHANGE: the top-level option `prerenderFormat` is replaced by `prerender.format`. Run `ng add @angular-schule/prerender-format` again or move the value into the `prerender` object.
1 parent 32680f2 commit 4d75e1b

15 files changed

Lines changed: 171 additions & 197 deletions

‎.github/scripts/test-angular-app.sh‎

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
# package-tgz: packed @angular-schule/prerender-format
77
# angular-version-range: version range for all @angular/* packages, e.g. "^22.0.0" or "~22.0.0"
88
#
9-
# Checks: ng add and ng build warn for outputMode "server", ng add + ng build write <route>.html files
10-
# (also for a second locale), and a route "index" stays index/index.html with a warning.
9+
# Checks: ng add and ng build warn for outputMode "server", ng add sets prerender.format "file",
10+
# and ng build writes <route>.html files (also for a second locale).
1111
set -euo pipefail
1212

1313
NG_NEW="$1"
@@ -67,6 +67,11 @@ expect_output "is not considered when the build produces a server" $NG build
6767

6868
set_build_option outputMode '"static"'
6969
$NG add @angular-schule/prerender-format --skip-confirmation
70+
node -e '
71+
const a = require("./angular.json");
72+
const format = a.projects["'"$APP"'"].architect.build.options.prerender?.format;
73+
if (format !== "file") { console.error("Expected prerender.format \"file\", got " + format); process.exit(1); }
74+
'
7075

7176
cat > src/app/app.routes.ts <<'EOF'
7277
import { Routes } from '@angular/router';
@@ -118,14 +123,4 @@ for locale in en-US de; do
118123
test ! -e "dist/$APP/browser/$locale/about/index.html" || { echo "Unexpected $locale/about/index.html"; exit 1; }
119124
done
120125

121-
# A route "index" would overwrite the start page: it stays index/index.html, with a warning
122-
node -e '
123-
const fs = require("fs");
124-
const file = "src/app/app.routes.ts";
125-
fs.writeFileSync(file, fs.readFileSync(file, "utf8").replace("];", ",\n { path: \"index\", component: App }\n];"));
126-
'
127-
expect_output "Route '/index' is written to 'index/index.html' instead" $NG build
128-
test -f "dist/$APP/browser/en-US/index/index.html" || { echo "Missing en-US/index/index.html"; exit 1; }
129-
test -f "dist/$APP/browser/en-US/index.html" || { echo "Missing en-US/index.html"; exit 1; }
130-
131126
echo "Angular $RANGE: prerender format 'file' successful"

‎CLAUDE.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance when working with code in this repository.
44

55
## Overview
66

7-
`@angular-schule/prerender-format` is an Angular CLI builder that wraps `@angular/build:application`. With `prerenderFormat: "file"`, prerendered routes are written as `foo.html` instead of `foo/index.html`, so static hosts serve `/foo` without a trailing slash redirect. It implements the idea from [angular/angular-cli#29173](https://github.com/angular/angular-cli/issues/29173); the pull request [angular/angular-cli#34180](https://github.com/angular/angular-cli/pull/34180) adds the same option to Angular itself. Structure and conventions follow [angular-cli-ghpages](https://github.com/angular-schule/angular-cli-ghpages).
7+
`@angular-schule/prerender-format` is an Angular CLI builder that wraps `@angular/build:application`. With `"prerender": { "format": "file" }`, prerendered routes are written as `foo.html` instead of `foo/index.html`, so static hosts serve `/foo` without a trailing slash redirect. It implements the idea from [angular/angular-cli#29173](https://github.com/angular/angular-cli/issues/29173); the pull request [angular/angular-cli#34180](https://github.com/angular/angular-cli/pull/34180) adds the same option to Angular itself. Structure and conventions follow [angular-cli-ghpages](https://github.com/angular-schule/angular-cli-ghpages).
88

99
## Development Commands
1010

@@ -26,7 +26,7 @@ Build process: `prebuild` (clean) → `build` (tsc) → `postbuild` (copy metada
2626
```bash
2727
npm run build:schema
2828
```
29-
`application/schema.json` is generated: the schema of `@angular/build:application` from the installed version plus `prerenderFormat`. Regenerate it after updating `@angular/build` and commit the result. `npm test` fails if it is out of sync.
29+
`application/schema.json` is generated: the schema of `@angular/build:application` from the installed version plus `format` in the `prerender` object. Regenerate it after updating `@angular/build` and commit the result. `npm test` fails if it is out of sync.
3030

3131
### Test
3232
```bash
@@ -72,21 +72,21 @@ npm dist-tag add @angular-schule/prerender-format@X.X.X-rc.X next
7272

7373
## Principle: warn and fall back, never fail
7474

75-
The option must never make a build fail. Whenever the "file" format does not apply, log a warning and keep the behavior of `@angular/build:application` (`<route>/index.html`), for the whole build or for a single route. No `throw` and no `try/catch` for control flow: functions that can fail return a result (see `InstallResult`). The only exceptions are the `SchematicsException`s of `ng add` for invalid input, as the Angular schematics API expects.
75+
The option must never make a build fail. Whenever the "file" format does not apply, log a warning and keep the behavior of `@angular/build:application` (`<route>/index.html`). No `throw` and no `try/catch` for control flow: functions that can fail return a result (see `InstallResult`). The only exceptions are the `SchematicsException`s of `ng add` for invalid input, as the Angular schematics API expects.
7676

7777
## Architecture
7878

7979
1. **Builder** (`src/application/`):
80-
- `builder.ts` - Angular builder entry point, called by `ng build`. Strips `prerenderFormat` and delegates to `buildApplication`. Whenever the "file" format does not apply (a server is produced, the internals of `@angular/build` differ, nothing went through the wrapper), it logs a warning and the build behaves like `@angular/build:application`.
80+
- `builder.ts` - Angular builder entry point, called by `ng build`. Takes `format` out of the `prerender` option (`toApplicationOptions()`) and delegates to `buildApplication`. With `outputMode`, `@angular/build` ignores `prerender` and warns about it, so a `prerender` object that only carries `format` is dropped there. Whenever the "file" format does not apply (a server is produced, the internals of `@angular/build` differ, nothing went through the wrapper), it logs a warning and the build behaves like `@angular/build:application`.
8181
- `ships-server.ts` - The rule for "this build ships an SSR server", shared by builder and `ng add`.
8282
- `file-format.ts` - Wraps the internal `prerenderPages()` of `@angular/build`. `execute-post-bundle.js` reads it from the module's exports object at call time, so replacing the export takes effect for regular and localized builds.
8383
- `schema.json` - Generated, see above.
8484

8585
2. **Schematic** (`src/ng-add.ts`):
8686
- Implements `ng add @angular-schule/prerender-format`
87-
- Swaps the build target's builder and sets `prerenderFormat: "file"`
87+
- Swaps the build target's builder and sets `prerender.format: "file"`, keeping other `prerender` settings
8888
- Warns if the build target or one of its configurations produces an SSR server
8989

9090
### Internal API
9191

92-
`prerenderPages()` in `@angular/build/src/utils/server-rendering/prerender.js` is not public. If the module or the export is missing, `installFileFormat()` returns the reason and the builder warns instead of patching, and `file-format.spec.ts` checks the call site in `execute-post-bundle.js`. The package supports Angular 22 only. `.github/scripts/test-angular-app.sh` builds a fresh Angular app (lowest and latest 22.x in CI) and checks `ng add`, the server warning, the "file" output for two locales and the `/index` fallback.
92+
`prerenderPages()` in `@angular/build/src/utils/server-rendering/prerender.js` is not public. If the module or the export is missing, `installFileFormat()` returns the reason and the builder warns instead of patching, and `file-format.spec.ts` checks the call site in `execute-post-bundle.js`. The package supports Angular 22 only. `.github/scripts/test-angular-app.sh` builds a fresh Angular app (lowest and latest 22.x in CI) and checks `ng add`, the server warning, and the "file" output for two locales.

‎README.md‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The same route becomes `blog/my-article.html`, and hosts like Cloudflare Pages s
4141
- **Old links keep working:** addresses with a trailing slash redirect to the address without it (measured on Cloudflare Pages, see [Hosts](#hosts)).
4242

4343
The idea comes from [angular/angular-cli#29173](https://github.com/angular/angular-cli/issues/29173), which asks for an option to write `<route>.html` instead of `<route>/index.html`.
44-
The pull request [angular/angular-cli#34180](https://github.com/angular/angular-cli/pull/34180) adds such an option to Angular itself, with the same name and values as this builder: `prerenderFormat`, `'directory'` or `'file'`.
44+
The pull request [angular/angular-cli#34180](https://github.com/angular/angular-cli/pull/34180) adds the same option to Angular itself: `prerender.format` with `'directory'` or `'file'`.
4545
The builder is a stopgap: developed and tested for Angular 22, until Angular has a built-in option and this package is no longer needed.
4646
If you want the option in Angular, please give the issue and the pull request a 👍.
4747

@@ -62,15 +62,17 @@ ng build
6262

6363
## ⚙️ Installation <a name="installation"></a>
6464

65-
`ng add @angular-schule/prerender-format` installs the package and changes the builder of your build target in `angular.json` and sets `prerenderFormat`.
66-
`prerenderFormat: "file"` only applies to static builds. If the build target (or one of its configurations) produces an SSR server, `ng add` still sets it up and shows a warning.
65+
`ng add @angular-schule/prerender-format` installs the package, changes the builder of your build target in `angular.json` and sets `prerender.format`.
66+
`"format": "file"` only applies to static builds. If the build target (or one of its configurations) produces an SSR server, `ng add` still sets it up and shows a warning.
6767

6868
```json
6969
"build": {
7070
"builder": "@angular-schule/prerender-format:application",
7171
"options": {
7272
"outputMode": "static",
73-
"prerenderFormat": "file"
73+
"prerender": {
74+
"format": "file"
75+
}
7476
}
7577
}
7678
```
@@ -80,12 +82,12 @@ Use `--project` to choose the project in a workspace with several projects.
8082

8183
## 📦 Options <a name="options"></a>
8284

83-
#### prerenderFormat
85+
#### prerender.format
8486

8587
- **optional**
8688
- Default: `directory`
8789

88-
| `prerenderFormat` | Route `blog/my-article` | Start page |
90+
| `prerender.format` | Route `blog/my-article` | Start page |
8991
|---|---|---|
9092
| `directory` | `blog/my-article/index.html` | `index.html` |
9193
| `file` | `blog/my-article.html` | `index.html` |
@@ -95,11 +97,11 @@ The name and the values follow Astro's [`build.format`](#other-frameworks).
9597
Parent and child routes live side by side: `blog.html` next to the folder `blog/`.
9698
The start page of each locale (for example with base href `/en/`) stays `index.html`.
9799

98-
If `blog/my-article.html` is not safe for a route, that route keeps `blog/my-article/index.html` and the build shows a warning. This applies to a route whose last segment is `index` in any letter case (`/index`, `/blog/index`), to a file name the build uses itself (such as `index.csr.html`), and to a file name another route already uses.
100+
`prerender.format` is also considered with `"outputMode": "static"`, where Angular ignores the other `prerender` settings.
99101

100102
## 🔭 Other frameworks <a name="other-frameworks"></a>
101103

102-
Static site generators have offered this choice for a long time. We decided to borrow the terminology from Astro: `build.format` with `'directory'` and `'file'` became `prerenderFormat` with the same values.
104+
Static site generators have offered this choice for a long time. We decided to borrow the terminology from Astro: `build.format` with `'directory'` and `'file'` became `prerender.format` with the same values.
103105

104106
| Framework | Option | `blog/my-article/index.html` | `blog/my-article.html` |
105107
|---|---|---|---|
@@ -108,13 +110,13 @@ Static site generators have offered this choice for a long time. We decided to b
108110
| SvelteKit | `trailingSlash` | `'always'` | `'never'` (default) |
109111
| Nuxt 2 | `generate.subFolders` | `true` (default) | `false` |
110112
| Hugo | `uglyURLs` | `false` (default) | `true` |
111-
| **Angular** | **NEW: `prerenderFormat`** | **`'directory'` (default)** | **`'file'`** |
113+
| **Angular** | **NEW: `prerender.format`** | **`'directory'` (default)** | **`'file'`** |
112114

113115
Astro's documentation recommends `build.format: 'file'` together with `trailingSlash: 'never'`, which is exactly the combination this builder enables for Angular.
114116

115117
## 🌍 Hosts <a name="hosts"></a>
116118

117-
Measured on **Cloudflare Pages** with `prerenderFormat: "file"`:
119+
Measured on **Cloudflare Pages** with `"prerender": { "format": "file" }`:
118120

119121
| Request | Response |
120122
|---|---|
@@ -130,7 +132,8 @@ Check your host before switching.
130132

131133
## 🔧 How it works <a name="how-it-works"></a>
132134

133-
The builder calls `buildApplication` from `@angular/build` and wraps its internal `prerenderPages()` function, which returns the prerendered pages as a record of output paths.
135+
The builder takes `format` out of the `prerender` option and calls `buildApplication` from `@angular/build` with all other options.
136+
It wraps the internal `prerenderPages()` function of `@angular/build`, which returns the prerendered pages as a record of output paths.
134137
The wrapper renames `blog/my-article/index.html` to `blog/my-article.html` before anything is written, so the service worker manifest and all later build steps see the final file names.
135138

136139
`prerenderPages()` is internal API, so this package supports Angular 22 only.
@@ -139,7 +142,7 @@ The option never makes your build fail.
139142

140143
## 📁 Known limitations <a name="limitations"></a>
141144

142-
- **Static builds only, by design.** `prerenderFormat: "file"` solves a problem of static hosting and makes no sense in other setups. An `ssr` entry is fine as long as `"outputMode"` is `"static"`: Angular then uses it only during `ng build` to prerender the pages, and no server is deployed. If a server is deployed, the option is not considered and the build shows a warning: a server needs no `.html` files, it answers `/blog/my-article` directly without redirecting to `/blog/my-article/`, and the Angular SSR server looks up prerendered pages as `index.html`.
145+
- **Static builds only, by design.** `"format": "file"` solves a problem of static hosting and makes no sense in other setups. An `ssr` entry is fine as long as `"outputMode"` is `"static"`: Angular then uses it only during `ng build` to prerender the pages, and no server is deployed. If a server is deployed, the option is not considered and the build shows a warning: a server needs no `.html` files, it answers `/blog/my-article` directly without redirecting to `/blog/my-article/`, and the Angular SSR server looks up prerendered pages as `index.html`.
143146

144147
✅ Works: static output, the `ssr` entry only renders at build time
145148

@@ -148,7 +151,7 @@ The option never makes your build fail.
148151
"outputMode": "static",
149152
"server": "src/main.server.ts",
150153
"ssr": { "entry": "src/server.ts" },
151-
"prerenderFormat": "file"
154+
"prerender": { "format": "file" }
152155
}
153156
```
154157

@@ -157,8 +160,7 @@ The option never makes your build fail.
157160
```json
158161
"options": {
159162
"server": "src/main.server.ts",
160-
"prerender": true,
161-
"prerenderFormat": "file"
163+
"prerender": { "format": "file" }
162164
}
163165
```
164166

@@ -169,7 +171,7 @@ The option never makes your build fail.
169171
"outputMode": "server",
170172
"server": "src/main.server.ts",
171173
"ssr": { "entry": "src/server.ts" },
172-
"prerenderFormat": "file"
174+
"prerender": { "format": "file" }
173175
}
174176
```
175177

@@ -179,7 +181,7 @@ The option never makes your build fail.
179181
"options": {
180182
"server": "src/main.server.ts",
181183
"ssr": { "entry": "src/server.ts" },
182-
"prerenderFormat": "file"
184+
"prerender": { "format": "file" }
183185
}
184186
```
185187

‎src/application/builder.spec.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { toApplicationOptions } from './builder';
2+
3+
describe('toApplicationOptions', () => {
4+
it('uses the "directory" format without a prerender object', () => {
5+
expect(toApplicationOptions({ outputMode: 'static', prerender: true } as never)).toEqual({
6+
applicationOptions: { outputMode: 'static', prerender: true },
7+
format: 'directory'
8+
});
9+
});
10+
11+
it('removes the format from the prerender options of a build without outputMode', () => {
12+
expect(toApplicationOptions({ prerender: { format: 'file', discoverRoutes: true } } as never)).toEqual({
13+
applicationOptions: { prerender: { discoverRoutes: true } },
14+
format: 'file'
15+
});
16+
});
17+
18+
it('drops a prerender object that only carries the format when outputMode prerenders pages', () => {
19+
expect(
20+
toApplicationOptions({
21+
outputMode: 'static',
22+
server: 'src/main.server.ts',
23+
prerender: { format: 'file', discoverRoutes: true }
24+
} as never)
25+
).toEqual({
26+
applicationOptions: { outputMode: 'static', server: 'src/main.server.ts', prerender: undefined },
27+
format: 'file'
28+
});
29+
});
30+
31+
it('keeps the other prerender options with outputMode, so that @angular/build warns about them', () => {
32+
expect(
33+
toApplicationOptions({
34+
outputMode: 'static',
35+
server: 'src/main.server.ts',
36+
prerender: { format: 'file', routesFile: 'routes.txt' }
37+
} as never)
38+
).toEqual({
39+
applicationOptions: {
40+
outputMode: 'static',
41+
server: 'src/main.server.ts',
42+
prerender: { routesFile: 'routes.txt' }
43+
},
44+
format: 'file'
45+
});
46+
});
47+
});

0 commit comments

Comments
 (0)