build(docs-infra): auto-generate SW `navigationUrls` from Firebase config (#42452)

Previously, redirects had to be configured in both the Firebase config
(`firebase.json`) and the ServiceWorker config (`ngsw-config.json`).
This made it challenging to correctly configure redirects, since one had
to understand the different formats of the two configs, and was also
prone to getting out-of-sync configs.

This commit simplifies the process of adding redirects by removing the
need to update the ServiceWorker config (`ngsw-config.json`) and keep it
in sync with the Firebase config (`firebase.json`). Instead the
ServiceWorker `navigationUrls` are automatically generated from the list
of redirects in the Firebase config.

NOTE:
Currently, the automatic generation only supports the limited set of
patterns that are necessary to translate the existing redirects. It can
be made more sophisticated in the future, should the need arise.

PR Close #42452
This commit is contained in:
George Kalpakas 2021-06-18 00:13:10 +03:00 committed by Dylan Hunn
parent 982521f284
commit d07e736f17
8 changed files with 182 additions and 154 deletions

View File

@ -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",

View File

@ -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*"},

View File

@ -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}"
]
}

View File

@ -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}"
]
}

View File

@ -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",

View File

@ -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`);
}

View File

@ -24,7 +24,7 @@
<link rel="apple-touch-icon" sizes="144x144" href="assets/images/favicons/favicon-144x144.png">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/images/favicons/favicon-144x144.png">
<!-- NOTE: These need to be kept in sync with `ngsw-config.json`. -->
<!-- NOTE: These need to be kept in sync with `ngsw-config.template.json`. -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons&display=block">

View File

@ -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