├── test
├── html
│ ├── app.css
│ ├── app2.css
│ ├── js
│ │ └── app.js
│ ├── subimport.html
│ ├── import.html
│ ├── import2.html
│ ├── basic2.html
│ └── basic.html
├── mocha.opts
└── tests.js
├── .gitignore
├── .travis.yml
├── .jshintrc
├── .editorconfig
├── package.json
├── lib
├── listresources.js
└── manifest.js
├── bin
└── http2-push-manifest
├── README.md
└── LICENSE
/test/html/app.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/html/app2.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/html/js/app.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/html/subimport.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --ui tdd
2 | --slow 500
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | push_manifest.json
3 |
--------------------------------------------------------------------------------
/test/html/import.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/html/import2.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4"
4 | - "node"
5 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "indent": 2,
3 | "node": true,
4 | "browser": true,
5 | "esnext": true,
6 | "bitwise": true,
7 | "immed": true,
8 | "noarg": true
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 2
6 | indent_style = space
7 | trim_trailing_whitespace = true
8 |
9 | [*.md]
10 | trim_trailing_whitespace = false
11 |
--------------------------------------------------------------------------------
/test/html/basic2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/html/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "http2-push-manifest",
3 | "version": "1.0.1",
4 | "description": "Generate a list of static resources for http2 push and preload.",
5 | "homepage": "https://github.com/GoogleChrome/http2-push-manifest",
6 | "author": "Eric Bidelman ",
7 | "license": "Apache-2.0",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/GoogleChrome/http2-push-manifest"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/GoogleChrome/http2-push-manifest/issues"
14 | },
15 | "keywords": [
16 | "http2",
17 | "http2 push",
18 | "performance"
19 | ],
20 | "main": "lib/manifest.js",
21 | "bin": {
22 | "http2-push-manifest": "bin/http2-push-manifest"
23 | },
24 | "engines": {
25 | "node": ">=4.0"
26 | },
27 | "scripts": {
28 | "lint": "jshint --verbose lib",
29 | "test": "npm run lint; mocha"
30 | },
31 | "dependencies": {
32 | "dom5": "^1.1.1",
33 | "hydrolysis": "1.22.0",
34 | "nopt": "^3.0.4",
35 | "vulcanize": "^1.13.1"
36 | },
37 | "devDependencies": {
38 | "jshint": "^2.8.0",
39 | "mocha": "^2.3.3"
40 | },
41 | "optionalDependencies": {
42 | "update-notifier": "^0.5.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/listresources.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // jshint node: true
18 | 'use strict';
19 |
20 | let fs = require('fs');
21 | let hyd = require('hydrolysis');
22 | let dom5 = require('dom5');
23 | let url = require('url');
24 | let path = require('path');
25 |
26 | /**
27 | * Swallows errors from Hydrolysis so ENOENT files don't throw errors.
28 | * @class
29 | * @extends {hydrolysis.FSResolver}
30 | */
31 | class ErrorSwallowingFSResolver extends hyd.FSResolver {
32 | constructor(config) {
33 | super(config);
34 | }
35 |
36 | accept(uri, deferred) {
37 | var reject = deferred.reject;
38 | deferred.reject = arg => deferred.resolve('');
39 | return super.accept(uri, deferred);
40 | }
41 | }
42 |
43 |
44 | /**
45 | * Finds and collects the static resources in a page.
46 | * @class
47 | */
48 | class ResourceList {
49 |
50 | static get EXTERNAL_RESOURCE() {
51 | return /^(?:https?:)?\/\//;
52 | }
53 |
54 | constructor(opts) {
55 | this.basePath = opts.basePath;
56 |
57 | let inputPath = opts.inputPath;
58 |
59 | if (!inputPath || !this.basePath) {
60 | console.error('Need input path!');
61 | process.exit(1);
62 | }
63 |
64 | this.basePath = path.resolve(this.basePath);
65 | inputPath = path.resolve(path.resolve(this.basePath, inputPath));
66 |
67 | if (fs.statSync(inputPath).isDirectory()) {
68 | inputPath = path.join(inputPath, 'index.html');
69 | }
70 |
71 | let loader = new hyd.Loader();
72 | loader.addResolver(new ErrorSwallowingFSResolver({
73 | root: this.basePath,
74 | basePath: '/'
75 | }));
76 |
77 | // Ignore external resources.
78 | loader.addResolver(new hyd.NoopResolver(ResourceList.EXTERNAL_RESOURCE));
79 |
80 | this.analyzer = new hyd.Analyzer(false, loader);
81 |
82 | this.inputPath = path.join('/', path.relative(this.basePath, inputPath));
83 | }
84 |
85 | treeToList(tree, accum) {
86 | if (!accum) {
87 | accum = [];
88 | }
89 | accum.push(tree.href);
90 | }
91 |
92 | styleToUrl(href, style) {
93 | let src = dom5.getAttribute(style, 'href');
94 | if (ResourceList.EXTERNAL_RESOURCE.test(src)) {
95 | return;
96 | }
97 | if (src) {
98 | return url.resolve(href, src);
99 | }
100 | }
101 |
102 | scriptToUrl(href, script) {
103 | let src = dom5.getAttribute(script, 'src');
104 | if (ResourceList.EXTERNAL_RESOURCE.test(src)) {
105 | return;
106 | }
107 | if (src) {
108 | return url.resolve(href, src);
109 | }
110 | }
111 |
112 | treeToUrls(tree, accum) {
113 | if (!accum) {
114 | accum = [];
115 | }
116 | if (!tree) {
117 | return accum;
118 | }
119 | if (!tree.href) {
120 | return accum;
121 | }
122 | accum.push(tree.href);
123 | tree.imports.forEach(im => {
124 | if (im.href) {
125 | this.treeToUrls(im, accum);
126 | }
127 | });
128 | tree.html.script.forEach(script => {
129 | let u = this.scriptToUrl(tree.href, script);
130 | if (u) {
131 | accum.push(u);
132 | }
133 | });
134 | tree.html.style.forEach(style => {
135 | let u = this.styleToUrl(tree.href, style);
136 | if (u) {
137 | accum.push(u);
138 | }
139 | });
140 | return accum;
141 | }
142 |
143 | list() {
144 | return this.analyzer.metadataTree(this.inputPath).then(tree => {
145 | let list = this.treeToUrls(tree).slice(1).reverse();
146 | return list;
147 | }).catch(err => {
148 | console.error(err);
149 | });
150 | }
151 | }
152 |
153 | module.exports = ResourceList;
154 |
--------------------------------------------------------------------------------
/lib/manifest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2015 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // jshint node: true
18 | 'use strict';
19 |
20 |
21 | let fs = require('fs');
22 | let path = require('path');
23 | let ResourceList = require('./listresources');
24 |
25 | /**
26 | * Map of file extension to request type.
27 | * See https://fetch.spec.whatwg.org/#concept-request-type
28 | * @const
29 | */
30 | const EXTENSION_TO_TYPE = {
31 | '.css': 'style',
32 | '.gif': 'image',
33 | '.html': 'document',
34 | '.png': 'image',
35 | '.jpg': 'image',
36 | '.js': 'script',
37 | '.json': 'script',
38 | '.svg': 'image',
39 | '.webp': 'image',
40 | '.woff': 'font',
41 | '.woff2': 'font'
42 | };
43 |
44 | /**
45 | * JSON manifest for push resources.
46 | * @class
47 | */
48 | class PushManifest {
49 |
50 | // Single file format:
51 | // {
52 | // "/style.css": {
53 | // "type": "style",
54 | // "weight": 50
55 | // },
56 | // "/path/to/app.js": {
57 | // "type": "script",
58 | // "weight": 10
59 | // }
60 | // }
61 | //
62 | // The multi-file format is a superset of the single case:
63 |
64 | // Multi-file format:
65 | // {
66 | // "/index.html": {
67 | // "/style.css": {
68 | // "type": "style",
69 | // "weight": 50
70 | // },
71 | // "/path/to/app.js"; {
72 | // "type": "script",
73 | // "weight": 10
74 | // }
75 | // },
76 | // "/page.html": {
77 | // ...
78 | // }
79 | // }
80 |
81 | // TODO: node 4.1.2 doesn't support default function args yet.
82 | constructor(opts) {
83 | opts = opts || {};
84 |
85 | this.name = opts.name || this.DEFAULT_MANIFEST_NAME;
86 | this.basePath = opts.basePath;
87 | this.inputPath = opts.inputPath;
88 |
89 | if (this.basePath && this.inputPath) {
90 | this.resourceList = new ResourceList(opts);
91 | }
92 | }
93 |
94 | get DEFAULT_MANIFEST_NAME() {
95 | return 'push_manifest.json';
96 | }
97 |
98 | get PUSH_PRIORITY() {
99 | return 1; // TODO: this gives every resource priority 1.
100 | }
101 |
102 | /**
103 | * Generates the JSON format listing the inputs resources.
104 | *
105 | * @return {Promise({urls, fileContent}) The list of urls and generated JSON.
106 | */
107 | generate() {
108 | if (!this.resourceList) {
109 | console.warn('Cannot generate resources. You did not provide an inputPath/basePath.');
110 | }
111 |
112 | return this.resourceList.list().then(urls => {
113 | console.log(`Found ${urls.length} resource URLs in ${this.inputPath}:`);
114 |
115 | /* jshint ignore:start */
116 | for (let i = 0, url; url = urls[i]; ++i) {
117 | console.log(' ', url);
118 | }
119 | /* jshint ignore:end */
120 |
121 | let priorityMapping = {};
122 |
123 | urls.map((url, i) => {
124 | priorityMapping[url] = {
125 | weight: this.PUSH_PRIORITY
126 | };
127 | var type = EXTENSION_TO_TYPE[path.extname(url)];
128 | priorityMapping[url].type = type ? type : '';
129 | });
130 |
131 | this.fileContent = priorityMapping;
132 |
133 | // TODO: node 4.2.1 doesnt support ... spread operator.
134 | return {urls: urls, file: this.fileContent};
135 | });
136 | }
137 |
138 | /**
139 | * Writes the manifest.
140 | *
141 | * @param {Promise} Optional file content to write. By default, the content
142 | * from calling generate() is used.
143 | */
144 | write(opt_fileContent) {
145 | let fileContent = opt_fileContent || this.fileContent;
146 | fs.writeFile(this.name, JSON.stringify(fileContent, null, 2), err => {
147 | if (err) {
148 | return console.log(err);
149 | }
150 | });
151 | }
152 |
153 | }
154 |
155 | module.exports = PushManifest;
156 |
--------------------------------------------------------------------------------
/bin/http2-push-manifest:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Copyright 2015 Google Inc. All rights reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | // jshint node: true
20 | 'use strict';
21 |
22 | let nopt = require('nopt');
23 | let path = require('path');
24 | let Manifest = require('../lib/manifest');
25 | let pkg = require('../package.json');
26 |
27 | const SCRIPT_NAME = path.basename(__filename);
28 |
29 | const help = `${SCRIPT_NAME}: Generate a list of static resources for http2 push.
30 |
31 | Usage:
32 | ${SCRIPT_NAME} -f path/to/file.html
33 | ${SCRIPT_NAME} -f path/to/file.html -f path/to/file2.html ...
34 |
35 | Options:
36 | -h|--help: print this message
37 | -v|--version: print version number
38 | -f|--file: file to discover resources in. Use multiple times to produce a mult-file manifest format.
39 | -m|--manifest : Custom filename for the manifest file
40 |
41 | Examples:
42 |
43 | List all of the resources used in app/index.html, including sub-HTML Imports:
44 |
45 | ${SCRIPT_NAME} -f app/index.html
46 |
47 | {
48 | "/css/app.css": {
49 | "type": "style",
50 | "weight": 1
51 | },
52 | "/js/app.js": {
53 | "type": "script",
54 | "weight": 1
55 | },
56 | ...
57 | }
58 |
59 | List all the resources used in static/elements/elements.html:
60 |
61 | ${SCRIPT_NAME} -f static/elements elements.html
62 |
63 | List all the resources app/index.html and page.html, and combine into a singe manifest:
64 |
65 | ${SCRIPT_NAME} -f app/index.html -f page.html
66 |
67 | {
68 | "index.html": {
69 | "/css/app.css": {
70 | "type": "style",
71 | "weight": 1
72 | },
73 | ...
74 | },
75 | "page.html": {
76 | "/css/page.css": {
77 | "type": "style",
78 | "weight": 1
79 | },
80 | ...
81 | }
82 | }
83 |
84 | Using a custom filename:
85 |
86 | ${SCRIPT_NAME} -f path/to/site/index.html -m push.json
87 | ${SCRIPT_NAME} -f path/to/site/index.html --manifest push.json
88 | `;
89 |
90 | function printHelp() {
91 | console.log(help);
92 | }
93 |
94 | function printVersion() {
95 | console.log(`${SCRIPT_NAME}:`, pkg.version);
96 | }
97 |
98 | function notifyIfUpdateAvailable() {
99 | try {
100 | let updateNotifier = require('update-notifier');
101 | updateNotifier({pkg: pkg}).notify();
102 | } catch(e) {
103 | // noop
104 | }
105 | }
106 |
107 | let jsonOutput = {};
108 |
109 | function writeManifest(manifest, opt_content) {
110 | manifest.write(opt_content);
111 | console.log(`Wrote ${manifest.name}`);
112 | }
113 |
114 | function generateManifest(manifestName, files, singleFile) {
115 | if (!files.length) {
116 | let manifest = new Manifest({name: manifestName});
117 | writeManifest(manifest, jsonOutput);
118 | return;
119 | }
120 |
121 | let f = files[0];
122 |
123 | // Make a path if one wasn't given. e.g. basic.html -> ./basic.html
124 | if (f.indexOf(path.sep) === -1) {
125 | f = `.${path.sep}${f}`;
126 | }
127 |
128 | let basePath = f.slice(0, f.lastIndexOf(path.sep))
129 | let inputPath = f.slice(f.lastIndexOf(path.sep) + 1);
130 |
131 | if (!basePath || !inputPath) {
132 | printHelp();
133 | process.exit(1);
134 | }
135 |
136 | let manifest = new Manifest({basePath, inputPath, name: manifestName});
137 | manifest.generate().then(output => {
138 | if (singleFile) {
139 | writeManifest(manifest);
140 | return;
141 | }
142 |
143 | jsonOutput[inputPath] = output.file;
144 |
145 | // Remove processed file from list and proceed with next.
146 | files.shift();
147 | generateManifest(manifestName, files, singleFile);
148 | }).catch(err => {
149 | console.warn(err);
150 | });
151 | }
152 |
153 | let args = nopt({
154 | help: Boolean,
155 | version: Boolean,
156 | manifest: String,
157 | file: [String, Array]
158 | }, {
159 | 'h': ['--help'],
160 | 'v': ['--version'],
161 | 'm': ['--manifest'],
162 | 'f': ['--file']
163 | });
164 |
165 | let files = args.file || [];
166 | let manifestName = args.manifest;
167 | // let basePath = args.argv.remain[0];
168 | // let inputPath = args.argv.remain[1];
169 |
170 | if (args.version) {
171 | printVersion();
172 | process.exit(0);
173 | }
174 |
175 | if (args.help || !files.length) {
176 | printHelp();
177 | process.exit(0);
178 | }
179 |
180 | notifyIfUpdateAvailable(); // Let user know if there's a newer version.
181 |
182 | generateManifest(manifestName, files, files.length < 2);
183 |
184 |
185 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![NPM version][npm-image]][npm-url]
2 | [![Build status][travis-image]][travis-url]
3 | [![Dependency Status][daviddm-image]][daviddm-url]
4 | [![License][license-image]][license-url]
5 |
6 | > A utility script for doing http2 push and/or preload.
7 |
8 | Generates a list of **local static resources** used in your web app by outputting a json
9 | file. This file can be read by your web server to more easily construct the
10 | appropriate `Link: ; rel=preload; as=` headers(s) for http2 push/preload.
11 |
12 | ## Install
13 |
14 | npm install --save-dev http2-push-manifest
15 |
16 | ## Run tests
17 |
18 | npm run test
19 |
20 | ## What's a push manifest?
21 |
22 | > A **manifest is not required by the HTTP2 protocol**. We need it up! We found that it
23 | is useful for telling your server what critical resources to push for the requesting page.
24 |
25 | `http2-push-manifest` is a Node script for generating a JSON file listing
26 | all of the static resources used on a page. It tries to discover the resources
27 | in an .html file you specify. This file can be read by your web server to more
28 | easily construct the appropriate `Link: ; rel=preload` headers(s) used in
29 | HTTP2 push. Since all the resources are discovered, you'll almost certainly want
30 | to **prune the list of files that get pushed**. Pushing too much can actually [hurt
31 | page load performance](https://twitter.com/ebidel/status/761016996339134464).
32 |
33 | By default, the script generates `push_manifest.json` in the top level directory
34 | of your app with a mapping of `: `. Feel free to add/remove
35 | URLs from this list as necessary for your app or change the priority level.
36 |
37 | Example of generated `push_manifest.json` with discovered local resources:
38 |
39 | {
40 | "/css/app.css": {
41 | "type": "style",
42 | "weight": 1
43 | },
44 | "/js/app.js": {
45 | "type": "script",
46 | "weight": 1
47 | },
48 | "/bower_components/webcomponentsjs/webcomponents-lite.js": {
49 | "type": "script",
50 | "weight": 1
51 | },
52 | "/bower_components/iron-selector/iron-selection.html": {
53 | "type": "document",
54 | "weight": 1
55 | },
56 | ...
57 | "/elements.html": {
58 | "type": "document",
59 | "weight": 1
60 | },
61 | "/elements.vulcanize.html": {
62 | "type": "document",
63 | "weight": 1
64 | }
65 | }
66 |
67 | **Note**: as of now, no browser implements control over the priority/weight level.
68 |
69 | ## Examples
70 |
71 | **Example** - list all the static resources of `app/index.html` (including sub-HTML Imports):
72 |
73 | http2-push-manifest -f app/index.html
74 |
75 | A single file produces the "single-file manifest format":
76 |
77 | {
78 | "/css/app.css": {
79 | "type": "style",
80 | "weight": 1
81 | },
82 | "/js/app.js": {
83 | "type": "script",
84 | "weight": 1
85 | },
86 | ...
87 | }
88 |
89 | **Example** - list all the resources in `static/elements/elements.html`:
90 |
91 | http2-push-manifest -f static/elements/elements.html
92 |
93 | **Example** - list all the resources app/index.html and page.html, and combine
94 | into a singe manifest:
95 |
96 | http2-push-manifest -f app/index.html -f page.html
97 |
98 | Using multiple files produces the "multi-file manifest format". Each key is the file
99 | and it's sub-objects are the found resources. It would be up to your server to
100 | decide how the mappings of key -> actual URL work.
101 |
102 | {
103 | "index.html": {
104 | "/css/app.css": {
105 | "type": "style",
106 | "weight": 1
107 | },
108 | ...
109 | },
110 | "page.html": {
111 | "/css/page.css": {
112 | "type": "style",
113 | "weight": 1
114 | },
115 | ...
116 | }
117 | }
118 |
119 | **Example** - using a custom manifest filename:
120 |
121 | http2-push-manifest -f path/to/site/index.html -m push.json
122 | http2-push-manifest -f path/to/site/index.html --manifest push.json
123 |
124 | ## Usage on App Engine
125 |
126 | If you're using App Engine for your server, check out [http2push-gae](https://github.com/GoogleChrome/http2push-gae). It leverages this manifest file format and automagically reads
127 | `push_manifest.json`, setting the `Link: rel="preload"` header for you.
128 |
129 | Simply decorate your request handler like so:
130 |
131 | ```python
132 | class Handler(http2.PushHandler):
133 |
134 | @http2push.push('push_manifest.json')
135 | def get(self):
136 | # Resources in push_manifest.json will be server-pushed with this handler.
137 | ```
138 |
139 | ## License
140 |
141 | [Apache 2.0](https://github.com/googlechrome/http2-push-manifest/blob/master/LICENSE) © 2015 Google Inc.
142 |
143 | [npm-url]: https://www.npmjs.com/package/http2-push-manifest
144 | [npm-image]: https://badge.fury.io/js/http2-push-manifest.svg
145 | [travis-url]: https://travis-ci.org/GoogleChrome/http2-push-manifest
146 | [travis-image]: https://travis-ci.org/GoogleChrome/http2-push-manifest.svg?branch=master
147 | [daviddm-url]: https://david-dm.org/GoogleChrome/http2-push-manifest
148 | [daviddm-image]: https://david-dm.org/GoogleChrome/http2-push-manifest.svg
149 | [license-image]: https://img.shields.io/npm/l/http2-push-manifest.svg
150 | [license-url]: LICENSE
151 |
--------------------------------------------------------------------------------
/test/tests.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // jshint node: true
4 | let assert = require('assert');
5 | let path = require('path');
6 | let fs = require('fs');
7 |
8 | const oldOld = console.log;
9 | const EXPECTED_FILE = {
10 | "/css/app.css": {
11 | "weight": 1,
12 | "type": "style"
13 | },
14 | "/js/app.js": {
15 | "weight": 1,
16 | "type": "script"
17 | },
18 | "/doesntexist.json": {
19 | "weight": 1,
20 | "type": "script"
21 | },
22 | "/api/endpoint": {
23 | "weight": 1
24 | },
25 | "/subimport.html": {
26 | "weight": 1,
27 | "type": "document"
28 | },
29 | "/import.html": {
30 | "weight": 1,
31 | "type": "document"
32 | }
33 | };
34 |
35 | function muteLogger() {
36 | console.log = function() {}
37 | }
38 |
39 | function unmuteLogger() {
40 | console.log = oldOld;
41 | }
42 |
43 | function listresources(manifest) {
44 | muteLogger();
45 | return manifest.generate().then(output => {
46 | manifest.write();
47 | unmuteLogger();
48 | return output;
49 | });
50 | }
51 |
52 | suite('manifest.js', () => {
53 |
54 | let PushManifest = require('../lib/manifest.js');
55 |
56 | let BASE = __dirname + '/html';
57 | let INPUT = 'basic.html';
58 |
59 | let manifest = null;
60 |
61 | suiteSetup(() => {
62 | manifest = new PushManifest({basePath: BASE, inputPath: INPUT});
63 | });
64 |
65 | test('defaults', done => {
66 | assert.equal(manifest.DEFAULT_MANIFEST_NAME,
67 | 'push_manifest.json', 'default manifest file name');
68 | assert.equal(manifest.name, manifest.DEFAULT_MANIFEST_NAME,
69 | 'default manifest file set');
70 | assert.equal(manifest.basePath, BASE, 'basePath set');
71 | assert.equal(manifest.inputPath, INPUT, 'default inputPath set');
72 | done();
73 | });
74 |
75 | test('list resources', done => {
76 |
77 | listresources(manifest).then(output => {
78 | let name = manifest.DEFAULT_MANIFEST_NAME;
79 | let urls = Object.keys(EXPECTED_FILE);
80 |
81 | assert.equal(output.urls.length, urls.length, 'found all resources');
82 |
83 | fs.readFile(name, (err, data) => {
84 | if (err) {
85 | throw err;
86 | }
87 |
88 | var json = JSON.parse(data);
89 |
90 | assert.equal(JSON.stringify(json), JSON.stringify(output.file),
91 | 'Written file does not match .file property');
92 |
93 |
94 | assert(!json['https://example.com/json'], 'External files are left out');
95 | assert(EXPECTED_FILE['/doesntexist.json'], 'non-existent local resources are included');
96 | assert(EXPECTED_FILE['/api/endpoint'], 'url without file extension is included');
97 |
98 | // Node 4.2.1 doesn't support ...[] yet. Built ourself.
99 | let arr = urls.concat(Object.keys(json));
100 | let union = new Set(arr);
101 | assert.equal(union.size, urls.length, 'all resources written to file');
102 |
103 | // Node 4.2.1 doesn't support for...of
104 | for (let key in json) {
105 | assert('type' in json[key], '.type property exists for all urls');
106 | if (key === '/api/endpoint') {
107 | assert.equal(json[key].type, '', '.type is empty for urls without file extensions');
108 | }
109 | }
110 |
111 | fs.unlinkSync(name); // cleanup
112 |
113 | done();
114 | });
115 | }).catch(err => {
116 | console.log(err);
117 | });
118 | });
119 |
120 | test('custom manifest', function(done) {
121 | this.timeout(2000);
122 |
123 | let name = 'custom_manifest.json';
124 | let manifest = new PushManifest({
125 | basePath: BASE, inputPath: INPUT, name: name
126 | });
127 |
128 | assert.equal(manifest.name, name, 'custom manifest file name set');
129 |
130 | listresources(manifest).then(output => {
131 | assert(fs.statSync(name).isFile(), 'custom manifest written');
132 | fs.unlinkSync(name); // cleanup
133 | done();
134 | });
135 |
136 | });
137 |
138 | });
139 |
140 | suite('cli', () => {
141 | var exec = require('child_process').exec;
142 |
143 | let CMD = `${__dirname}/../bin/http2-push-manifest`;
144 | let INPUT = `${__dirname}/html/basic.html`;
145 | let INPUT2 = `${__dirname}/html/basic2.html`;
146 | let NAME = 'push_manifest.json';
147 |
148 | function process(cmd, cb) {
149 | exec(cmd, (err, stdout, stderr) => {
150 | assert(!err, 'error running cli');
151 | cb(stdout);
152 | });
153 | }
154 |
155 | suiteSetup(() => {
156 | //manifest = new PushManifest({basePath: BASE, inputPath: INPUT});
157 | });
158 |
159 | test('single manifest', done => {
160 | process(`${CMD} -f ${INPUT}`, stdout => {
161 | assert(fs.statSync(NAME).isFile(), 'single file manifest written');
162 | fs.unlinkSync(NAME); // cleanup
163 | done();
164 | });
165 | });
166 |
167 | test('custom manifest', done => {
168 | let name = 'custom_manifest.json';
169 | process(`${CMD} -f ${INPUT} -m ${name}`, stdout => {
170 | assert(fs.statSync(name).isFile(), 'custom manifest written');
171 | fs.unlinkSync(name); // cleanup
172 | done();
173 | });
174 | });
175 |
176 | test('multi manifest', done => {
177 | process(`${CMD} -f ${INPUT} -f ${INPUT2}`, stdout => {
178 | assert(fs.statSync(NAME).isFile(), 'multi file manifest written');
179 |
180 | fs.readFile(NAME, (err, data) => {
181 | assert(!err, 'error reading multi file manifest');
182 |
183 | var json = JSON.parse(data);
184 |
185 | // Check top-level keys are the input file names.
186 | assert(path.basename(INPUT) in json);
187 | assert(path.basename(INPUT2) in json);
188 |
189 | fs.unlinkSync(NAME); // cleanup
190 |
191 | done();
192 | });
193 | });
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2015 Google Inc.
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------