Skip to content

Commit cb2e418

Browse files
alxhubhansl
authored andcommitted
feat: add support for @angular/service-worker and manifest generation
Adds the flag 'serviceWorker' to angular-cli.json that enables support for @angular/service-worker. When this flag is true, production builds will be set up with a service worker. A ngsw-manifest.json file will be generated (or augmented) in the dist/ root, and the service worker script will be copied there. A short script will be added to index.html to register the service worker. @angular/service-worker is a dependency of @angular/cli, but not of generated projects. It is desirable for users to be able to update the version of @angular/service-worker used in their apps independently of the CLI version. Thus, the CLI will error if serviceWorker=true but @angular/service-worker is not installed in the application's node_modules, as it pulls all the service worker scripts from there. If the flag is false the effect on the CLI is minimal - the webpack plugins associated with the SW are not even require()'d. Closes #4544
1 parent 849155c commit cb2e418

File tree

8 files changed

+279
-177
lines changed

8 files changed

+279
-177
lines changed

packages/@angular/cli/lib/config/schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"prefix": {
6868
"type": "string"
6969
},
70-
"mobile": {
70+
"serviceWorker": {
71+
"description": "Experimental support for a service worker from @angular/service-worker.",
7172
"type": "boolean",
7273
"default": false
7374
},

packages/@angular/cli/models/webpack-configs/production.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import * as path from 'path';
22
import * as webpack from 'webpack';
3+
import * as fs from 'fs';
4+
import { stripIndent } from 'common-tags';
5+
import { StaticAssetPlugin } from '../../plugins/static-asset';
6+
import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin';
37
import { CompressionPlugin } from '../../lib/webpack/compression-plugin';
48
import { WebpackConfigOptions } from '../webpack-config';
59

@@ -8,7 +12,61 @@ export const getProdConfig = function (wco: WebpackConfigOptions) {
812
const { projectRoot, buildOptions, appConfig } = wco;
913
const appRoot = path.resolve(projectRoot, appConfig.root);
1014

15+
let extraPlugins: any[] = [];
16+
let entryPoints: {[key: string]: string[]} = {};
17+
18+
if (appConfig.serviceWorker) {
19+
const nodeModules = path.resolve(projectRoot, 'node_modules');
20+
const swModule = path.resolve(nodeModules, '@angular/service-worker');
21+
22+
// @angular/service-worker is required to be installed when serviceWorker is true.
23+
if (!fs.existsSync(swModule)) {
24+
throw new Error(stripIndent`
25+
Your project is configured with serviceWorker = true, but @angular/service-worker
26+
is not installed. Run \`npm install --save-dev @angular/service-worker\`
27+
and try again, or run \`ng set apps.0.serviceWorker=false\` in your angular-cli.json.
28+
`);
29+
}
30+
31+
// Path to the worker script itself.
32+
const workerPath = path.resolve(swModule, 'bundles/worker-basic.min.js');
33+
34+
// Path to a small script to register a service worker.
35+
const registerPath = path.resolve(swModule, 'build/assets/register-basic.min.js');
36+
37+
// Sanity check - both of these files should be present in @angular/service-worker.
38+
if (!fs.existsSync(workerPath) || !fs.existsSync(registerPath)) {
39+
throw new Error(stripIndent`
40+
The installed version of @angular/service-worker isn't supported by the CLI.
41+
Please install a supported version. The following files should exist:
42+
- ${registerPath}
43+
- ${workerPath}
44+
`);
45+
}
46+
47+
extraPlugins.push(new GlobCopyWebpackPlugin({
48+
patterns: ['ngsw-manifest.json'],
49+
globOptions: {
50+
optional: true,
51+
},
52+
}));
53+
54+
// Load the Webpack plugin for manifest generation and install it.
55+
const AngularServiceWorkerPlugin = require('@angular/service-worker/build/webpack')
56+
.AngularServiceWorkerPlugin;
57+
extraPlugins.push(new AngularServiceWorkerPlugin());
58+
59+
// Copy the worker script into assets.
60+
const workerContents = fs.readFileSync(workerPath).toString();
61+
extraPlugins.push(new StaticAssetPlugin('worker-basic.min.js', workerContents));
62+
63+
// Add a script to index.html that registers the service worker.
64+
// TODO(alxhub): inline this script somehow.
65+
entryPoints['sw-register'] = [registerPath];
66+
}
67+
1168
return {
69+
entry: entryPoints,
1270
plugins: [
1371
new webpack.DefinePlugin({
1472
'process.env.NODE_ENV': JSON.stringify('production')
@@ -24,6 +82,6 @@ export const getProdConfig = function (wco: WebpackConfigOptions) {
2482
test: /\.js$|\.html$|\.css$/,
2583
threshold: 10240
2684
})
27-
]
85+
].concat(extraPlugins)
2886
};
2987
};

packages/@angular/cli/plugins/glob-copy-webpack-plugin.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import * as denodeify from 'denodeify';
66
const globPromise = <any>denodeify(glob);
77
const statPromise = <any>denodeify(fs.stat);
88

9+
function isDirectory(path: string) {
10+
try {
11+
return fs.statSync(path).isDirectory();
12+
} catch (_) {
13+
return false;
14+
}
15+
}
16+
917
export interface GlobCopyWebpackPluginOptions {
1018
patterns: string[];
1119
globOptions: any;
@@ -17,9 +25,10 @@ export class GlobCopyWebpackPlugin {
1725
apply(compiler: any): void {
1826
let { patterns, globOptions } = this.options;
1927
let context = globOptions.cwd || compiler.options.context;
28+
let optional = !!globOptions.optional;
2029

2130
// convert dir patterns to globs
22-
patterns = patterns.map(pattern => fs.statSync(path.resolve(context, pattern)).isDirectory()
31+
patterns = patterns.map(pattern => isDirectory(path.resolve(context, pattern))
2332
? pattern += '/**/*'
2433
: pattern
2534
);
@@ -37,7 +46,8 @@ export class GlobCopyWebpackPlugin {
3746
.then((stat: any) => compilation.assets[relPath] = {
3847
size: () => stat.size,
3948
source: () => fs.readFileSync(path.resolve(context, relPath))
40-
});
49+
})
50+
.catch((err: any) => optional ? Promise.resolve() : Promise.reject(err));
4151

4252
Promise.all(globs)
4353
// flatten results
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as fs from 'fs';
2+
3+
export class StaticAssetPlugin {
4+
5+
constructor(private name: string, private contents: string) {}
6+
7+
apply(compiler: any): void {
8+
compiler.plugin('emit', (compilation: any, cb: Function) => {
9+
compilation.assets[this.name] = {
10+
size: () => this.contents.length,
11+
source: () => this.contents,
12+
};
13+
cb();
14+
});
15+
}
16+
}

packages/@angular/cli/utilities/package-chunk-sort.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ExtraEntry, extraEntryParser } from '../models/webpack-configs/utils';
33
// Sort chunks according to a predefined order:
44
// inline, polyfills, all scripts, all styles, vendor, main
55
export function packageChunkSort(appConfig: any) {
6-
let entryPoints = ['inline', 'polyfills'];
6+
let entryPoints = ['inline', 'polyfills', 'sw-register'];
77

88
const pushExtraEntries = (extraEntry: ExtraEntry) => {
99
if (entryPoints.indexOf(extraEntry.entry) === -1) {

scripts/publish/validate_dependencies.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ const ANGULAR_PACKAGES = [
2727
'@angular/compiler-cli',
2828
'@angular/core'
2929
];
30+
const OPTIONAL_PACKAGES = [
31+
'@angular/service-worker'
32+
];
3033

3134

3235
function listImportedModules(source) {
@@ -121,7 +124,9 @@ for (const packageName of Object.keys(packages)) {
121124
.concat(Object.keys(packageJson['devDependencies'] || {}))
122125
.concat(Object.keys(packageJson['peerDependencies'] || {}));
123126

124-
const missingDeps = dependencies.filter(d => allDeps.indexOf(d) == -1);
127+
const missingDeps = dependencies
128+
.filter(d => allDeps.indexOf(d) == -1)
129+
.filter(d => OPTIONAL_PACKAGES.indexOf(d) == -1);
125130
reportMissingDependencies(missingDeps);
126131

127132
const overDeps = allDeps.filter(d => dependencies.indexOf(d) == -1)
@@ -141,7 +146,8 @@ const allRootDeps = []
141146

142147
const internalPackages = Object.keys(packages);
143148
const missingRootDeps = overallDeps.filter(d => allRootDeps.indexOf(d) == -1)
144-
.filter(d => internalPackages.indexOf(d) == -1);
149+
.filter(d => internalPackages.indexOf(d) == -1)
150+
.filter(x => OPTIONAL_PACKAGES.indexOf(x) == -1);
145151
reportMissingDependencies(missingRootDeps);
146152

147153
const overRootDeps = allRootDeps.filter(d => overallDeps.indexOf(d) == -1)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {join} from 'path';
2+
import {npm} from '../../utils/process';
3+
import {expectFileToExist} from '../../utils/fs';
4+
import {ng} from '../../utils/process';
5+
6+
export default function() {
7+
// Can't use the `ng` helper because somewhere the environment gets
8+
// stuck to the first build done
9+
return npm('install', '@angular/service-worker')
10+
.then(() => ng('set', 'apps.0.serviceWorker=true'))
11+
.then(() => ng('build', '--prod'))
12+
.then(() => expectFileToExist(join(process.cwd(), 'dist')))
13+
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw-manifest.json')));
14+
}

0 commit comments

Comments
 (0)