@angular-schule/prerender-format 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,7 +50,7 @@ If you want the option in Angular, please give the issue and the pull request a
50
50
  ## ⚠️ Prerequisites <a name="prerequisites"></a>
51
51
 
52
52
  - Angular 22 with the application builder (`@angular/build:application`)
53
- - `"outputMode": "static"`
53
+ - `"outputMode": "static"` (with a server, the option is not considered, see [Known limitations](#limitations))
54
54
  - A host that serves `blog/my-article.html` under `/blog/my-article` without a redirect (see [Hosts](#hosts))
55
55
 
56
56
  ## 🚀 Quick Start <a name="quickstart"></a>
@@ -63,7 +63,7 @@ ng build
63
63
  ## ⚙️ Installation <a name="installation"></a>
64
64
 
65
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
- Your build must already use `"outputMode": "static"`: if the build target (or one of its configurations) ships an SSR server, `ng add` stops and tells you so.
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.
67
67
 
68
68
  ```json
69
69
  "build": {
@@ -95,7 +95,7 @@ The name and the values follow Astro's [`build.format`](#other-frameworks).
95
95
  Parent and child routes live side by side: `blog.html` next to the folder `blog/`.
96
96
  The start page of each locale (for example with base href `/en/`) stays `index.html`.
97
97
 
98
- A route whose last segment is `index` (`/index`, `/docs/index`) fails the build with a clear message: as `index.html` it would be served under the parent path, and `/index` would overwrite the start page.
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.
99
99
 
100
100
  ## 🔭 Other frameworks <a name="other-frameworks"></a>
101
101
 
@@ -134,11 +134,12 @@ The builder calls `buildApplication` from `@angular/build` and wraps its interna
134
134
  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.
135
135
 
136
136
  `prerenderPages()` is internal API, so this package supports Angular 22 only.
137
- After a successful build, the builder checks that the prerendered pages actually went through the wrapper, and fails otherwise.
137
+ If the prerendered pages did not go through the wrapper (nothing was prerendered, or a version of `@angular/build` with different internals), the build shows a warning and keeps the directory layout.
138
+ The option never makes your build fail.
138
139
 
139
140
  ## 📁 Known limitations <a name="limitations"></a>
140
141
 
141
- - **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 build fails: 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`.
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`.
142
143
 
143
144
  ✅ Works: static output, the `ssr` entry only renders at build time
144
145
 
@@ -161,7 +162,7 @@ After a successful build, the builder checks that the prerendered pages actually
161
162
  }
162
163
  ```
163
164
 
164
- ❌ Fails: a server is deployed
165
+ ⚠️ Not considered, with a warning: a server is deployed
165
166
 
166
167
  ```json
167
168
  "options": {
@@ -172,7 +173,7 @@ After a successful build, the builder checks that the prerendered pages actually
172
173
  }
173
174
  ```
174
175
 
175
- ❌ Fails: SSR without `outputMode` also deploys a server
176
+ ⚠️ Not considered, with a warning: SSR without `outputMode` also deploys a server
176
177
 
177
178
  ```json
178
179
  "options": {
@@ -39,6 +39,14 @@ const build_1 = require("@angular/build");
39
39
  const path = __importStar(require("path"));
40
40
  const file_format_1 = require("./file-format");
41
41
  const ships_server_1 = require("./ships-server");
42
+ function reservedFiles(options) {
43
+ const files = ['index.csr.html', 'index.server.html'];
44
+ const index = options.index;
45
+ if (index && typeof index === 'object' && typeof index.output === 'string') {
46
+ files.push(path.posix.basename(index.output));
47
+ }
48
+ return files;
49
+ }
42
50
  async function* executeBuild(options, context) {
43
51
  const { prerenderFormat = 'directory', ...applicationOptions } = options;
44
52
  if (prerenderFormat !== 'file') {
@@ -46,31 +54,27 @@ async function* executeBuild(options, context) {
46
54
  return;
47
55
  }
48
56
  if ((0, ships_server_1.shipsServer)(applicationOptions)) {
49
- context.logger.error(`❌ 'prerenderFormat: "file"' is not supported when the build produces a server ` +
50
- `('outputMode: "server"', or 'ssr' without 'outputMode'): the Angular SSR server looks up prerendered pages as 'index.html'.`);
51
- yield { success: false };
57
+ context.logger.warn(`The "prerenderFormat" option set to "file" is not considered when the build produces a server ` +
58
+ `("outputMode" set to "server", or "ssr" without "outputMode"). Prerendered routes are written to '<route>/index.html'.`);
59
+ yield* (0, build_1.buildApplication)(applicationOptions, context);
52
60
  return;
53
61
  }
54
- try {
55
- (0, file_format_1.installFileFormat)(path.dirname(require.resolve('@angular/build/package.json')));
56
- }
57
- catch (e) {
58
- context.logger.error('❌ ' + (e instanceof Error ? e.message : String(e)));
59
- yield { success: false };
62
+ const install = (0, file_format_1.installFileFormat)(path.dirname(require.resolve('@angular/build/package.json')));
63
+ if (!install.installed) {
64
+ context.logger.warn(`The "prerenderFormat" option set to "file" is not considered: ${install.reason}`);
65
+ yield* (0, build_1.buildApplication)(applicationOptions, context);
60
66
  return;
61
67
  }
68
+ (0, file_format_1.setReservedFiles)(reservedFiles(applicationOptions));
62
69
  let callsBefore = (0, file_format_1.getPrerenderCalls)();
63
70
  for await (const result of (0, build_1.buildApplication)(applicationOptions, context)) {
64
71
  const calls = (0, file_format_1.getPrerenderCalls)();
65
72
  if (result.success && calls === callsBefore) {
66
- context.logger.error(`❌ 'prerenderFormat: "file"' had no effect: no pages were prerendered through @angular-schule/prerender-format. ` +
67
- `Check that prerendering is enabled ('outputMode: "static"' with server routes, or 'prerender'). ` +
73
+ context.logger.warn(`The "prerenderFormat" option set to "file" had no effect: no pages were prerendered through @angular-schule/prerender-format. ` +
74
+ `Check that prerendering is enabled ("outputMode" set to "static" with server routes, or "prerender"). ` +
68
75
  `If it is, this version of @angular/build is not supported.`);
69
- yield { ...result, success: false };
70
- }
71
- else {
72
- yield result;
73
76
  }
77
+ yield result;
74
78
  callsBefore = calls;
75
79
  }
76
80
  }
@@ -1 +1 @@
1
- {"version":3,"file":"builder.js","sourceRoot":"","sources":["../../application/builder.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,oCAgDC;AAvED,yDAAyF;AACzF,0CAA6E;AAC7E,2CAA6B;AAE7B,+CAAqE;AACrE,iDAA6C;AAkBtC,KAAK,SAAS,CAAC,CAAC,YAAY,CACjC,OAAe,EACf,OAAuB;IAEvB,MAAM,EAAE,eAAe,GAAG,WAAW,EAAE,GAAG,kBAAkB,EAAE,GAAG,OAAO,CAAC;IAEzE,IAAI,eAAe,KAAK,MAAM,EAAE,CAAC;QAC/B,KAAK,CAAC,CAAC,IAAA,wBAAgB,EAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QAErD,OAAO;IACT,CAAC;IAED,IAAI,IAAA,0BAAW,EAAC,kBAAkB,CAAC,EAAE,CAAC;QACpC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gFAAgF;YAC9E,6HAA6H,CAChI,CAAC;QACF,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAEzB,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,IAAA,+BAAiB,EAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAEzB,OAAO;IACT,CAAC;IAID,IAAI,WAAW,GAAG,IAAA,+BAAiB,GAAE,CAAC;IACtC,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAA,wBAAgB,EAAC,kBAAkB,EAAE,OAAO,CAAC,EAAE,CAAC;QACzE,MAAM,KAAK,GAAG,IAAA,+BAAiB,GAAE,CAAC;QAClC,IAAI,MAAM,CAAC,OAAO,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;YAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iHAAiH;gBAC/G,kGAAkG;gBAClG,4DAA4D,CAC/D,CAAC;YACF,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,CAAC;QACf,CAAC;QACD,WAAW,GAAG,KAAK,CAAC;IACtB,CAAC;AACH,CAAC;AAED,kBAAe,IAAA,yBAAa,EAAS,YAAY,CAAC,CAAC","sourcesContent":["import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';\nimport { ApplicationBuilderOptions, buildApplication } from '@angular/build';\nimport * as path from 'path';\n\nimport { getPrerenderCalls, installFileFormat } from './file-format';\nimport { shipsServer } from './ships-server';\n\nexport type PrerenderFormat = 'directory' | 'file';\n\nexport interface Schema extends ApplicationBuilderOptions {\n /**\n * File layout of prerendered routes.\n * - `directory` (default): `foo/index.html`\n * - `file`: `foo.html`, the start page stays `index.html`\n */\n prerenderFormat?: PrerenderFormat;\n}\n\n/**\n * Runs `@angular/build:application` and, with `prerenderFormat: \"file\"`,\n * writes prerendered routes as `foo.html` instead of `foo/index.html`.\n * Exported separately for testing purposes.\n */\nexport async function* executeBuild(\n options: Schema,\n context: BuilderContext\n): AsyncIterable<BuilderOutput> {\n const { prerenderFormat = 'directory', ...applicationOptions } = options;\n\n if (prerenderFormat !== 'file') {\n yield* buildApplication(applicationOptions, context);\n\n return;\n }\n\n if (shipsServer(applicationOptions)) {\n context.logger.error(\n `❌ 'prerenderFormat: \"file\"' is not supported when the build produces a server ` +\n `('outputMode: \"server\"', or 'ssr' without 'outputMode'): the Angular SSR server looks up prerendered pages as 'index.html'.`\n );\n yield { success: false };\n\n return;\n }\n\n try {\n installFileFormat(path.dirname(require.resolve('@angular/build/package.json')));\n } catch (e) {\n context.logger.error('❌ ' + (e instanceof Error ? e.message : String(e)));\n yield { success: false };\n\n return;\n }\n\n // Every successful build must have passed its prerendered pages through the wrapper.\n // Otherwise nothing was prerendered, or @angular/build no longer calls the wrapped function.\n let callsBefore = getPrerenderCalls();\n for await (const result of buildApplication(applicationOptions, context)) {\n const calls = getPrerenderCalls();\n if (result.success && calls === callsBefore) {\n context.logger.error(\n `❌ 'prerenderFormat: \"file\"' had no effect: no pages were prerendered through @angular-schule/prerender-format. ` +\n `Check that prerendering is enabled ('outputMode: \"static\"' with server routes, or 'prerender'). ` +\n `If it is, this version of @angular/build is not supported.`\n );\n yield { ...result, success: false };\n } else {\n yield result;\n }\n callsBefore = calls;\n }\n}\n\nexport default createBuilder<Schema>(executeBuild);\n"]}
1
+ {"version":3,"file":"builder.js","sourceRoot":"","sources":["../../application/builder.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,oCA8CC;AAjFD,yDAAyF;AACzF,0CAA6E;AAC7E,2CAA6B;AAE7B,+CAAuF;AACvF,iDAA6C;AAc7C,SAAS,aAAa,CAAC,OAAkC;IACvD,MAAM,KAAK,GAAG,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAgB,CAAC;IACvC,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAQ,KAA8B,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACrG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAE,KAA4B,CAAC,MAAM,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAQM,KAAK,SAAS,CAAC,CAAC,YAAY,CACjC,OAAe,EACf,OAAuB;IAEvB,MAAM,EAAE,eAAe,GAAG,WAAW,EAAE,GAAG,kBAAkB,EAAE,GAAG,OAAO,CAAC;IAEzE,IAAI,eAAe,KAAK,MAAM,EAAE,CAAC;QAC/B,KAAK,CAAC,CAAC,IAAA,wBAAgB,EAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QAErD,OAAO;IACT,CAAC;IAED,IAAI,IAAA,0BAAW,EAAC,kBAAkB,CAAC,EAAE,CAAC;QACpC,OAAO,CAAC,MAAM,CAAC,IAAI,CACjB,gGAAgG;YAC9F,wHAAwH,CAC3H,CAAC;QACF,KAAK,CAAC,CAAC,IAAA,wBAAgB,EAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QAErD,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,+BAAiB,EAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC,CAAC,CAAC;IAChG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,iEAAiE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QACvG,KAAK,CAAC,CAAC,IAAA,wBAAgB,EAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QAErD,OAAO;IACT,CAAC;IACD,IAAA,8BAAgB,EAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC,CAAC;IAIpD,IAAI,WAAW,GAAG,IAAA,+BAAiB,GAAE,CAAC;IACtC,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAA,wBAAgB,EAAC,kBAAkB,EAAE,OAAO,CAAC,EAAE,CAAC;QACzE,MAAM,KAAK,GAAG,IAAA,+BAAiB,GAAE,CAAC;QAClC,IAAI,MAAM,CAAC,OAAO,IAAI,KAAK,KAAK,WAAW,EAAE,CAAC;YAC5C,OAAO,CAAC,MAAM,CAAC,IAAI,CACjB,gIAAgI;gBAC9H,wGAAwG;gBACxG,4DAA4D,CAC/D,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,CAAC;QACb,WAAW,GAAG,KAAK,CAAC;IACtB,CAAC;AACH,CAAC;AAED,kBAAe,IAAA,yBAAa,EAAS,YAAY,CAAC,CAAC","sourcesContent":["import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';\nimport { ApplicationBuilderOptions, buildApplication } from '@angular/build';\nimport * as path from 'path';\n\nimport { getPrerenderCalls, installFileFormat, setReservedFiles } from './file-format';\nimport { shipsServer } from './ships-server';\n\nexport type PrerenderFormat = 'directory' | 'file';\n\nexport interface Schema extends ApplicationBuilderOptions {\n /**\n * File layout of prerendered routes.\n * - `directory` (default): `foo/index.html`\n * - `file`: `foo.html`, the start page stays `index.html`\n */\n prerenderFormat?: PrerenderFormat;\n}\n\n/** Top-level file names of the build that a prerendered route must not take over. */\nfunction reservedFiles(options: ApplicationBuilderOptions): string[] {\n const files = ['index.csr.html', 'index.server.html'];\n const index = options.index as unknown;\n if (index && typeof index === 'object' && typeof (index as { output?: unknown }).output === 'string') {\n files.push(path.posix.basename((index as { output: string }).output));\n }\n\n return files;\n}\n\n/**\n * Runs `@angular/build:application` and, with `prerenderFormat: \"file\"`,\n * writes prerendered routes as `foo.html` instead of `foo/index.html`.\n * Whenever the \"file\" format does not apply, the build behaves like `@angular/build:application`\n * and logs a warning. Exported separately for testing purposes.\n */\nexport async function* executeBuild(\n options: Schema,\n context: BuilderContext\n): AsyncIterable<BuilderOutput> {\n const { prerenderFormat = 'directory', ...applicationOptions } = options;\n\n if (prerenderFormat !== 'file') {\n yield* buildApplication(applicationOptions, context);\n\n return;\n }\n\n if (shipsServer(applicationOptions)) {\n context.logger.warn(\n `The \"prerenderFormat\" option set to \"file\" is not considered when the build produces a server ` +\n `(\"outputMode\" set to \"server\", or \"ssr\" without \"outputMode\"). Prerendered routes are written to '<route>/index.html'.`\n );\n yield* buildApplication(applicationOptions, context);\n\n return;\n }\n\n const install = installFileFormat(path.dirname(require.resolve('@angular/build/package.json')));\n if (!install.installed) {\n context.logger.warn(`The \"prerenderFormat\" option set to \"file\" is not considered: ${install.reason}`);\n yield* buildApplication(applicationOptions, context);\n\n return;\n }\n setReservedFiles(reservedFiles(applicationOptions));\n\n // A successful build whose pages did not pass through the wrapper kept the directory layout:\n // nothing was prerendered, or this version of @angular/build no longer calls the wrapped function.\n let callsBefore = getPrerenderCalls();\n for await (const result of buildApplication(applicationOptions, context)) {\n const calls = getPrerenderCalls();\n if (result.success && calls === callsBefore) {\n context.logger.warn(\n `The \"prerenderFormat\" option set to \"file\" had no effect: no pages were prerendered through @angular-schule/prerender-format. ` +\n `Check that prerendering is enabled (\"outputMode\" set to \"static\" with server routes, or \"prerender\"). ` +\n `If it is, this version of @angular/build is not supported.`\n );\n }\n yield result;\n callsBefore = calls;\n }\n}\n\nexport default createBuilder<Schema>(executeBuild);\n"]}
@@ -5,16 +5,23 @@ export interface PrerenderedFile {
5
5
  export type PrerenderOutput = Record<string, PrerenderedFile>;
6
6
  export interface PrerenderResult {
7
7
  output: PrerenderOutput;
8
- errors?: string[];
8
+ warnings?: string[];
9
9
  [key: string]: unknown;
10
10
  }
11
11
  export type PrerenderPages = (...args: unknown[]) => Promise<PrerenderResult>;
12
12
  export interface FileOutputResult {
13
13
  output: PrerenderOutput;
14
- errors: string[];
14
+ warnings: string[];
15
15
  }
16
16
  export declare function getPrerenderCalls(): number;
17
+ export declare function setReservedFiles(files: string[]): void;
17
18
  export declare function toFilePath(outPath: string): string;
18
- export declare function toFileOutput(output: PrerenderOutput): FileOutputResult;
19
+ export declare function toFileOutput(output: PrerenderOutput, reserved?: string[]): FileOutputResult;
19
20
  export declare function wrapPrerenderPages(prerenderPages: PrerenderPages): PrerenderPages;
20
- export declare function installFileFormat(angularBuildRoot: string): void;
21
+ export type InstallResult = {
22
+ installed: true;
23
+ } | {
24
+ installed: false;
25
+ reason: string;
26
+ };
27
+ export declare function installFileFormat(angularBuildRoot: string): InstallResult;
@@ -34,19 +34,25 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getPrerenderCalls = getPrerenderCalls;
37
+ exports.setReservedFiles = setReservedFiles;
37
38
  exports.toFilePath = toFilePath;
38
39
  exports.toFileOutput = toFileOutput;
39
40
  exports.wrapPrerenderPages = wrapPrerenderPages;
40
41
  exports.installFileFormat = installFileFormat;
42
+ const fs = __importStar(require("fs"));
41
43
  const path = __importStar(require("path"));
42
44
  const INDEX_FILE = 'index.html';
43
45
  const INDEX_SUFFIX = '/' + INDEX_FILE;
44
46
  const PATCHED = Symbol.for('@angular-schule/prerender-format:patched');
45
47
  const PRERENDER_MODULE = 'src/utils/server-rendering/prerender.js';
46
48
  let prerenderCalls = 0;
49
+ let reservedFiles = [];
47
50
  function getPrerenderCalls() {
48
51
  return prerenderCalls;
49
52
  }
53
+ function setReservedFiles(files) {
54
+ reservedFiles = files.map(file => file.toLowerCase());
55
+ }
50
56
  function toFilePath(outPath) {
51
57
  if (!outPath.endsWith(INDEX_SUFFIX)) {
52
58
  return outPath;
@@ -59,29 +65,37 @@ function routeOf(outPath) {
59
65
  }
60
66
  return '/' + (outPath.endsWith(INDEX_SUFFIX) ? outPath.slice(0, -INDEX_SUFFIX.length) : outPath);
61
67
  }
62
- function toFileOutput(output) {
68
+ function toFileOutput(output, reserved = reservedFiles) {
63
69
  const renamed = {};
64
- const errors = [];
65
- const routeByFilePath = new Map();
70
+ const warnings = [];
71
+ const usedFiles = new Map();
72
+ const reservedLower = reserved.map(file => file.toLowerCase());
66
73
  for (const [outPath, file] of Object.entries(output)) {
67
74
  const route = routeOf(outPath);
68
75
  const filePath = toFilePath(outPath);
76
+ const lowerFilePath = filePath.toLowerCase();
69
77
  const lowerOutPath = outPath.toLowerCase();
70
- if (lowerOutPath.endsWith('/index' + INDEX_SUFFIX) || lowerOutPath === 'index' + INDEX_SUFFIX) {
71
- errors.push(`Route '${route}' cannot be prerendered with 'prerenderFormat: "file"': ` +
72
- `its file '${filePath}' would be served as '${route.slice(0, -'index'.length)}', not as '${route}'. ` +
73
- `Rename the route or use 'prerenderFormat: "directory"'.`);
74
- continue;
78
+ let reason;
79
+ if (filePath !== outPath) {
80
+ if (lowerOutPath.endsWith('/index' + INDEX_SUFFIX) || lowerOutPath === 'index' + INDEX_SUFFIX) {
81
+ reason = `'${filePath}' would be served for '${route.slice(0, -'index'.length)}'`;
82
+ }
83
+ else if (reservedLower.includes(lowerFilePath)) {
84
+ reason = `'${filePath}' is used by the build itself`;
85
+ }
86
+ else if (usedFiles.has(lowerFilePath)) {
87
+ reason = `'${filePath}' is already used by route '${usedFiles.get(lowerFilePath)}'`;
88
+ }
75
89
  }
76
- const existingRoute = routeByFilePath.get(filePath);
77
- if (existingRoute !== undefined) {
78
- errors.push(`Routes '${existingRoute}' and '${route}' both map to the file '${filePath}' with 'prerenderFormat: "file"'.`);
90
+ if (reason) {
91
+ warnings.push(`Route '${route}' is written to '${outPath}' instead, because ${reason}.`);
92
+ renamed[outPath] = file;
79
93
  continue;
80
94
  }
81
- routeByFilePath.set(filePath, route);
95
+ usedFiles.set(lowerFilePath, route);
82
96
  renamed[filePath] = file;
83
97
  }
84
- return { output: renamed, errors };
98
+ return { output: renamed, warnings };
85
99
  }
86
100
  function wrapPrerenderPages(prerenderPages) {
87
101
  const wrapped = async function (...args) {
@@ -89,13 +103,12 @@ function wrapPrerenderPages(prerenderPages) {
89
103
  if (!result ||
90
104
  typeof result.output !== 'object' ||
91
105
  result.output === null ||
92
- !Array.isArray(result.errors)) {
93
- throw new Error('@angular-schule/prerender-format: prerenderPages() returned an unknown result. ' +
94
- 'This version of @angular/build is not supported.');
106
+ !Array.isArray(result.warnings)) {
107
+ return result;
95
108
  }
96
109
  prerenderCalls++;
97
- const { output, errors } = toFileOutput(result.output);
98
- return { ...result, output, errors: [...result.errors, ...errors] };
110
+ const { output, warnings } = toFileOutput(result.output);
111
+ return { ...result, output, warnings: [...result.warnings, ...warnings] };
99
112
  };
100
113
  Object.defineProperty(wrapped, PATCHED, { value: true });
101
114
  return wrapped;
@@ -105,21 +118,21 @@ function isPatched(fn) {
105
118
  }
106
119
  function installFileFormat(angularBuildRoot) {
107
120
  const modulePath = path.join(angularBuildRoot, PRERENDER_MODULE);
108
- let prerenderModule;
109
- try {
110
- prerenderModule = require(modulePath);
111
- }
112
- catch (error) {
113
- throw new Error(`@angular-schule/prerender-format: cannot load '${modulePath}'. This version of @angular/build is not supported.`, { cause: error });
121
+ if (!fs.existsSync(modulePath)) {
122
+ return { installed: false, reason: `'${modulePath}' does not exist. This version of @angular/build is not supported.` };
114
123
  }
124
+ const prerenderModule = require(modulePath);
115
125
  const descriptor = Object.getOwnPropertyDescriptor(prerenderModule, 'prerenderPages');
116
126
  const prerenderPages = prerenderModule.prerenderPages;
117
127
  if (typeof prerenderPages !== 'function' || !descriptor?.writable) {
118
- throw new Error(`@angular-schule/prerender-format: '${modulePath}' exports no replaceable prerenderPages(). ` +
119
- 'This version of @angular/build is not supported.');
128
+ return {
129
+ installed: false,
130
+ reason: `'${modulePath}' exports no replaceable prerenderPages(). This version of @angular/build is not supported.`
131
+ };
120
132
  }
121
133
  if (!isPatched(prerenderPages)) {
122
134
  prerenderModule.prerenderPages = wrapPrerenderPages(prerenderPages);
123
135
  }
136
+ return { installed: true };
124
137
  }
125
138
  //# sourceMappingURL=file-format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-format.js","sourceRoot":"","sources":["../../application/file-format.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\n/** A prerendered page as returned by `prerenderPages()` of `@angular/build`. */\nexport interface PrerenderedFile {\n content: string;\n appShellRoute: boolean;\n}\n\nexport type PrerenderOutput = Record<string, PrerenderedFile>;\n\nexport interface PrerenderResult {\n output: PrerenderOutput;\n errors?: string[];\n [key: string]: unknown;\n}\n\nexport type PrerenderPages = (...args: unknown[]) => Promise<PrerenderResult>;\n\nexport interface FileOutputResult {\n output: PrerenderOutput;\n errors: string[];\n}\n\nconst INDEX_FILE = 'index.html';\nconst INDEX_SUFFIX = '/' + INDEX_FILE;\nconst PATCHED = Symbol.for('@angular-schule/prerender-format:patched');\nconst PRERENDER_MODULE = 'src/utils/server-rendering/prerender.js';\n\nlet prerenderCalls = 0;\n\n/** Number of `prerenderPages()` calls that went through the wrapper in this process. */\nexport function getPrerenderCalls(): number {\n return prerenderCalls;\n}\n\n/**\n * Maps a prerender output path from `foo/index.html` to `foo.html`.\n * The root `index.html` of a build (start page, or locale start page with its base href) stays as is.\n */\nexport function toFilePath(outPath: string): string {\n if (!outPath.endsWith(INDEX_SUFFIX)) {\n return outPath;\n }\n\n return outPath.slice(0, -INDEX_SUFFIX.length) + '.html';\n}\n\n/** Route of an output path, for error messages: `blog/index/index.html` → `/blog/index`. */\nfunction routeOf(outPath: string): string {\n if (outPath === INDEX_FILE) {\n return '/';\n }\n\n return '/' + (outPath.endsWith(INDEX_SUFFIX) ? outPath.slice(0, -INDEX_SUFFIX.length) : outPath);\n}\n\n/**\n * Renames all keys of a prerender `output` record.\n * Routes whose last segment is `index` are reported as errors: as `…/index.html` they would be\n * served under the parent path, and `/index` would overwrite the start page.\n */\nexport function toFileOutput(output: PrerenderOutput): FileOutputResult {\n const renamed: PrerenderOutput = {};\n const errors: string[] = [];\n const routeByFilePath = new Map<string, string>();\n\n for (const [outPath, file] of Object.entries(output)) {\n const route = routeOf(outPath);\n const filePath = toFilePath(outPath);\n\n // Case-insensitive: on macOS and Windows, 'Index.html' is the same file as 'index.html'.\n const lowerOutPath = outPath.toLowerCase();\n if (lowerOutPath.endsWith('/index' + INDEX_SUFFIX) || lowerOutPath === 'index' + INDEX_SUFFIX) {\n errors.push(\n `Route '${route}' cannot be prerendered with 'prerenderFormat: \"file\"': ` +\n `its file '${filePath}' would be served as '${route.slice(0, -'index'.length)}', not as '${route}'. ` +\n `Rename the route or use 'prerenderFormat: \"directory\"'.`\n );\n continue;\n }\n\n const existingRoute = routeByFilePath.get(filePath);\n if (existingRoute !== undefined) {\n errors.push(\n `Routes '${existingRoute}' and '${route}' both map to the file '${filePath}' with 'prerenderFormat: \"file\"'.`\n );\n continue;\n }\n\n routeByFilePath.set(filePath, route);\n renamed[filePath] = file;\n }\n\n return { output: renamed, errors };\n}\n\n/**\n * Wraps `prerenderPages` so that its `output` record uses the \"file\" format.\n * Problems are returned as build errors of `prerenderPages`, so Angular reports them like any other build error.\n */\nexport function wrapPrerenderPages(prerenderPages: PrerenderPages): PrerenderPages {\n const wrapped = async function (this: unknown, ...args: unknown[]) {\n const result = await prerenderPages.apply(this, args);\n if (\n !result ||\n typeof result.output !== 'object' ||\n result.output === null ||\n !Array.isArray(result.errors)\n ) {\n throw new Error(\n '@angular-schule/prerender-format: prerenderPages() returned an unknown result. ' +\n 'This version of @angular/build is not supported.'\n );\n }\n prerenderCalls++;\n\n const { output, errors } = toFileOutput(result.output);\n\n return { ...result, output, errors: [...result.errors, ...errors] };\n };\n Object.defineProperty(wrapped, PATCHED, { value: true });\n\n return wrapped;\n}\n\nfunction isPatched(fn: PrerenderPages): boolean {\n return (fn as unknown as Record<symbol, unknown>)[PATCHED] === true;\n}\n\n/**\n * Replaces `prerenderPages` of the given `@angular/build` installation.\n * `execute-post-bundle.js` reads the function from the module's exports object on every call,\n * so the replacement takes effect for regular and localized builds alike.\n */\nexport function installFileFormat(angularBuildRoot: string): void {\n const modulePath = path.join(angularBuildRoot, PRERENDER_MODULE);\n let prerenderModule: { prerenderPages?: PrerenderPages };\n try {\n prerenderModule = require(modulePath);\n } catch (error) {\n throw new Error(\n `@angular-schule/prerender-format: cannot load '${modulePath}'. This version of @angular/build is not supported.`,\n { cause: error }\n );\n }\n\n const descriptor = Object.getOwnPropertyDescriptor(prerenderModule, 'prerenderPages');\n const prerenderPages = prerenderModule.prerenderPages;\n if (typeof prerenderPages !== 'function' || !descriptor?.writable) {\n throw new Error(\n `@angular-schule/prerender-format: '${modulePath}' exports no replaceable prerenderPages(). ` +\n 'This version of @angular/build is not supported.'\n );\n }\n\n if (!isPatched(prerenderPages)) {\n prerenderModule.prerenderPages = wrapPrerenderPages(prerenderPages);\n }\n}\n"]}
1
+ {"version":3,"file":"file-format.js","sourceRoot":"","sources":["../../application/file-format.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,8CAEC;AAGD,4CAEC;AAMD,gCAMC;AAmBD,oCAkCC;AAOD,gDAoBC;AAcD,8CAqBC;AAvKD,uCAAyB;AACzB,2CAA6B;AAuB7B,MAAM,UAAU,GAAG,YAAY,CAAC;AAChC,MAAM,YAAY,GAAG,GAAG,GAAG,UAAU,CAAC;AACtC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;AACvE,MAAM,gBAAgB,GAAG,yCAAyC,CAAC;AAEnE,IAAI,cAAc,GAAG,CAAC,CAAC;AACvB,IAAI,aAAa,GAAa,EAAE,CAAC;AAGjC,SAAgB,iBAAiB;IAC/B,OAAO,cAAc,CAAC;AACxB,CAAC;AAGD,SAAgB,gBAAgB,CAAC,KAAe;IAC9C,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AACxD,CAAC;AAMD,SAAgB,UAAU,CAAC,OAAe;IACxC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACpC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;AAC1D,CAAC;AAGD,SAAS,OAAO,CAAC,OAAe;IAC9B,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;QAC3B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AACnG,CAAC;AAUD,SAAgB,YAAY,CAAC,MAAuB,EAAE,WAAqB,aAAa;IACtF,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAE/D,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,aAAa,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAE3C,IAAI,MAA0B,CAAC;QAC/B,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACzB,IAAI,YAAY,CAAC,QAAQ,CAAC,QAAQ,GAAG,YAAY,CAAC,IAAI,YAAY,KAAK,OAAO,GAAG,YAAY,EAAE,CAAC;gBAC9F,MAAM,GAAG,IAAI,QAAQ,0BAA0B,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;YACpF,CAAC;iBAAM,IAAI,aAAa,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;gBACjD,MAAM,GAAG,IAAI,QAAQ,+BAA+B,CAAC;YACvD,CAAC;iBAAM,IAAI,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;gBACxC,MAAM,GAAG,IAAI,QAAQ,+BAA+B,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC;YACtF,CAAC;QACH,CAAC;QAED,IAAI,MAAM,EAAE,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,UAAU,KAAK,oBAAoB,OAAO,sBAAsB,MAAM,GAAG,CAAC,CAAC;YACzF,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;YACxB,SAAS;QACX,CAAC;QAED,SAAS,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;QACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IAC3B,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AACvC,CAAC;AAOD,SAAgB,kBAAkB,CAAC,cAA8B;IAC/D,MAAM,OAAO,GAAG,KAAK,WAA0B,GAAG,IAAe;QAC/D,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACtD,IACE,CAAC,MAAM;YACP,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;YACjC,MAAM,CAAC,MAAM,KAAK,IAAI;YACtB,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC/B,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QACD,cAAc,EAAE,CAAC;QAEjB,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAEzD,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAI,MAAM,CAAC,QAAqB,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC;IAC1F,CAAC,CAAC;IACF,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,SAAS,CAAC,EAAkB;IACnC,OAAQ,EAAyC,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;AACtE,CAAC;AAUD,SAAgB,iBAAiB,CAAC,gBAAwB;IACxD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IACjE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,UAAU,oEAAoE,EAAE,CAAC;IAC1H,CAAC;IAED,MAAM,eAAe,GAAwC,OAAO,CAAC,UAAU,CAAC,CAAC;IACjF,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,CAAC,eAAe,EAAE,gBAAgB,CAAC,CAAC;IACtF,MAAM,cAAc,GAAG,eAAe,CAAC,cAAc,CAAC;IACtD,IAAI,OAAO,cAAc,KAAK,UAAU,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,CAAC;QAClE,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,IAAI,UAAU,6FAA6F;SACpH,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,EAAE,CAAC;QAC/B,eAAe,CAAC,cAAc,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n/** A prerendered page as returned by `prerenderPages()` of `@angular/build`. */\nexport interface PrerenderedFile {\n content: string;\n appShellRoute: boolean;\n}\n\nexport type PrerenderOutput = Record<string, PrerenderedFile>;\n\nexport interface PrerenderResult {\n output: PrerenderOutput;\n warnings?: string[];\n [key: string]: unknown;\n}\n\nexport type PrerenderPages = (...args: unknown[]) => Promise<PrerenderResult>;\n\nexport interface FileOutputResult {\n output: PrerenderOutput;\n warnings: string[];\n}\n\nconst INDEX_FILE = 'index.html';\nconst INDEX_SUFFIX = '/' + INDEX_FILE;\nconst PATCHED = Symbol.for('@angular-schule/prerender-format:patched');\nconst PRERENDER_MODULE = 'src/utils/server-rendering/prerender.js';\n\nlet prerenderCalls = 0;\nlet reservedFiles: string[] = [];\n\n/** Number of `prerenderPages()` calls that went through the wrapper in this process. */\nexport function getPrerenderCalls(): number {\n return prerenderCalls;\n}\n\n/** Top-level file names that a route must not take over, such as the CSR index `index.csr.html`. */\nexport function setReservedFiles(files: string[]): void {\n reservedFiles = files.map(file => file.toLowerCase());\n}\n\n/**\n * Maps a prerender output path from `foo/index.html` to `foo.html`.\n * The root `index.html` of a build (start page, or locale start page with its base href) stays as is.\n */\nexport function toFilePath(outPath: string): string {\n if (!outPath.endsWith(INDEX_SUFFIX)) {\n return outPath;\n }\n\n return outPath.slice(0, -INDEX_SUFFIX.length) + '.html';\n}\n\n/** Route of an output path, for messages: `blog/index/index.html` → `/blog/index`. */\nfunction routeOf(outPath: string): string {\n if (outPath === INDEX_FILE) {\n return '/';\n }\n\n return '/' + (outPath.endsWith(INDEX_SUFFIX) ? outPath.slice(0, -INDEX_SUFFIX.length) : outPath);\n}\n\n/**\n * Renames all keys of a prerender `output` record from `foo/index.html` to `foo.html`.\n * A route keeps `foo/index.html` (with a warning) when `foo.html` is not safe:\n * - its last segment is `index` in any letter case: `index.html` is served for the parent path,\n * and on macOS and Windows `Index.html` is the same file as `index.html`,\n * - the file name is reserved, such as the CSR index `index.csr.html`,\n * - another route already uses the file name (compared case-insensitively).\n */\nexport function toFileOutput(output: PrerenderOutput, reserved: string[] = reservedFiles): FileOutputResult {\n const renamed: PrerenderOutput = {};\n const warnings: string[] = [];\n const usedFiles = new Map<string, string>();\n const reservedLower = reserved.map(file => file.toLowerCase());\n\n for (const [outPath, file] of Object.entries(output)) {\n const route = routeOf(outPath);\n const filePath = toFilePath(outPath);\n const lowerFilePath = filePath.toLowerCase();\n const lowerOutPath = outPath.toLowerCase();\n\n let reason: string | undefined;\n if (filePath !== outPath) {\n if (lowerOutPath.endsWith('/index' + INDEX_SUFFIX) || lowerOutPath === 'index' + INDEX_SUFFIX) {\n reason = `'${filePath}' would be served for '${route.slice(0, -'index'.length)}'`;\n } else if (reservedLower.includes(lowerFilePath)) {\n reason = `'${filePath}' is used by the build itself`;\n } else if (usedFiles.has(lowerFilePath)) {\n reason = `'${filePath}' is already used by route '${usedFiles.get(lowerFilePath)}'`;\n }\n }\n\n if (reason) {\n warnings.push(`Route '${route}' is written to '${outPath}' instead, because ${reason}.`);\n renamed[outPath] = file;\n continue;\n }\n\n usedFiles.set(lowerFilePath, route);\n renamed[filePath] = file;\n }\n\n return { output: renamed, warnings };\n}\n\n/**\n * Wraps `prerenderPages` so that its `output` record uses the \"file\" format.\n * Routes that keep the directory format are reported as build warnings of `prerenderPages`.\n * An unknown result is passed through unchanged.\n */\nexport function wrapPrerenderPages(prerenderPages: PrerenderPages): PrerenderPages {\n const wrapped = async function (this: unknown, ...args: unknown[]) {\n const result = await prerenderPages.apply(this, args);\n if (\n !result ||\n typeof result.output !== 'object' ||\n result.output === null ||\n !Array.isArray(result.warnings)\n ) {\n return result;\n }\n prerenderCalls++;\n\n const { output, warnings } = toFileOutput(result.output);\n\n return { ...result, output, warnings: [...(result.warnings as string[]), ...warnings] };\n };\n Object.defineProperty(wrapped, PATCHED, { value: true });\n\n return wrapped;\n}\n\nfunction isPatched(fn: PrerenderPages): boolean {\n return (fn as unknown as Record<symbol, unknown>)[PATCHED] === true;\n}\n\nexport type InstallResult = { installed: true } | { installed: false; reason: string };\n\n/**\n * Replaces `prerenderPages` of the given `@angular/build` installation.\n * `execute-post-bundle.js` reads the function from the module's exports object on every call,\n * so the replacement takes effect for regular and localized builds alike.\n * Returns the reason instead of replacing anything when the internals are not as expected.\n */\nexport function installFileFormat(angularBuildRoot: string): InstallResult {\n const modulePath = path.join(angularBuildRoot, PRERENDER_MODULE);\n if (!fs.existsSync(modulePath)) {\n return { installed: false, reason: `'${modulePath}' does not exist. This version of @angular/build is not supported.` };\n }\n\n const prerenderModule: { prerenderPages?: PrerenderPages } = require(modulePath);\n const descriptor = Object.getOwnPropertyDescriptor(prerenderModule, 'prerenderPages');\n const prerenderPages = prerenderModule.prerenderPages;\n if (typeof prerenderPages !== 'function' || !descriptor?.writable) {\n return {\n installed: false,\n reason: `'${modulePath}' exports no replaceable prerenderPages(). This version of @angular/build is not supported.`\n };\n }\n\n if (!isPatched(prerenderPages)) {\n prerenderModule.prerenderPages = wrapPrerenderPages(prerenderPages);\n }\n\n return { installed: true };\n}\n"]}
package/ng-add.js CHANGED
@@ -39,17 +39,18 @@ const ngAdd = (options) => async (tree, context) => {
39
39
  ]
40
40
  .filter(([, options]) => (0, ships_server_1.shipsServer)(options))
41
41
  .map(([name]) => name);
42
- if (configurationsWithServer.length) {
43
- throw new schematics_1.SchematicsException(`The build target of "${options.project}" ships an Angular SSR server (${configurationsWithServer.join(', ')}). ` +
44
- `prerenderFormat "file" is not supported when the build produces a server, because the SSR server looks up prerendered pages as 'index.html'. ` +
45
- `Use a static build ("outputMode": "static", or prerendering without "ssr") and run ng add again.`);
46
- }
47
42
  buildTarget.builder = exports.BUILDER_NAME;
48
43
  buildTarget.options = { ...buildTarget.options, prerenderFormat: 'file' };
49
44
  await core_1.workspaces.writeWorkspace(workspace, host);
50
45
  context.logger.info('');
51
46
  context.logger.info('🚀 @angular-schule/prerender-format is ready!');
52
47
  context.logger.info('');
48
+ if (configurationsWithServer.length) {
49
+ context.logger.warn(`⚠️ The build target of "${options.project}" produces a server (${configurationsWithServer.join(', ')}). ` +
50
+ `There, prerenderFormat "file" is not considered and routes stay '<route>/index.html'. ` +
51
+ `Use "outputMode": "static" to get '<route>.html'.`);
52
+ context.logger.info('');
53
+ }
53
54
  context.logger.info('Next steps:');
54
55
  context.logger.info(' 1. Make sure your host serves foo.html under /foo without a redirect.');
55
56
  context.logger.info(' 2. Build via: ng build');
package/ng-add.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ng-add.js","sourceRoot":"","sources":["../ng-add.ts"],"names":[],"mappings":";;;AAAA,+CAAkD;AAClD,2DAAyF;AAEzF,6DAAyD;AACzD,mCAAqC;AAMxB,QAAA,YAAY,GAAG,8CAA8C,CAAC;AAC3E,MAAM,oBAAoB,GAAG,4BAA4B,CAAC;AAEnD,MAAM,KAAK,GAAG,CAAC,OAAqB,EAAE,EAAE,CAAC,KAAK,EAAE,IAAU,EAAE,OAAyB,EAAE,EAAE;IAC9F,MAAM,IAAI,GAAG,IAAA,kBAAU,EAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,iBAAU,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAEhE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAElC,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,gCAAmB,CAC3B,8GAA8G,CAC/G,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,gCAAmB,CAAC,gEAAgE,CAAC,CAAC;IAClG,CAAC;IAED,IAAI,OAAO,CAAC,UAAU,CAAC,WAAW,KAAK,aAAa,EAAE,CAAC;QACrD,MAAM,IAAI,gCAAmB,CAC3B,oGAAoG,CACrG,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,gCAAmB,CAC3B,qDAAqD,OAAO,CAAC,OAAO,oBAAoB,CACzF,CAAC;IACJ,CAAC;IAED,IAAI,WAAW,CAAC,OAAO,KAAK,oBAAoB,IAAI,WAAW,CAAC,OAAO,KAAK,oBAAY,EAAE,CAAC;QACzF,MAAM,IAAI,gCAAmB,CAC3B,wBAAwB,OAAO,CAAC,OAAO,WAAW,WAAW,CAAC,OAAO,KAAK;YACxE,8CAA8C,oBAAoB,SAAS,CAC9E,CAAC;IACJ,CAAC;IAED,MAAM,wBAAwB,GAAG;QAC/B,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,IAAI,EAAE,CAAU;QAC/C,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CACrD,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,EAAE,EAAE,CACxB,CAAC,kBAAkB,IAAI,GAAG,EAAE,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,GAAG,aAAa,EAAE,CAAU,CACrF;KACF;SACE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,IAAA,0BAAW,EAAC,OAAO,CAAC,CAAC;SAC7C,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;IAEzB,IAAI,wBAAwB,CAAC,MAAM,EAAE,CAAC;QACpC,MAAM,IAAI,gCAAmB,CAC3B,wBAAwB,OAAO,CAAC,OAAO,kCAAkC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YAC/G,+IAA+I;YAC/I,kGAAkG,CACrG,CAAC;IACJ,CAAC;IAED,WAAW,CAAC,OAAO,GAAG,oBAAY,CAAC;IACnC,WAAW,CAAC,OAAO,GAAG,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;IAE1E,MAAM,iBAAU,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAEjD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;IACrE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACnC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,yEAAyE,CAAC,CAAC;IAC/F,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAChD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAE7C,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAxEW,QAAA,KAAK,SAwEhB","sourcesContent":["import { workspaces } from '@angular-devkit/core';\nimport { SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics';\n\nimport { shipsServer } from './application/ships-server';\nimport { createHost } from './utils';\n\ninterface NgAddOptions {\n project: string;\n}\n\nexport const BUILDER_NAME = '@angular-schule/prerender-format:application';\nconst ANGULAR_BUILDER_NAME = '@angular/build:application';\n\nexport const ngAdd = (options: NgAddOptions) => async (tree: Tree, context: SchematicContext) => {\n const host = createHost(tree);\n const { workspace } = await workspaces.readWorkspace('/', host);\n\n if (!options.project) {\n if (workspace.projects.size === 1) {\n // If there is only one project, return that one.\n options.project = Array.from(workspace.projects.keys())[0];\n } else {\n throw new SchematicsException(\n 'There is more than one project in your workspace. Please select it manually by using the --project argument.'\n );\n }\n }\n\n const project = workspace.projects.get(options.project);\n if (!project) {\n throw new SchematicsException('The specified Angular project is not defined in this workspace');\n }\n\n if (project.extensions.projectType !== 'application') {\n throw new SchematicsException(\n `@angular-schule/prerender-format requires an Angular project type of \"application\" in angular.json`\n );\n }\n\n const buildTarget = project.targets.get('build');\n if (!buildTarget) {\n throw new SchematicsException(\n `Cannot find build target for the Angular project \"${options.project}\" in angular.json.`\n );\n }\n\n if (buildTarget.builder !== ANGULAR_BUILDER_NAME && buildTarget.builder !== BUILDER_NAME) {\n throw new SchematicsException(\n `The build target of \"${options.project}\" uses \"${buildTarget.builder}\". ` +\n `@angular-schule/prerender-format replaces \"${ANGULAR_BUILDER_NAME}\" only.`\n );\n }\n\n const configurationsWithServer = [\n ['options', buildTarget.options ?? {}] as const,\n ...Object.entries(buildTarget.configurations ?? {}).map(\n ([name, configuration]) =>\n [`configuration \"${name}\"`, { ...buildTarget.options, ...configuration }] as const\n )\n ]\n .filter(([, options]) => shipsServer(options))\n .map(([name]) => name);\n\n if (configurationsWithServer.length) {\n throw new SchematicsException(\n `The build target of \"${options.project}\" ships an Angular SSR server (${configurationsWithServer.join(', ')}). ` +\n `prerenderFormat \"file\" is not supported when the build produces a server, because the SSR server looks up prerendered pages as 'index.html'. ` +\n `Use a static build (\"outputMode\": \"static\", or prerendering without \"ssr\") and run ng add again.`\n );\n }\n\n buildTarget.builder = BUILDER_NAME;\n buildTarget.options = { ...buildTarget.options, prerenderFormat: 'file' };\n\n await workspaces.writeWorkspace(workspace, host);\n\n context.logger.info('');\n context.logger.info('🚀 @angular-schule/prerender-format is ready!');\n context.logger.info('');\n context.logger.info('Next steps:');\n context.logger.info(' 1. Make sure your host serves foo.html under /foo without a redirect.');\n context.logger.info(' 2. Build via: ng build');\n context.logger.info(' 3. Have a nice day!');\n\n return tree;\n};\n"]}
1
+ {"version":3,"file":"ng-add.js","sourceRoot":"","sources":["../ng-add.ts"],"names":[],"mappings":"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","sourcesContent":["import { workspaces } from '@angular-devkit/core';\nimport { SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics';\n\nimport { shipsServer } from './application/ships-server';\nimport { createHost } from './utils';\n\ninterface NgAddOptions {\n project: string;\n}\n\nexport const BUILDER_NAME = '@angular-schule/prerender-format:application';\nconst ANGULAR_BUILDER_NAME = '@angular/build:application';\n\nexport const ngAdd = (options: NgAddOptions) => async (tree: Tree, context: SchematicContext) => {\n const host = createHost(tree);\n const { workspace } = await workspaces.readWorkspace('/', host);\n\n if (!options.project) {\n if (workspace.projects.size === 1) {\n // If there is only one project, return that one.\n options.project = Array.from(workspace.projects.keys())[0];\n } else {\n throw new SchematicsException(\n 'There is more than one project in your workspace. Please select it manually by using the --project argument.'\n );\n }\n }\n\n const project = workspace.projects.get(options.project);\n if (!project) {\n throw new SchematicsException('The specified Angular project is not defined in this workspace');\n }\n\n if (project.extensions.projectType !== 'application') {\n throw new SchematicsException(\n `@angular-schule/prerender-format requires an Angular project type of \"application\" in angular.json`\n );\n }\n\n const buildTarget = project.targets.get('build');\n if (!buildTarget) {\n throw new SchematicsException(\n `Cannot find build target for the Angular project \"${options.project}\" in angular.json.`\n );\n }\n\n if (buildTarget.builder !== ANGULAR_BUILDER_NAME && buildTarget.builder !== BUILDER_NAME) {\n throw new SchematicsException(\n `The build target of \"${options.project}\" uses \"${buildTarget.builder}\". ` +\n `@angular-schule/prerender-format replaces \"${ANGULAR_BUILDER_NAME}\" only.`\n );\n }\n\n const configurationsWithServer = [\n ['options', buildTarget.options ?? {}] as const,\n ...Object.entries(buildTarget.configurations ?? {}).map(\n ([name, configuration]) =>\n [`configuration \"${name}\"`, { ...buildTarget.options, ...configuration }] as const\n )\n ]\n .filter(([, options]) => shipsServer(options))\n .map(([name]) => name);\n\n buildTarget.builder = BUILDER_NAME;\n buildTarget.options = { ...buildTarget.options, prerenderFormat: 'file' };\n\n await workspaces.writeWorkspace(workspace, host);\n\n context.logger.info('');\n context.logger.info('🚀 @angular-schule/prerender-format is ready!');\n context.logger.info('');\n if (configurationsWithServer.length) {\n context.logger.warn(\n `⚠️ The build target of \"${options.project}\" produces a server (${configurationsWithServer.join(', ')}). ` +\n `There, prerenderFormat \"file\" is not considered and routes stay '<route>/index.html'. ` +\n `Use \"outputMode\": \"static\" to get '<route>.html'.`\n );\n context.logger.info('');\n }\n context.logger.info('Next steps:');\n context.logger.info(' 1. Make sure your host serves foo.html under /foo without a redirect.');\n context.logger.info(' 2. Build via: ng build');\n context.logger.info(' 3. Have a nice day!');\n\n return tree;\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-schule/prerender-format",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Angular application builder that writes prerendered routes as foo.html instead of foo/index.html (no trailing slash redirects)",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",