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:
parent
982521f284
commit
d07e736f17
|
@ -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",
|
||||
|
|
|
@ -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*"},
|
||||
|
||||
|
|
|
@ -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}"
|
||||
]
|
||||
}
|
|
@ -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}"
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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`);
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue