Skip to content

Commit 48fc292

Browse files
committed
feat!: prerender.format as a separate object, as in angular/angular-cli#34180 (0.4.0)
As in the pull request after its review, `format` is a separate object in the `oneOf` of the prerender option, so it cannot be combined with routesFile and discoverRoutes, which only apply without outputMode. `format` is required in that object, so `"prerender": {}` keeps matching the object of @angular/build only. Schema defaults no longer add `format` to that object, so with outputMode the builder warns for every prerender value other than an object with `format`. A schema test covers both objects. BREAKING CHANGE: `prerender.format` cannot be combined with `routesFile` or `discoverRoutes`. `ng add` stops if the prerender option contains them.
1 parent 16bdb84 commit 48fc292

11 files changed

Lines changed: 117 additions & 46 deletions

File tree

‎CLAUDE.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 `format` in the `prerender` object. 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 a separate object with `format` in the `oneOf` of `prerender`, as in the pull request, so `format` cannot be combined with `routesFile` and `discoverRoutes`. `format` is required in that object, so `{}` keeps matching the object of `@angular/build` only. Regenerate it after updating `@angular/build` and commit the result. `npm test` fails if it is out of sync.
3030

3131
### Test
3232
```bash
@@ -77,14 +77,14 @@ The option must never make a build fail. Whenever the "file" format does not app
7777
## Architecture
7878

7979
1. **Builder** (`src/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`, so the builder drops it there and applies only `format`. If one of the other `prerender` settings has no effect, the builder warns with the rule and the message of the pull request. 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`, so the builder drops it there and applies only `format`. If `prerender` is set to anything other than an object with `format`, the builder warns with the rule and the message of the pull request. 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 `prerender.format: "file"`, keeping other `prerender` settings
87+
- Swaps the build target's builder and sets `"prerender": { "format": "file" }`; stops if `prerender` contains `routesFile` or `discoverRoutes`, which cannot be combined with `format`
8888
- Warns if the build target or one of its configurations produces an SSR server
8989

9090
### Internal API

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ng build
6464

6565
`ng add @angular-schule/prerender-format` installs the package, changes the builder of your build target in `angular.json` and sets `prerender.format`.
6666
`"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.
67+
If `prerender` contains `routesFile` or `discoverRoutes`, `ng add` stops, as they cannot be combined with `format`.
6768

6869
```json
6970
"build": {
@@ -97,7 +98,8 @@ The name and the values follow Astro's [`build.format`](#other-frameworks).
9798
Parent and child routes live side by side: `blog.html` next to the folder `blog/`.
9899
The start page of each locale (for example with base href `/en/`) stays `index.html`.
99100

100-
`prerender.format` is also considered with `"outputMode": "static"`, where Angular ignores the other `prerender` settings.
101+
`prerender.format` cannot be combined with `routesFile` and `discoverRoutes`, which only apply to builds without `"outputMode"`.
102+
With `"outputMode": "static"`, Angular ignores every other value of `prerender`, and `prerender.format` is considered.
101103

102104
## 🔭 Other frameworks <a name="other-frameworks"></a>
103105

‎src/application/builder.spec.ts‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,28 @@ describe('toApplicationOptions', () => {
99
});
1010
});
1111

