Skip to content

Commit ee72835

Browse files
committed
feat: Generate an AppCache/ServiceWorker manifest during the build step, from all of the resources in the output directory.
1 parent de16404 commit ee72835

File tree

6 files changed

+141
-2
lines changed

6 files changed

+141
-2
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
node_modules/
22
.idea
3+
jsconfig.json
34
npm-debug.log
45
typings/
5-
tmp/
6+
tmp/

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The generated project has dependencies that require **Node 4 or greater**.
3232
* [Running Unit Tests](#running-unit-tests)
3333
* [Running End-to-End Tests](#running-end-to-end-tests)
3434
* [Deploying the App via GitHub Pages](#deploying-the-app-via-github-pages)
35+
* [Support for offline applications](#support-for-offline-applications)
3536
* [Known Issues](#known-issues)
3637

3738
## Installation
@@ -192,6 +193,16 @@ This will use the `format` npm script that in generated projects uses `clang-for
192193
You can modify the `format` script in `package.json` to run whatever formatting tool
193194
you prefer and `ng format` will still run it.
194195

196+
### Support for offline applications
197+
198+
By default a file `manifest.appcache` will be generated which lists all files included in
199+
a project's output, along with SHA1 hashes of all file contents. This file can be used
200+
directly as an AppCache manifest (for now, `index.html` must be manually edited to set this up).
201+
202+
The manifest is also annotated for use with `angular2-service-worker`. Some manual operations
203+
are currently required to enable this usage. The package must be installed, and `worker.js`
204+
manually copied into the build directory. Then, the commented snippet in `index.html` must be
205+
uncommented to register the worker script as a service worker.
195206

196207
## Known issues
197208

addon/ng2/blueprints/ng2/files/src/index.html

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
<base href="/">
77
{{content-for 'head'}}
88
<link rel="icon" type="image/x-icon" href="favicon.ico">
9+
10+
<!-- Service worker support is disabled by default.
11+
Install the worker script and uncomment to enable.
12+
Only enable service workers in production.
13+
<script type="text/javascript">
14+
if ('serviceWorker' in navigator) {
15+
navigator.serviceWorker.register('/worker.js').catch(function(err) {
16+
console.log('Error installing service worker: ', err);
17+
});
18+
}
19+
</script>
20+
-->
921
</head>
1022
<body>
1123
<<%= htmlComponentName %>-app>Loading...</<%= htmlComponentName %>-app>

lib/broccoli/angular2-app.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ var path = require('path');
22
var Concat = require('broccoli-concat');
33
var configReplace = require('./broccoli-config-replace');
44
var compileWithTypescript = require('./broccoli-typescript').default;
5+
var SwManifest = require('./service-worker-manifest').default;
56
var fs = require('fs');
67
var Funnel = require('broccoli-funnel');
78
var mergeTrees = require('broccoli-merge-trees');
@@ -82,7 +83,7 @@ Angular2App.prototype.toTree = function() {
8283
allowNone: true
8384
});
8485

85-
return mergeTrees([
86+
var merged = mergeTrees([
8687
assetTree,
8788
tsSrcTree,
8889
tsTree,
@@ -91,6 +92,8 @@ Angular2App.prototype.toTree = function() {
9192
vendorNpmJs,
9293
thirdPartyJs
9394
], { overwrite: true });
95+
96+
return mergeTrees([merged, new SwManifest(merged)]);
9497
};
9598

9699
/**
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"use strict";
2+
3+
var diffingPlugin = require('./diffing-broccoli-plugin');
4+
var path = require('path');
5+
var fs = require('fs');
6+
var crypto = require('crypto');
7+
8+
var FILE_ENCODING = { encoding: 'utf-8' };
9+
var MANIFEST_FILE = 'manifest.appcache';
10+
var FILE_HASH_PREFIX = '# sw.file.hash:';
11+
12+
class DiffingSWManifest {
13+
constructor(inputPath, cachePath, options) {
14+
this.inputPath = inputPath;
15+
this.cachePath = cachePath;
16+
this.options = options;
17+
this.firstBuild = true;
18+
}
19+
20+
rebuild(diff) {
21+
var manifest = {};
22+
if (this.firstBuild) {
23+
this.firstBuild = false;
24+
} else {
25+
// Read manifest from disk.
26+
manifest = this.readManifestFromCache();
27+
}
28+
29+
// Remove manifest entries for files that are no longer present.
30+
diff.removedPaths.forEach((file) => delete manifest[file]);
31+
32+
// Merge the lists of added and changed paths and update their hashes in the manifest.
33+
[]
34+
.concat(diff.addedPaths)
35+
.concat(diff.changedPaths)
36+
.filter((file) => file !== MANIFEST_FILE)
37+
.forEach((file) => manifest[file] = this.computeFileHash(file));
38+
var manifestPath = path.join(this.cachePath, MANIFEST_FILE);
39+
fs.writeFileSync(manifestPath, this.generateManifest(manifest));
40+
}
41+
42+
// Compute the hash of the given relative file.
43+
computeFileHash(file) {
44+
var contents = fs.readFileSync(path.join(this.inputPath, file));
45+
return crypto
46+
.createHash('sha1')
47+
.update(contents)
48+
.digest('hex');
49+
}
50+
51+
// Compute the hash of the bundle from the names and hashes of all included files.
52+
computeBundleHash(files, manifest) {
53+
var hash = crypto.createHash('sha1');
54+
files.forEach((file) => hash.update(manifest[file] + ':' + file));
55+
return hash.digest('hex');
56+
}
57+
58+
// Generate the string contents of the manifest.
59+
generateManifest(manifest) {
60+
var files = Object.keys(manifest).sort();
61+
var bundleHash = this.computeBundleHash(files, manifest);
62+
var contents = files
63+
.map((file) => `# sw.file.hash: ${this.computeFileHash(file)}\n/${file}`)
64+
.join('\n');
65+
return `CACHE MANIFEST
66+
# sw.bundle: ng-cli
67+
# sw.version: ${bundleHash}
68+
${contents}
69+
`;
70+
}
71+
72+
// Read the manifest from the cache and split it out into a dict of files to hashes.
73+
readManifestFromCache() {
74+
var contents = fs.readFileSync(path.join(this.cachePath, MANIFEST_FILE), FILE_ENCODING);
75+
var manifest = {};
76+
var hash = null;
77+
contents
78+
.split('\n')
79+
.map((line) => line.trim())
80+
.filter((line) => line !== 'CACHE MANIFEST')
81+
.filter((line) => line !== '')
82+
.filter((line) => !line.startsWith('#') || line.startsWith('# sw.'))
83+
.forEach((line) => {
84+
if (line.startsWith(FILE_HASH_PREFIX)) {
85+
// This is a hash prefix for the next file in the list.
86+
hash = line.substring(FILE_HASH_PREFIX.length).trim();
87+
} else if (line.startsWith('/')) {
88+
// This is a file belonging to the application.
89+
manifest[line.substring(1)] = hash;
90+
hash = null;
91+
}
92+
});
93+
return manifest;
94+
}
95+
}
96+
97+
Object.defineProperty(exports, "__esModule", { value: true });
98+
exports.default = diffingPlugin.wrapDiffingPlugin(DiffingSWManifest);

tests/e2e/e2e_workflow.spec.js

+14
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ describe('Basic end-to-end Workflow', function () {
6767
});
6868
});
6969

70+
it('Produces a service worker manifest after initial build', function() {
71+
var manifestPath = path.join(process.cwd(), 'dist', 'manifest.appcache');
72+
expect(fs.existsSync(manifestPath)).to.equal(true);
73+
// Read the worker.
74+
var lines = fs
75+
.readFileSync(manifestPath, {encoding: 'utf8'})
76+
.trim()
77+
.split('\n');
78+
79+
// Check that a few critical files have been detected.
80+
expect(lines).to.include('/index.html');
81+
expect(lines).to.include('/thirdparty/vendor.js');
82+
});
83+
7084
it('Perform `ng test` after initial build', function() {
7185
this.timeout(420000);
7286

0 commit comments

Comments
 (0)