diff --git a/aio/angular.json b/aio/angular.json
index 2afa8f3df7..513d7eef7f 100644
--- a/aio/angular.json
+++ b/aio/angular.json
@@ -32,6 +32,7 @@
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
+ "ngswConfigPath": "src/generated/ngsw-config.json",
"tsConfig": "tsconfig.app.json",
"webWorkerTsConfig": "tsconfig.worker.json",
"optimization": {
@@ -47,9 +48,16 @@
"namedChunks": true,
"assets": [
"src/assets",
- "src/generated",
"src/pwa-manifest.json",
- "src/google385281288605d160.html"
+ "src/google385281288605d160.html",
+ {
+ "input": "src/generated",
+ "output": "generated",
+ "glob": "**",
+ "ignore": [
+ "ngsw-config.json"
+ ]
+ }
],
"styles": [
"src/styles/main.scss",
diff --git a/aio/firebase.json b/aio/firebase.json
index c1e3e90b26..2e61ba4448 100644
--- a/aio/firebase.json
+++ b/aio/firebase.json
@@ -4,12 +4,6 @@
"public": "dist",
"cleanUrls": true,
"redirects": [
- //////////////////////////////////////////////////////////////////////////////////////////////
- // README:
- // Redirects must also be handled by the ServiceWorker. If you add a redirect rule here,
- // make sure it is compatible with the configuration in `ngsw-config.json`.
- //////////////////////////////////////////////////////////////////////////////////////////////
-
// A random bad indexed page that used `api/api`
{"type": 301, "source": "/api/api/:rest*", "destination": "/api/:rest*"},
diff --git a/aio/ngsw-config.json b/aio/ngsw-config.json
deleted file mode 100644
index 10555ab0ca..0000000000
--- a/aio/ngsw-config.json
+++ /dev/null
@@ -1,143 +0,0 @@
-{
- "index": "/index.html",
- "assetGroups": [
- {
- "name": "app-shell",
- "installMode": "prefetch",
- "updateMode": "prefetch",
- "resources": {
- "files": [
- "/index.html",
- "/pwa-manifest.json",
- "/assets/images/favicons/favicon.ico",
- "/assets/js/*.js",
- "/*.css",
- "/*.js",
- "!/*-es5*.js"
- ],
- "urls": [
- "https://fonts.googleapis.com/**",
- "https://fonts.gstatic.com/s/**"
- ]
- }
- },
- {
- "name": "assets-eager",
- "installMode": "prefetch",
- "updateMode": "prefetch",
- "resources": {
- "files": [
- "/assets/images/**",
- "/generated/images/marketing/**",
- "!/assets/images/favicons/**",
- "!/**/_unused/**"
- ]
- }
- },
- {
- "name": "assets-lazy",
- "installMode": "lazy",
- "updateMode": "prefetch",
- "resources": {
- "files": [
- "/assets/images/favicons/**",
- "/generated/js/custom-elements-es5-polyfills.js",
- "/*-es5*.js",
- "!/**/_unused/**"
- ]
- }
- },
- {
- "name": "docs-index",
- "installMode": "prefetch",
- "updateMode": "prefetch",
- "resources": {
- "files": [
- "/generated/*.json",
- "/generated/docs/*.json",
- "/generated/docs/api/api-list.json",
- "/generated/docs/app/search-data.json"
- ]
- }
- },
- {
- "name": "docs-lazy",
- "installMode": "lazy",
- "updateMode": "lazy",
- "resources": {
- "files": [
- "/generated/docs/**/*.json",
- "/generated/images/**",
- "!/**/_unused/**"
- ]
- }
- }
- ],
- "navigationUrls": [
- "/**",
- "!/**/*.*",
- "!/**/*__*",
- "!/**/*__*/**",
- "!/**/stackblitz/{0,1}",
- "!/**/AnimationStateDeclarationMetadata*",
- "!/**/CORE_DIRECTIVES*",
- "!/**/DirectiveMetadata-*",
- "!/**/HTTP_PROVIDERS*",
- "!/**/NgFor-*",
- "!/**/OptionalMetadata-*",
- "!/**/PLATFORM_PIPES*",
- "!/**/api/common/Control*",
- "!/**/api/common/ControlGroup*",
- "!/**/api/common/NgModel/{0,1}",
- "!/**/api/common/SelectControlValueAccessor-*",
- "!/**/api/common/index/MaxLengthValidator-*",
- "!/**/cookbook/ts-to-js*",
- "!/api/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
- "!/api/*/testing/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
- "!/api/*/testing/index/*",
- "!/api/animate/**",
- "!/api/api/**",
- "!/api/http/**",
- "!/api/http/{0,1}",
- "!/api/platform-browser/AnimationDriver/{0,1}",
- "!/api/testing/*-*",
- "!/api/upgrade/*/*-(class|decorator|directive|function|interface|let|pipe|type|type-alias|var)",
- "!/api/upgrade/*/index/*",
- "!/config/app-package-json/{0,1}",
- "!/config/solution-tsconfig/{0,1}",
- "!/config/tsconfig/{0,1}",
- "!/devtools/{0,1}",
- "!/docs/*/latest/**",
- "!/docs/*/latest/{0,1}",
- "!/docs/latest/**",
- "!/docs/styleguide*",
- "!/docs/styleguide/{0,1}",
- "!/getting-started/**",
- "!/getting-started/{0,1}",
- "!/guide/bazel/{0,1}",
- "!/guide/change-log/{0,1}",
- "!/guide/cli-quickstart/{0,1}",
- "!/guide/displaying-data/{0,1}",
- "!/guide/learning-angular*",
- "!/guide/metadata/{0,1}",
- "!/guide/ngmodule/{0,1}",
- "!/guide/quickstart/{0,1}",
- "!/guide/service-worker-comm/{0,1}",
- "!/guide/service-worker-configref/{0,1}",
- "!/guide/service-worker-getstart/{0,1}",
- "!/guide/setup-systemjs-anatomy/{0,1}",
- "!/guide/setup/{0,1}",
- "!/guide/updating-to-version-10/{0,1}",
- "!/guide/updating-to-version-11/{0,1}",
- "!/guide/webpack/{0,1}",
- "!/news*",
- "!/start/data/{0,1}",
- "!/start/deployment/{0,1}",
- "!/start/forms/{0,1}",
- "!/start/routing/{0,1}",
- "!/strict/{0,1}",
- "!/styleguide/{0,1}",
- "!/testing/**",
- "!/testing/{0,1}"
- ]
-}
diff --git a/aio/ngsw-config.template.json b/aio/ngsw-config.template.json
new file mode 100644
index 0000000000..f28aceefde
--- /dev/null
+++ b/aio/ngsw-config.template.json
@@ -0,0 +1,83 @@
+{
+ "index": "/index.html",
+ "assetGroups": [
+ {
+ "name": "app-shell",
+ "installMode": "prefetch",
+ "updateMode": "prefetch",
+ "resources": {
+ "files": [
+ "/index.html",
+ "/pwa-manifest.json",
+ "/assets/images/favicons/favicon.ico",
+ "/assets/js/*.js",
+ "/*.css",
+ "/*.js",
+ "!/*-es5*.js"
+ ],
+ "urls": [
+ "https://fonts.googleapis.com/**",
+ "https://fonts.gstatic.com/s/**"
+ ]
+ }
+ },
+ {
+ "name": "assets-eager",
+ "installMode": "prefetch",
+ "updateMode": "prefetch",
+ "resources": {
+ "files": [
+ "/assets/images/**",
+ "/generated/images/marketing/**",
+ "!/assets/images/favicons/**",
+ "!/**/_unused/**"
+ ]
+ }
+ },
+ {
+ "name": "assets-lazy",
+ "installMode": "lazy",
+ "updateMode": "prefetch",
+ "resources": {
+ "files": [
+ "/assets/images/favicons/**",
+ "/generated/js/custom-elements-es5-polyfills.js",
+ "/*-es5*.js",
+ "!/**/_unused/**"
+ ]
+ }
+ },
+ {
+ "name": "docs-index",
+ "installMode": "prefetch",
+ "updateMode": "prefetch",
+ "resources": {
+ "files": [
+ "/generated/*.json",
+ "/generated/docs/*.json",
+ "/generated/docs/api/api-list.json",
+ "/generated/docs/app/search-data.json"
+ ]
+ }
+ },
+ {
+ "name": "docs-lazy",
+ "installMode": "lazy",
+ "updateMode": "lazy",
+ "resources": {
+ "files": [
+ "/generated/docs/**/*.json",
+ "/generated/images/**",
+ "!/**/_unused/**"
+ ]
+ }
+ }
+ ],
+ "navigationUrls": [
+ "/**",
+ "!/**/*.*",
+ "!/**/*__*",
+ "!/**/*__*/**",
+ "!/**/stackblitz/{0,1}"
+ ]
+}
diff --git a/aio/package.json b/aio/package.json
index 3d3bcb2f97..448c547162 100644
--- a/aio/package.json
+++ b/aio/package.json
@@ -71,10 +71,11 @@
"~~audit-web-app": "node scripts/audit-web-app",
"~~check-env": "node scripts/check-environment",
"~~clean-generated": "node --eval \"require('shelljs').rm('-rf', 'src/generated')\"",
- "pre~~build": "yarn ~~build-ce-es5-polyfills",
+ "pre~~build": "yarn ~~build-ce-es5-polyfills && yarn ~~build-ngsw-config",
"~~build": "ng build --configuration=stable",
"post~~build": "yarn build-404-page",
"~~build-ce-es5-polyfills": "esbuild src/custom-elements-es5-polyfills.js --bundle --minify | swc --config=minify=true --filename=custom-elements-es5-polyfills.js --out-file=src/generated/js/custom-elements-es5-polyfills.js --no-swcrc",
+ "~~build-ngsw-config": "node scripts/build-ngsw-config",
"~~light-server": "light-server --bind=localhost --historyindex=/index.html --no-reload"
},
"//engines-comment": "Keep this in sync with /package.json and /aio/tools/examples/shared/package.json",
diff --git a/aio/scripts/build-ngsw-config.js b/aio/scripts/build-ngsw-config.js
new file mode 100644
index 0000000000..2cf16009f8
--- /dev/null
+++ b/aio/scripts/build-ngsw-config.js
@@ -0,0 +1,85 @@
+// Imports
+const {basename, dirname, resolve: resolvePath} = require('canonical-path');
+const {mkdirSync, readFileSync, writeFileSync} = require('fs');
+const {parse: parseJson} = require('json5');
+
+
+// Constants
+const FIREBASE_CONFIG_PATH = resolvePath(__dirname, '../firebase.json');
+const NGSW_CONFIG_TEMPLATE_PATH = resolvePath(__dirname, '../ngsw-config.template.json');
+const NGSW_CONFIG_OUTPUT_PATH = resolvePath(__dirname, '../src/generated/ngsw-config.json');
+
+// Run
+_main();
+
+// Helpers
+function _main() {
+ const firebaseConfig = readJson(FIREBASE_CONFIG_PATH);
+ const ngswConfig = readJson(NGSW_CONFIG_TEMPLATE_PATH);
+
+ const firebaseRedirects = firebaseConfig.hosting.redirects;
+
+ // Check that there are no regex-based redirects.
+ const regexBasedRedirects = firebaseRedirects.filter(({regex}) => regex !== undefined);
+ if (regexBasedRedirects.length > 0) {
+ throw new Error(
+ 'The following redirects use `regex`, which is currently not supported by ' +
+ `${basename(__filename)}:` +
+ regexBasedRedirects.map(x => `\n - ${JSON.stringify(x)}`).join(''));
+ }
+
+ // Check that there are no unsupported glob characters/patterns.
+ const redirectsWithUnsupportedGlobs = firebaseRedirects
+ .filter(({source}) => !/^(?:[/\w\-.*]|:\w+|@\([\w\-.|]+\))+$/.test(source));
+ if (redirectsWithUnsupportedGlobs.length > 0) {
+ throw new Error(
+ 'The following redirects use glob characters/patterns that are currently not supported by ' +
+ `${basename(__filename)}:` +
+ redirectsWithUnsupportedGlobs.map(x => `\n - ${JSON.stringify(x)}`).join(''));
+ }
+
+ // Compute additional ignore glob patterns to be added to `navigationUrls`.
+ const additionalNavigationUrls = firebaseRedirects
+ // Grab the redirect source glob.
+ .map(({source}) => source)
+ // Ignore redirects for files (since these are already ignored by the SW).
+ .filter(glob => /\/[^/.]*$/.test(glob))
+ // Convert each Firebase-specific glob to a SW-compatible glob.
+ .map(glob => `!${glob.replace(/:\w+/g, '*').replace(/@(\([\w\-.|]+\))/g, '$1')}`)
+ // Add optional trailing `/` for globs that don't end with `*`.
+ .map(glob => /\w$/.test(glob) ? `${glob}\/{0,1}` : glob)
+ // Remove more specific globs that are covered by more generic ones.
+ .filter((glob, _i, globs) => !isGlobRedundant(glob, globs))
+ // Sort the generated globs alphabetically.
+ .sort();
+
+ // Add the additional `navigationUrls` globs and save the config.
+ ngswConfig.navigationUrls.push(...additionalNavigationUrls);
+
+ mkdirSync(dirname(NGSW_CONFIG_OUTPUT_PATH), {recursive: true});
+ writeJson(NGSW_CONFIG_OUTPUT_PATH, ngswConfig);
+}
+
+function isGlobRedundant(glob, globs) {
+ // Find all globs that could cover other globs.
+ // For simplicity, we only consider globs ending with `/**`.
+ const genericGlobs = globs.filter(g => g.endsWith('/**'));
+
+ // A glob is redundant if it starts with the path of a potentially generic glob (excluding the
+ // trailing `**`) followed by a word character or a `*`.
+ // For example, `/foo/bar/baz` is covered (and thus made redundant) by `/foo/**`, but `/foo/{0,1}`
+ // is not.
+ return genericGlobs.some(g => {
+ const pathPrefix = g.slice(0, -2);
+ return (glob !== g) && glob.startsWith(pathPrefix) &&
+ /^[\w*]/.test(glob.slice(pathPrefix.length));
+ });
+}
+
+function readJson(filePath) {
+ return parseJson(readFileSync(filePath, 'utf8'));
+}
+
+function writeJson(filePath, obj) {
+ return writeFileSync(filePath, `${JSON.stringify(obj, null, 2)}\n`);
+}
diff --git a/aio/src/index.html b/aio/src/index.html
index 46ef427ffa..2444bf4735 100644
--- a/aio/src/index.html
+++ b/aio/src/index.html
@@ -24,7 +24,7 @@
-
+
diff --git a/aio/tests/deployment/shared/helpers.ts b/aio/tests/deployment/shared/helpers.ts
index fd0c0a6ba6..e439e985cf 100644
--- a/aio/tests/deployment/shared/helpers.ts
+++ b/aio/tests/deployment/shared/helpers.ts
@@ -19,7 +19,7 @@ export function getRedirector() {
}
export function getSwNavigationUrlChecker() {
- const config = loadJson(`${AIO_DIR}/ngsw-config.json`);
+ const config = loadJson(`${AIO_DIR}/src/generated/ngsw-config.json`);
const navigationUrlSpecs = processNavigationUrls('', config.navigationUrls);
const includePatterns = navigationUrlSpecs