12-
it('removes the format from the prerender options of a build without outputMode', () => {
13-
expect(toApplicationOptions({ prerender: { format: 'file', discoverRoutes: true } } as never)).toEqual({
14-
applicationOptions: { prerender: { discoverRoutes: true } },
12+
it('removes the format from the prerender option of a build without outputMode', () => {
13+
expect(toApplicationOptions({ prerender: { format: 'file' } } as never)).toEqual({
14+
applicationOptions: { prerender: {} },
1515
format: 'file',
1616
prerenderIgnored: false
1717
});
1818
});
1919

20+
it('passes the prerender settings of @angular/build through without outputMode', () => {
21+
expect(toApplicationOptions({ prerender: { routesFile: 'routes.txt', discoverRoutes: true } } as never)).toEqual({
22+
applicationOptions: { prerender: { routesFile: 'routes.txt', discoverRoutes: true } },
23+
format: 'directory',
24+
prerenderIgnored: false
25+
});
26+
});
27+
2028
it('drops the prerender option with outputMode and keeps the format', () => {
2129
expect(
2230
toApplicationOptions({
2331
outputMode: 'static',
2432
server: 'src/main.server.ts',
25-
prerender: { format: 'file', discoverRoutes: true }
33+
prerender: { format: 'file' }
2634
} as never)
2735
).toEqual({
2836
applicationOptions: { outputMode: 'static', server: 'src/main.server.ts' },
@@ -32,14 +40,15 @@ describe('toApplicationOptions', () => {
3240
});
3341

3442
it.each([
35-
['routesFile', { format: 'file', routesFile: 'routes.txt', discoverRoutes: true }],
36-
['discoverRoutes: false', { format: 'file', discoverRoutes: false }],
43+
['routesFile', { routesFile: 'routes.txt', discoverRoutes: true }],
44+
['discoverRoutes: false', { discoverRoutes: false }],
45+
['discoverRoutes: true', { discoverRoutes: true }],
3746
['true', true],
3847
['false', false]
3948
])('reports prerender with %s as ignored with outputMode', (_, prerender) => {
4049
expect(toApplicationOptions({ outputMode: 'static', server: 'src/main.server.ts', prerender } as never)).toEqual({
4150
applicationOptions: { outputMode: 'static', server: 'src/main.server.ts' },
42-
format: typeof prerender === 'object' ? 'file' : 'directory',
51+
format: 'directory',
4352
prerenderIgnored: true
4453
});
4554
});

‎src/application/builder.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ type PrerenderObject = Exclude<ApplicationBuilderOptions['prerender'], boolean |
1111

1212
export interface Schema extends Omit<ApplicationBuilderOptions, 'prerender'> {
1313
/**
14-
* Same as the `prerender` option of `@angular/build:application`, plus `format`:
14+
* Same as the `prerender` option of `@angular/build:application`, or an object with `format`
15+
* (`schema.json` does not allow `format` together with `routesFile` or `discoverRoutes`):
1516
* - `directory` (default): `/foo` is written to `foo/index.html`
1617
* - `file`: `/foo` is written to `foo.html`, the start page stays `index.html`
1718
*/
@@ -21,8 +22,8 @@ export interface Schema extends Omit<ApplicationBuilderOptions, 'prerender'> {
2122
/**
2223
* Splits `prerender.format` from the options that `@angular/build:application` understands.
2324
* With `outputMode`, `@angular/build` does not consider the `prerender` option, only `format` applies.
24-
* The `prerender` option is dropped there, and `prerenderIgnored` tells whether one of its other
25-
* settings has no effect, with the rule of angular/angular-cli#34180.
25+
* The `prerender` option is dropped there, and `prerenderIgnored` tells whether it is set to anything
26+
* other than an object with `format`, with the rule of angular/angular-cli#34180.
2627
*/
2728
export function toApplicationOptions(options: Schema): {
2829
applicationOptions: ApplicationBuilderOptions;
@@ -40,12 +41,8 @@ export function toApplicationOptions(options: Schema): {
4041
};
4142
}
4243

43-
// Values are compared, as schema defaults are added to the "prerender" object.
4444
const prerenderIgnored =
45-
prerender !== undefined &&
46-
(typeof prerender !== 'object' ||
47-
prerenderOptions.routesFile !== undefined ||
48-
prerenderOptions.discoverRoutes === false);
45+
prerender !== undefined && (typeof prerender === 'boolean' || !('format' in prerender));
4946

5047
return { applicationOptions: rest, format, prerenderIgnored };
5148
}

‎src/application/schema.json‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -620,18 +620,26 @@
620620
"type": "boolean",
621621
"description": "Whether the builder should process the Angular Router configuration to find all unparameterized routes and prerender them.",
622622
"default": true
623-
},
623+
}
624+
},
625+
"additionalProperties": false
626+
},
627+
{
628+
"type": "object",
629+
"properties": {
624630
"format": {
625631
"type": "string",
626632
"description": "Defines the file layout of prerendered pages. 'directory': '/foo' is written to 'foo/index.html'. 'file': '/foo' is written to 'foo.html', which some hosting services serve for '/foo' without a redirect to '/foo/'. The root route of the application and of each locale is always written to 'index.html'. Only considered when the build does not produce a server.",
627633
"enum": [
628634
"directory",
629635
"file"
630-
],
631-
"default": "directory"
636+
]
632637
}
633638
},
634-
"additionalProperties": false
639+
"additionalProperties": false,
640+
"required": [
641+
"format"
642+
]
635643
}
636644
]
637645
},

‎src/application/schema.spec.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { schema } from '@angular-devkit/core';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
const applicationSchema = JSON.parse(fs.readFileSync(path.join(__dirname, 'schema.json'), 'utf8'));
6+
7+
/** Validates `prerender` as `ng build` does for this builder, including the schema defaults. */
8+
async function validatePrerender(prerender: unknown): Promise<{ success: boolean; prerender: unknown }> {
9+
const registry = new schema.CoreSchemaRegistry();
10+
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
11+
const validator = await registry.compile(applicationSchema);
12+
const { success, data } = await validator({ tsConfig: 'tsconfig.app.json', prerender } as never);
13+
14+
return { success, prerender: (data as { prerender: unknown }).prerender };
15+
}
16+
17+
describe('application/schema.json', () => {
18+
it('accepts an object with format', async () => {
19+
expect(await validatePrerender({ format: 'file' })).toEqual({ success: true, prerender: { format: 'file' } });
20+
});
21+
22+
it.each([{}, { routesFile: 'routes.txt' }, { discoverRoutes: false }])(
23+
'keeps %o an object of @angular/build, without format',
24+
async prerender => {
25+
const result = await validatePrerender(prerender);
26+
27+
expect(result.success).toBe(true);
28+
expect(Object.keys(result.prerender as object)).not.toContain('format');
29+
}
30+
);
31+
32+
it.each([
33+
{ format: 'file', routesFile: 'routes.txt' },
34+
{ format: 'file', discoverRoutes: false }
35+
])('rejects format together with the settings of @angular/build: %o', async prerender => {
36+
expect((await validatePrerender(prerender)).success).toBe(false);
37+
});
38+
});

‎src/ng-add.spec.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,17 @@ describe('ng-add', () => {
6969
});
7070
});
7171

72-
it('adds the format to existing prerender settings', async () => {
72+
it('refuses prerender settings that cannot be combined with the format', async () => {
7373
const tree = Tree.empty();
74-
tree.create('angular.json', angularJson({ site: app(undefined, { prerender: { routesFile: 'routes.txt' } }) }));
75-
76-
await ngAdd({ project: 'site' })(tree, mockContext);
74+
tree.create(
75+
'angular.json',
76+
angularJson({ site: app(undefined, { prerender: { routesFile: 'routes.txt', discoverRoutes: false } }) })
77+
);
7778

78-
expect(buildTarget(tree, 'site').options).toEqual({ prerender: { routesFile: 'routes.txt', format: 'file' } });
79+
await expect(ngAdd({ project: 'site' })(tree, mockContext)).rejects.toThrow(
80+
'"prerender.format" cannot be combined with "routesFile" and "discoverRoutes".'
81+
);
82+
expect(buildTarget(tree, 'site').builder).toBe('@angular/build:application');
7983
});
8084

8185
it('switches a build that produces a server, with a warning', async () => {

‎src/ng-add.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,17 @@ export const ngAdd = (options: NgAddOptions) => async (tree: Tree, context: Sche
6666
.map(([name]) => name);
6767

6868
const prerender = buildTarget.options?.prerender;
69+
const prerenderSettings = isJsonObject(prerender) ? Object.keys(prerender).filter(key => key !== 'format') : [];
70+
if (prerenderSettings.length > 0) {
71+
const settings = prerenderSettings.map(key => `"${key}"`).join(' and ');
72+
throw new SchematicsException(
73+
`"prerender.format" cannot be combined with ${settings}. ` +
74+
`Remove ${settings} from the "prerender" option of "${options.project}" and run ng add again.`
75+
);
76+
}
77+
6978
buildTarget.builder = BUILDER_NAME;
70-
buildTarget.options = {
71-
...buildTarget.options,
72-
prerender: { ...(isJsonObject(prerender) ? prerender : {}), format: 'file' }
73-
};
79+
buildTarget.options = { ...buildTarget.options, prerender: { format: 'file' } };
7480

7581
await workspaces.writeWorkspace(workspace, host);
7682

‎src/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@angular-schule/prerender-format",
3-
"version": "0.3.2",
3+
"version": "0.4.0",
44
"description": "Angular application builder that writes prerendered routes as foo.html instead of foo/index.html (no trailing slash redirects)",
55
"main": "index.js",
66
"types": "index.d.ts",

0 commit comments

Comments
 (0)