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