Skip to content

Commit

Permalink
feat: add support for @angular/service-worker and manifest generation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alxhub committed Feb 9, 2017
1 parent 7807998 commit 53946df
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 177 deletions.
3 changes: 2 additions & 1 deletion packages/@angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"prefix": {
"type": "string"
},
"mobile": {
"serviceWorker": {
"description": "Experimental support for a service worker from @angular/service-worker.",
"type": "boolean",
"default": false
},
Expand Down
60 changes: 59 additions & 1 deletion packages/@angular/cli/models/webpack-configs/production.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as path from 'path';
import * as webpack from 'webpack';
import * as fs from 'fs';
import { stripIndent } from 'common-tags';
import { StaticAssetPlugin } from '../../plugins/static-asset';
import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin';
import { CompressionPlugin } from '../../lib/webpack/compression-plugin';
import { WebpackConfigOptions } from '../webpack-config';

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

let extraPlugins: any[] = [];
let entryPoints: {[key: string]: string[]} = {};

if (appConfig.serviceWorker) {
const nodeModules = path.resolve(projectRoot, 'node_modules');
const swModule = path.resolve(nodeModules, '@angular/service-worker');

// @angular/service-worker is required to be installed when serviceWorker is true.
if (!fs.existsSync(swModule)) {
throw new Error(stripIndent`
Your project is configured with serviceWorker = true, but @angular/service-worker
is not installed. Run \`npm install --save-dev @angular/service-worker\`
and try again, or run \`ng set apps.0.serviceWorker=false\` in your angular-cli.json.
`);
}

// Path to the worker script itself.
const workerPath = path.resolve(swModule, 'bundles/worker-basic.min.js');

// Path to a small script to register a service worker.
const registerPath = path.resolve(swModule, 'build/assets/register-basic.min.js');

// Sanity check - both of these files should be present in @angular/service-worker.
if (!fs.existsSync(workerPath) || !fs.existsSync(registerPath)) {
throw new Error(stripIndent`
The installed version of @angular/service-worker isn't supported by the CLI.
Please install a supported version. The following files should exist:
- ${registerPath}
- ${workerPath}
`);
}

extraPlugins.push(new GlobCopyWebpackPlugin({
patterns: ['ngsw-manifest.json'],
globOptions: {
optional: true,
},
}));

// Load the Webpack plugin for manifest generation and install it.
const AngularServiceWorkerPlugin = require('@angular/service-worker/build/webpack')
.AngularServiceWorkerPlugin;
extraPlugins.push(new AngularServiceWorkerPlugin());

// Copy the worker script into assets.
const workerContents = fs.readFileSync(workerPath).toString();
extraPlugins.push(new StaticAssetPlugin('worker-basic.min.js', workerContents));

// Add a script to index.html that registers the service worker.
// TODO(alxhub): inline this script somehow.
entryPoints['sw-register'] = [registerPath];
}

return {
entry: entryPoints,
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
Expand All @@ -24,6 +82,6 @@ export const getProdConfig = function (wco: WebpackConfigOptions) {
test: /\.js$|\.html$|\.css$/,
threshold: 10240
})
]
].concat(extraPlugins)
};
};
14 changes: 12 additions & 2 deletions packages/@angular/cli/plugins/glob-copy-webpack-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import * as denodeify from 'denodeify';
const globPromise = <any>denodeify(glob);
const statPromise = <any>denodeify(fs.stat);

function isDirectory(path: string) {
try {
return fs.statSync(path).isDirectory();
} catch (_) {
return false;
}
}

export interface GlobCopyWebpackPluginOptions {
patterns: string[];
globOptions: any;
Expand All @@ -17,9 +25,10 @@ export class GlobCopyWebpackPlugin {
apply(compiler: any): void {
let { patterns, globOptions } = this.options;
let context = globOptions.cwd || compiler.options.context;
let optional = !!globOptions.optional;

// convert dir patterns to globs
patterns = patterns.map(pattern => fs.statSync(path.resolve(context, pattern)).isDirectory()
patterns = patterns.map(pattern => isDirectory(path.resolve(context, pattern))
? pattern += '/**/*'
: pattern
);
Expand All @@ -37,7 +46,8 @@ export class GlobCopyWebpackPlugin {
.then((stat: any) => compilation.assets[relPath] = {
size: () => stat.size,
source: () => fs.readFileSync(path.resolve(context, relPath))
});
})
.catch((err: any) => optional ? Promise.resolve() : Promise.reject(err));

Promise.all(globs)
// flatten results
Expand Down
16 changes: 16 additions & 0 deletions packages/@angular/cli/plugins/static-asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as fs from 'fs';

export class StaticAssetPlugin {

constructor(private name: string, private contents: string) {}

apply(compiler: any): void {
compiler.plugin('emit', (compilation: any, cb: Function) => {
compilation.assets[this.name] = {
size: () => this.contents.length,
source: () => this.contents,
};
cb();
});
}
}
2 changes: 1 addition & 1 deletion packages/@angular/cli/utilities/package-chunk-sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ExtraEntry, extraEntryParser } from '../models/webpack-configs/utils';
// Sort chunks according to a predefined order:
// inline, polyfills, all scripts, all styles, vendor, main
export function packageChunkSort(appConfig: any) {
let entryPoints = ['inline', 'polyfills'];
let entryPoints = ['inline', 'polyfills', 'sw-register'];

const pushExtraEntries = (extraEntry: ExtraEntry) => {
if (entryPoints.indexOf(extraEntry.entry) === -1) {
Expand Down
10 changes: 8 additions & 2 deletions scripts/publish/validate_dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const ANGULAR_PACKAGES = [
'@angular/compiler-cli',
'@angular/core'
];
const OPTIONAL_PACKAGES = [
'@angular/service-worker'
];


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

const missingDeps = dependencies.filter(d => allDeps.indexOf(d) == -1);
const missingDeps = dependencies
.filter(d => allDeps.indexOf(d) == -1)
.filter(d => OPTIONAL_PACKAGES.indexOf(d) == -1);
reportMissingDependencies(missingDeps);

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

const internalPackages = Object.keys(packages);
const missingRootDeps = overallDeps.filter(d => allRootDeps.indexOf(d) == -1)
.filter(d => internalPackages.indexOf(d) == -1);
.filter(d => internalPackages.indexOf(d) == -1)
.filter(x => OPTIONAL_PACKAGES.indexOf(x) == -1);
reportMissingDependencies(missingRootDeps);

const overRootDeps = allRootDeps.filter(d => overallDeps.indexOf(d) == -1)
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/tests/build/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {join} from 'path';
import {npm} from '../../utils/process';
import {expectFileToExist} from '../../utils/fs';
import {ng} from '../../utils/process';

export default function() {
// Can't use the `ng` helper because somewhere the environment gets
// stuck to the first build done
return npm('install', '@angular/service-worker')
.then(() => ng('set', 'apps.0.serviceWorker=true'))
.then(() => ng('build', '--prod'))
.then(() => expectFileToExist(join(process.cwd(), 'dist')))
.then(() => expectFileToExist(join(process.cwd(), 'dist/ngsw-manifest.json')));
}
Loading

0 comments on commit 53946df

Please sign in to comment.