UNPKG

9.37 kBJavaScriptView Raw
1'use strict';
2
3// Node.js built-ins
4
5var crypto = require('crypto');
6var fs = require('graceful-fs');
7var path = require('path');
8var url = require('url');
9
10// 3rd-party modules
11
12var AppCache = require('@jokeyrhyme/appcache');
13
14var mkdirp = require('mkdirp');
15var request = require('request');
16var temp = require('temp').track();
17
18// our modules
19
20var FetcherIndex = require(path.join(__dirname, 'lib', 'fetcher-index'));
21
22var doBrowserify = require(path.join(__dirname, 'lib', 'do-browserify'));
23var urlVars = require(path.join(__dirname, 'lib', 'url_variations'));
24
25// this module
26
27/**
28 * @constructor
29 * @param {Object} opts { remoteUrl: '', localPath: '' }
30 */
31function Fetcher (opts) {
32 this.date = new Date();
33 this.remoteUrl = opts.remoteUrl;
34 this.localPath = opts.localPath;
35 this.tempPath = '';
36 this.index = new FetcherIndex({ remoteUrl: this.remoteUrl });
37 this.manifestUrl = '';
38
39 this.transforms = {
40 css: [],
41 html: [],
42 js: []
43 };
44 this.extractors = {
45 manifestUrl: []
46 };
47
48 this.addExtractor('manifestUrl', require(path.join(__dirname, 'lib', 'extractors', 'manifestUrl.w3c')));
49
50 this.addTransform('css', require(path.join(__dirname, 'lib', 'transforms', 'css.localUrls')));
51 this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.removeManifest')));
52 this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.localLinkHrefs')));
53 this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.localScriptSrcs')));
54 this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.injectAppCacheIndex')));
55 this.addTransform('html', require(path.join(__dirname, 'lib', 'transforms', 'html.injectRequireJsShim')));
56}
57
58Fetcher.prototype.addExtractor = function (prop, fn) {
59 if (!Array.isArray(this.extractors[prop])) {
60 this.extractors[prop] = [];
61 }
62 this.extractors[prop].push(fn);
63};
64
65Fetcher.prototype.addTransform = function (ext, fn) {
66 if (!Array.isArray(this.transforms[ext])) {
67 this.transforms[ext] = [];
68 }
69 this.transforms[ext].push(fn);
70};
71
72Fetcher.prototype.afterTempPath = function () {
73 var me = this;
74
75 return new Promise(function (resolve, reject) {
76 temp.mkdir('appcache-fetcher-' + me.date.valueOf(), function (err, dirPath) {
77 if (err) {
78 reject(err);
79 return;
80 }
81 me.tempPath = dirPath;
82 resolve(dirPath);
83 });
84 });
85};
86
87Fetcher.prototype.afterLocalPath = function () {
88 var me = this;
89
90 return new Promise(function (resolve, reject) {
91 mkdirp(me.localPath, function (err) {
92 if (err) {
93 reject(err);
94 return;
95 }
96 resolve(me.localPath);
97 });
98 });
99};
100
101Fetcher.prototype.readFile = function (filePath) {
102 return new Promise(function (resolve, reject) {
103 fs.readFile(filePath, { encoding: 'utf8' }, function (err, contents) {
104 if (err) {
105 console.error(err);
106 reject(err);
107 return;
108 }
109 resolve(contents);
110 });
111 });
112};
113
114Fetcher.prototype.writeFile = function (filePath, contents) {
115 return new Promise(function (resolve, reject) {
116 fs.writeFile(filePath, contents, function (err) {
117 if (err) {
118 console.error(err);
119 reject(err);
120 return;
121 }
122 resolve();
123 });
124 });
125};
126
127Fetcher.prototype.generateAppCacheIndexShim = function () {
128 var addPath = path.join(__dirname, 'lib', 'app-cache-index.js');
129 var filePath = path.join(this.localPath, 'appCacheIndex.js');
130 var options = {
131 paths: [ this.localPath ],
132 standalone: 'appCacheIndex'
133 };
134
135 return doBrowserify(addPath, filePath, options);
136};
137
138Fetcher.prototype.generateRequireShim = function () {
139 var addPath = path.join(__dirname, 'lib', 'require.load.js');
140 var filePath = path.join(this.localPath, 'require.load.js');
141
142 return doBrowserify(addPath, filePath, {});
143};
144
145Fetcher.prototype.download = function (remoteUrl, localPath) {
146 var me = this;
147 var filePath;
148 var filename;
149
150 if (Array.isArray(remoteUrl)) {
151 return Promise.all(remoteUrl.map(function (r) {
152 return me.download(r, localPath);
153 }));
154 }
155
156 console.log('download:', remoteUrl);
157
158 filename = me.generateLocalFilePath(remoteUrl);
159 filePath = path.join(localPath, filename);
160
161 return new Promise(function (resolve, reject) {
162 var reader, writer;
163
164 reader = request(remoteUrl)
165 .on('error', function (err) {
166 console.error(err);
167 reject(err);
168 })
169 .on('response', function (res) {
170 var errorMsg;
171 if (res.statusCode !== 200) {
172 errorMsg = remoteUrl + ' : ' + res.statusCode;
173 console.error(errorMsg);
174 reject(new Error(errorMsg));
175 return;
176 }
177 me.index.set(remoteUrl, filename);
178 });
179
180 writer = fs.createWriteStream(filePath)
181 .on('error', function (err) {
182 console.error(err);
183 reject(err);
184 })
185 .on('finish', function () {
186 resolve();
187 });
188
189 reader.pipe(writer);
190 });
191};
192
193Fetcher.prototype.generateLocalFilePath = function (remoteUrl) {
194 var parsed;
195 var hash;
196 if (remoteUrl === this.remoteUrl) {
197 return 'index.html';
198 }
199 if (remoteUrl === this.manifestUrl) {
200 return 'appcache.manifest';
201 }
202 parsed = url.parse(remoteUrl, true, true);
203 hash = crypto.createHash('sha1');
204 hash.update(parsed.pathname + parsed.search);
205 return hash.digest('hex') + path.extname(parsed.pathname);
206};
207
208Fetcher.prototype.persistFilesIndex = function () {
209 var content = JSON.stringify(this.index, null, 2);
210 var filePath = path.join(this.localPath, 'index.json');
211
212 return this.writeFile(filePath, content);
213};
214
215Fetcher.prototype.getManifestURL = function () {
216 var me = this;
217 var filePath = path.join(this.localPath, 'index.html');
218 var extractors = this.extractors.manifestUrl;
219
220 if (!Array.isArray(extractors) || !extractors.length) {
221 return Promise.reject(new Error('no manifestUrl extractors'));
222 }
223
224 return me.readFile(filePath)
225 .then(function (contents) {
226 var manifestUrl;
227 var e, eLength, extractor;
228 eLength = extractors.length;
229 for (e = 0; e < eLength; e++) {
230 extractor = extractors[e];
231 manifestUrl = extractor({
232 contents: contents,
233 remoteUrl: me.remoteUrl
234 });
235 if (manifestUrl) {
236 return Promise.resolve(manifestUrl);
237 }
238 }
239 return Promise.resolve('');
240 });
241};
242
243Fetcher.prototype.downloadAppCacheEntries = function () {
244 var me = this;
245 var appCache;
246 var remoteUrls;
247
248 delete require.cache[path.join(me.localPath, 'appcache.json')];
249 appCache = require(path.join(me.localPath, 'appcache.json'));
250
251 remoteUrls = appCache.cache.map(function (entry) {
252 return url.resolve(me.remoteUrl, entry.replace(/^\/\//, 'https://'));
253 });
254
255 return this.download(remoteUrls, me.localPath);
256};
257
258Fetcher.prototype.postProcessFile = function (filePath) {
259 var me = this;
260 var ext = path.extname(filePath).toLowerCase().replace('.', '');
261 var transforms = me.transforms[ext];
262 if (!Array.isArray(transforms) || !transforms.length) {
263 return Promise.resolve();
264 }
265 console.log('postProcessFile:', filePath.replace(process.cwd(), ''));
266 return this.readFile(filePath)
267 .then(function (contents) {
268 var transformedContents = contents;
269 var t, tLength, transform;
270 tLength = transforms.length;
271 for (t = 0; t < tLength; t++) {
272 transform = transforms[t];
273 transformedContents = transform({
274 contents: transformedContents,
275 filePath: filePath,
276 index: me.index
277 });
278 }
279 return Promise.resolve(transformedContents);
280 })
281 .then(function (contents) {
282 return me.writeFile(filePath, contents);
283 });
284};
285
286Fetcher.prototype.postProcessDownloads = function () {
287 var me = this;
288
289 return new Promise(function (resolve, reject) {
290 fs.readdir(me.localPath, function (err, files) {
291 if (err) {
292 console.error(err);
293 reject(err);
294 return;
295 }
296 Promise.all(files.map(function (file) {
297 return me.postProcessFile(path.join(me.localPath, file));
298 })).then(resolve, reject);
299 });
300 });
301};
302
303Fetcher.prototype.go = function () {
304 var me = this;
305
306 return Promise.all([
307 this.afterTempPath(),
308 this.afterLocalPath()
309 ])
310 .then(function () {
311 return me.download(me.remoteUrl, me.localPath);
312 })
313 .then(function () {
314 return me.getManifestURL();
315 })
316 .then(function (manifestUrl) {
317 me.manifestUrl = manifestUrl;
318 return me.download(me.manifestUrl, me.localPath);
319 })
320 .then(function () {
321 return me.readFile(path.join(me.localPath, 'appcache.manifest'));
322 })
323 .then(function (contents) {
324 var appCache = AppCache.parse(contents);
325 return me.writeFile(
326 path.join(me.localPath, 'appcache.json'),
327 JSON.stringify(appCache, null, 2)
328 );
329 })
330 .then(function () {
331 return me.downloadAppCacheEntries();
332 })
333 .then(function () {
334 return me.persistFilesIndex();
335 })
336 .then(function () {
337 return me.postProcessDownloads();
338 })
339 .then(function () {
340 return Promise.all([
341 me.generateAppCacheIndexShim(),
342 me.generateRequireShim()
343 ]);
344 })
345 .then(null, function (err) {
346 console.error(err);
347 });
348};
349
350Fetcher.getURLVariationsOnQuery = urlVars.getURLVariationsOnQuery;
351Fetcher.getURLVariationsOnScheme = urlVars.getURLVariationsOnScheme;
352Fetcher.getURLVariations = urlVars.getURLVariations;
353
354module.exports = Fetcher;