Skip to content

Commit 7b4b2c7

Browse files
alxhubhansl
authored andcommitted
feat(@angular/cli): support 5.0.0+ builds of @angular/service-worker
1 parent 00ca690 commit 7b4b2c7

File tree

10 files changed

+239
-62
lines changed

10 files changed

+239
-62
lines changed

package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"@angular/compiler": "^5.0.0",
108108
"@angular/compiler-cli": "^5.0.0",
109109
"@angular/core": "^5.0.0",
110+
"@angular/service-worker": "^5.0.0",
110111
"@types/common-tags": "^1.2.4",
111112
"@types/copy-webpack-plugin": "^4.0.0",
112113
"@types/denodeify": "^1.2.30",

packages/@angular/cli/commands/build.ts

+8
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ export const baseBuildCommandOptions: any = [
188188
default: 'none',
189189
description: 'Available on server platform only. Which external dependencies to bundle into '
190190
+ 'the module. By default, all of node_modules will be kept as requires.'
191+
},
192+
{
193+
name: 'service-worker',
194+
type: Boolean,
195+
default: true,
196+
aliases: ['sw'],
197+
description: 'Generates a service worker config for production builds, if the app has '
198+
+ 'service worker enabled.'
191199
}
192200
];
193201

packages/@angular/cli/models/build-options.ts

+1
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ export interface BuildOptions {
3131
namedChunks?: boolean;
3232
subresourceIntegrity?: boolean;
3333
forceTsCommonjs?: boolean;
34+
serviceWorker?: boolean;
3435
}

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

+57-48
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ import { PurifyPlugin } from '@angular-devkit/build-optimizer';
88
import { StaticAssetPlugin } from '../../plugins/static-asset';
99
import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin';
1010
import { WebpackConfigOptions } from '../webpack-config';
11+
import { NEW_SW_VERSION } from '../../utilities/service-worker';
1112

1213
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
1314

15+
const OLD_SW_VERSION = '>= 1.0.0-beta.5 < 2.0.0';
16+
1417
/**
1518
* license-webpack-plugin has a peer dependency on webpack-sources, list it in a comment to
1619
* let the dependency validator know it is used.
@@ -40,63 +43,69 @@ export function getProdConfig(wco: WebpackConfigOptions) {
4043

4144
// Read the version of @angular/service-worker and throw if it doesn't match the
4245
// expected version.
43-
const allowedVersion = '>= 1.0.0-beta.5 < 2.0.0';
4446
const swPackageJson = fs.readFileSync(`${swModule}/package.json`).toString();
4547
const swVersion = JSON.parse(swPackageJson)['version'];
46-
if (!semver.satisfies(swVersion, allowedVersion)) {
48+
49+
const isLegacySw = semver.satisfies(swVersion, OLD_SW_VERSION);
50+
const isModernSw = semver.satisfies(swVersion, NEW_SW_VERSION);
51+
52+
if (!isLegacySw && !isModernSw) {
4753
throw new Error(stripIndent`
4854
The installed version of @angular/service-worker is ${swVersion}. This version of the CLI
49-
requires the @angular/service-worker version to satisfy ${allowedVersion}. Please upgrade
55+
requires the @angular/service-worker version to satisfy ${OLD_SW_VERSION}. Please upgrade
5056
your service worker version.
5157
`);
5258
}
5359

54-
// Path to the worker script itself.
55-
const workerPath = path.resolve(swModule, 'bundles/worker-basic.min.js');
56-
57-
// Path to a small script to register a service worker.
58-
const registerPath = path.resolve(swModule, 'build/assets/register-basic.min.js');
59-
60-
// Sanity check - both of these files should be present in @angular/service-worker.
61-
if (!fs.existsSync(workerPath) || !fs.existsSync(registerPath)) {
62-
throw new Error(stripIndent`
63-
The installed version of @angular/service-worker isn't supported by the CLI.
64-
Please install a supported version. The following files should exist:
65-
- ${registerPath}
66-
- ${workerPath}
67-
`);
60+
if (isLegacySw) {
61+
// Path to the worker script itself.
62+
const workerPath = path.resolve(swModule, 'bundles/worker-basic.min.js');
63+
64+
// Path to a small script to register a service worker.
65+
const registerPath = path.resolve(swModule, 'build/assets/register-basic.min.js');
66+
67+
// Sanity check - both of these files should be present in @angular/service-worker.
68+
if (!fs.existsSync(workerPath) || !fs.existsSync(registerPath)) {
69+
throw new Error(stripIndent`
70+
The installed version of @angular/service-worker isn't supported by the CLI.
71+
Please install a supported version. The following files should exist:
72+
- ${registerPath}
73+
- ${workerPath}
74+
`);
75+
}
76+
77+
// CopyWebpackPlugin replaces GlobCopyWebpackPlugin, but AngularServiceWorkerPlugin depends
78+
// on specific behaviour from latter.
79+
// AngularServiceWorkerPlugin expects the ngsw-manifest.json to be present in the 'emit' phase
80+
// but with CopyWebpackPlugin it's only there on 'after-emit'.
81+
// So for now we keep it here, but if AngularServiceWorkerPlugin changes we remove it.
82+
extraPlugins.push(new GlobCopyWebpackPlugin({
83+
patterns: [
84+
'ngsw-manifest.json',
85+
{ glob: 'ngsw-manifest.json',
86+
input: path.resolve(projectRoot, appConfig.root), output: '' }
87+
],
88+
globOptions: {
89+
cwd: projectRoot,
90+
optional: true,
91+
},
92+
}));
93+
94+
// Load the Webpack plugin for manifest generation and install it.
95+
const AngularServiceWorkerPlugin = require('@angular/service-worker/build/webpack')
96+
.AngularServiceWorkerPlugin;
97+
extraPlugins.push(new AngularServiceWorkerPlugin({
98+
baseHref: buildOptions.baseHref || '/',
99+
}));
100+
101+
// Copy the worker script into assets.
102+
const workerContents = fs.readFileSync(workerPath).toString();
103+
extraPlugins.push(new StaticAssetPlugin('worker-basic.min.js', workerContents));
104+
105+
// Add a script to index.html that registers the service worker.
106+
// TODO(alxhub): inline this script somehow.
107+
entryPoints['sw-register'] = [registerPath];
68108
}
69-
70-
// CopyWebpackPlugin replaces GlobCopyWebpackPlugin, but AngularServiceWorkerPlugin depends
71-
// on specific behaviour from latter.
72-
// AngularServiceWorkerPlugin expects the ngsw-manifest.json to be present in the 'emit' phase
73-
// but with CopyWebpackPlugin it's only there on 'after-emit'.
74-
// So for now we keep it here, but if AngularServiceWorkerPlugin changes we remove it.
75-
extraPlugins.push(new GlobCopyWebpackPlugin({
76-
patterns: [
77-
'ngsw-manifest.json',
78-
{ glob: 'ngsw-manifest.json', input: path.resolve(projectRoot, appConfig.root), output: '' }
79-
],
80-
globOptions: {
81-
cwd: projectRoot,
82-
optional: true,
83-
},
84-
}));
85-
86-
// Load the Webpack plugin for manifest generation and install it.
87-
const AngularServiceWorkerPlugin = require('@angular/service-worker/build/webpack')
88-
.AngularServiceWorkerPlugin;
89-
extraPlugins.push(new AngularServiceWorkerPlugin({
90-
baseHref: buildOptions.baseHref || '/',
91-
}));
92-
93-
// Copy the worker script into assets.
94-
const workerContents = fs.readFileSync(workerPath).toString();
95-
extraPlugins.push(new StaticAssetPlugin('worker-basic.min.js', workerContents));
96-
97-
// Add a script to index.html that registers the service worker.
98-
// TODO(alxhub): inline this script somehow.
99-
entryPoints['sw-register'] = [registerPath];
100109
}
101110

102111
if (buildOptions.extractLicenses) {

packages/@angular/cli/tasks/build.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NgCliWebpackConfig } from '../models/webpack-config';
88
import { getWebpackStatsConfig } from '../models/webpack-configs/utils';
99
import { CliConfig } from '../models/config';
1010
import { statsToString, statsWarningsToString, statsErrorsToString } from '../utilities/stats';
11+
import { augmentAppWithServiceWorker, usesServiceWorker } from '../utilities/service-worker';
1112

1213
const Task = require('../ember-cli/lib/models/task');
1314
const SilentError = require('silent-error');
@@ -69,7 +70,15 @@ export default Task.extend({
6970
if (stats.hasErrors()) {
7071
reject();
7172
} else {
72-
resolve();
73+
if (!!app.serviceWorker && runTaskOptions.target === 'production' &&
74+
usesServiceWorker(this.project.root) && runTaskOptions.serviceWorker !== false) {
75+
const appRoot = path.resolve(this.project.root, app.root);
76+
augmentAppWithServiceWorker(this.project.root, appRoot, path.resolve(outputPath),
77+
runTaskOptions.baseHref || '/')
78+
.then(() => resolve(), (err: any) => reject(err));
79+
} else {
80+
resolve();
81+
}
7382
}
7483
};
7584

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Filesystem } from '@angular/service-worker/config';
2+
import { stripIndent } from 'common-tags';
3+
import * as crypto from 'crypto';
4+
import * as fs from 'fs';
5+
import * as path from 'path';
6+
import * as semver from 'semver';
7+
8+
export const NEW_SW_VERSION = '>= 5.0.0-rc.0';
9+
10+
class CliFilesystem implements Filesystem {
11+
constructor(private base: string) {}
12+
13+
list(_path: string): Promise<string[]> {
14+
return Promise.resolve(this.syncList(_path));
15+
}
16+
17+
private syncList(_path: string): string[] {
18+
const dir = this.canonical(_path);
19+
const entries = fs.readdirSync(dir).map(
20+
(entry: string) => ({entry, stats: fs.statSync(path.posix.join(dir, entry))}));
21+
const files = entries.filter((entry: any) => !entry.stats.isDirectory())
22+
.map((entry: any) => path.posix.join(_path, entry.entry));
23+
24+
return entries.filter((entry: any) => entry.stats.isDirectory())
25+
.map((entry: any) => path.posix.join(_path, entry.entry))
26+
.reduce((list: string[], subdir: string) => list.concat(this.syncList(subdir)), files);
27+
}
28+
29+
read(_path: string): Promise<string> {
30+
const file = this.canonical(_path);
31+
return Promise.resolve(fs.readFileSync(file).toString());
32+
}
33+
34+
hash(_path: string): Promise<string> {
35+
const sha1 = crypto.createHash('sha1');
36+
const file = this.canonical(_path);
37+
const contents: Buffer = fs.readFileSync(file);
38+
sha1.update(contents);
39+
return Promise.resolve(sha1.digest('hex'));
40+
}
41+
42+
write(_path: string, contents: string): Promise<void> {
43+
const file = this.canonical(_path);
44+
fs.writeFileSync(file, contents);
45+
return Promise.resolve();
46+
}
47+
48+
private canonical(_path: string): string { return path.posix.join(this.base, _path); }
49+
}
50+
51+
export function usesServiceWorker(projectRoot: string): boolean {
52+
const nodeModules = path.resolve(projectRoot, 'node_modules');
53+
const swModule = path.resolve(nodeModules, '@angular/service-worker');
54+
if (!fs.existsSync(swModule)) {
55+
return false;
56+
}
57+
58+
const swPackageJson = fs.readFileSync(`${swModule}/package.json`).toString();
59+
const swVersion = JSON.parse(swPackageJson)['version'];
60+
61+
return semver.satisfies(swVersion, NEW_SW_VERSION);
62+
}
63+
64+
export function augmentAppWithServiceWorker(projectRoot: string, appRoot: string,
65+
outputPath: string, baseHref: string): Promise<void> {
66+
const nodeModules = path.resolve(projectRoot, 'node_modules');
67+
const swModule = path.resolve(nodeModules, '@angular/service-worker');
68+
69+
// Path to the worker script itself.
70+
const workerPath = path.resolve(swModule, 'ngsw-worker.js');
71+
const configPath = path.resolve(appRoot, 'ngsw-config.json');
72+
73+
if (!fs.existsSync(configPath)) {
74+
throw new Error(stripIndent`Expected to find an ngsw-config.json configuration file in the
75+
application root. Either provide one or disable Service Worker
76+
build support in angular-cli.json.`);
77+
}
78+
const config = fs.readFileSync(configPath, 'utf8');
79+
80+
const Generator = require('@angular/service-worker/config').Generator;
81+
const gen = new Generator(new CliFilesystem(outputPath), baseHref);
82+
return gen
83+
.process(JSON.parse(config))
84+
.then((output: Object) => {
85+
const manifest = JSON.stringify(output, null, 2);
86+
fs.writeFileSync(path.resolve(outputPath, 'ngsw.json'), manifest);
87+
// Copy worker script to dist directory.
88+
const workerCode = fs.readFileSync(workerPath);
89+
fs.writeFileSync(path.resolve(outputPath, 'ngsw-worker.js'), workerCode);
90+
});
91+
}

scripts/publish/validate_dependencies.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const REQUIRE_RE = /\brequire\('[^)]+?'\)/g;
1414
const IGNORE_RE = /\s+@ignoreDep\s+\S+/g;
1515
const NODE_PACKAGES = [
1616
'child_process',
17+
'crypto',
1718
'fs',
1819
'https',
1920
'os',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {join} from 'path';
2+
import {getGlobalVariable} from '../../utils/env';
3+
import {expectFileToExist, expectFileToMatch, writeFile, moveFile} from '../../utils/fs';
4+
import {ng, silentNpm} from '../../utils/process';
5+
6+
export default function() {
7+
// Skip this in ejected tests.
8+
if (getGlobalVariable('argv').eject) {
9+
return Promise.resolve();
10+
}
11+
12+
const rootManifest = join(process.cwd(), 'ngsw-manifest.json');
13+
14+
// Can't use the `ng` helper because somewhere the environment gets
15+
// stuck to the first build done
16+
return silentNpm('remove', '@angular/service-worker')
17+
.then(() => silentNpm('install', '@angular/[email protected]'))
18+
.then(() => ng('set', 'apps.0.serviceWorker=true'))
19+
.then(() => ng('build', '--prod'))
20+
.then(() => expectFileToExist(join(process.cwd(), 'dist')))
21+
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw-manifest.json')))
22+
.then(() => ng('build', '--prod', '--base-href=/foo/bar'))
23+
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw-manifest.json')))
24+
.then(() => expectFileToMatch('dist/ngsw-manifest.json', /"\/foo\/bar\/index.html"/))
25+
.then(() => writeFile(rootManifest, '{"local": true}'))
26+
.then(() => ng('build', '--prod'))
27+
.then(() => expectFileToMatch('dist/ngsw-manifest.json', /\"local\"/))
28+
.then(() => moveFile(rootManifest, join(process.cwd(), 'src/ngsw-manifest.json')))
29+
.then(() => ng('build', '--prod'))
30+
.then(() => expectFileToMatch('dist/ngsw-manifest.json', /\"local\"/))
31+
.then(() => ng('set', 'apps.0.serviceWorker=false'));
32+
}
+29-13
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
11
import {join} from 'path';
22
import {getGlobalVariable} from '../../utils/env';
3-
import {expectFileToExist, expectFileToMatch, writeFile, moveFile} from '../../utils/fs';
3+
import {expectFileNotToExist, expectFileToExist, expectFileToMatch, writeFile} from '../../utils/fs';
44
import {ng, silentNpm} from '../../utils/process';
55

6+
const MANIFEST = {
7+
index: '/index.html',
8+
assetGroups: [{
9+
name: 'cli',
10+
resources: {
11+
files: [
12+
'/**/*.html',
13+
'/**/*.js',
14+
'/**/*.css',
15+
'/assets/**/*',
16+
'!/ngsw-worker.js',
17+
],
18+
urls: [
19+
'http://test.com/foo/bar',
20+
],
21+
},
22+
}],
23+
};
24+
625
export default function() {
726
// Skip this in ejected tests.
827
if (getGlobalVariable('argv').eject) {
928
return Promise.resolve();
1029
}
1130

12-
const rootManifest = join(process.cwd(), 'ngsw-manifest.json');
13-
1431
// Can't use the `ng` helper because somewhere the environment gets
1532
// stuck to the first build done
16-
return silentNpm('install', '@angular/[email protected]')
33+
return silentNpm('remove', '@angular/service-worker')
34+
.then(() => silentNpm('install', '@angular/service-worker'))
1735
.then(() => ng('set', 'apps.0.serviceWorker=true'))
36+
.then(() => writeFile('src/ngsw-config.json', JSON.stringify(MANIFEST, null, 2)))
1837
.then(() => ng('build', '--prod'))
1938
.then(() => expectFileToExist(join(process.cwd(), 'dist')))
20-
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw-manifest.json')))
39+
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw.json')))
2140
.then(() => ng('build', '--prod', '--base-href=/foo/bar'))
22-
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw-manifest.json')))
23-
.then(() => expectFileToMatch('dist/ngsw-manifest.json', /"\/foo\/bar\/index.html"/))
24-
.then(() => writeFile(rootManifest, '{"local": true}'))
25-
.then(() => ng('build', '--prod'))
26-
.then(() => expectFileToMatch('dist/ngsw-manifest.json', /\"local\"/))
27-
.then(() => moveFile(rootManifest, join(process.cwd(), 'src/ngsw-manifest.json')))
28-
.then(() => ng('build', '--prod'))
29-
.then(() => expectFileToMatch('dist/ngsw-manifest.json', /\"local\"/));
41+
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw.json')))
42+
.then(() => expectFileToMatch('dist/ngsw.json', /"\/foo\/bar\/index.html"/))
43+
.then(() => ng('build', '--prod', '--service-worker=false'))
44+
.then(() => expectFileNotToExist('dist/ngsw.json'))
45+
.then(() => ng('set', 'apps.0.serviceWorker=false'));
3046
}

0 commit comments

Comments
 (0)