1 | 'use strict';
|
2 |
|
3 | const os = require('os');
|
4 | const url = require('url');
|
5 | const path = require('path');
|
6 | const fs = require('fs-extra');
|
7 | const {chain, first, map} = require('lodash');
|
8 | const Bluebird = require('bluebird');
|
9 | const got = require('got');
|
10 | const debug = require('debug')('critical:file');
|
11 | const mime = require('mime-types');
|
12 | const slash = require('slash');
|
13 | const oust = require('oust');
|
14 | const chalk = require('chalk');
|
15 | const tempy = require('tempy');
|
16 |
|
17 | const File = require('./vinyl-remote');
|
18 | const gc = require('./gc');
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | function normalizePath(str) {
|
26 | return process.platform === 'win32' ? slash(str) : str;
|
27 | }
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | function replaceAssetPaths(html, opts) {
|
37 |
|
38 | const destPath = opts.destFolder || (opts.dest && path.dirname(opts.dest)) || path.dirname(html.path);
|
39 | const destPathResolved = path.resolve(destPath);
|
40 | const baseResolved = path.resolve(opts.base);
|
41 |
|
42 | |
43 |
|
44 |
|
45 | return stylesheet => {
|
46 |
|
47 | const css = stylesheet.contents.toString().replace(/url\(['"]?([^'"\\)]+)['"]?\)/g, (match, assetPath) => {
|
48 |
|
49 | if (assetPath.startsWith('data:') || /(?:^\/)|(?::\/\/)/.test(assetPath)) {
|
50 | return match;
|
51 | }
|
52 |
|
53 |
|
54 | const stylesheetPath = path.dirname(stylesheet.path);
|
55 | const assetRelative = path.relative(baseResolved, path.resolve(path.join(stylesheetPath, assetPath)));
|
56 |
|
57 |
|
58 | const pathPrefixDefault = path.relative(destPathResolved, baseResolved);
|
59 |
|
60 | const pathPrefix = (typeof opts.pathPrefix === 'undefined') ? pathPrefixDefault : opts.pathPrefix;
|
61 |
|
62 | return normalizePath(match.replace(assetPath, path.join(pathPrefix, assetRelative)));
|
63 | });
|
64 |
|
65 | stylesheet.contents = Buffer.from(css);
|
66 | return stylesheet;
|
67 | };
|
68 | }
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | function getPenthouseUrl(opts, file) {
|
78 | if (opts.src && isExternal(opts.src)) {
|
79 | debug('Fetching remote html:', opts.src);
|
80 | return opts.src;
|
81 | }
|
82 |
|
83 | let filepath = path.resolve(file.path);
|
84 | if (!fs.existsSync(filepath)) {
|
85 | filepath = path.resolve(file.history[0]);
|
86 | }
|
87 |
|
88 | debug('Fetching local html:', `file://${filepath}`);
|
89 | return `file://${filepath}`;
|
90 | }
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | function isExternal(href) {
|
98 | return /(^\/\/)|(:\/\/)/.test(href);
|
99 | }
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | function temp(resp) {
|
107 | const contentType = resp.headers['content-type'];
|
108 | return Promise.resolve()
|
109 | .then(() => {
|
110 | const filepath = tempy.file({extension: mime.extension(contentType)});
|
111 | gc.addFile(filepath);
|
112 | return fs.outputFile(filepath, resp.body).then(() => {
|
113 | return filepath;
|
114 | });
|
115 | });
|
116 | }
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 | const token = (user, pass) => Buffer.from([user, pass].join(':')).toString('base64');
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | function requestAsync(uri, secure = true, opts = {}) {
|
134 | const {user, pass, userAgent} = opts;
|
135 | let resourceUrl = uri;
|
136 |
|
137 | if (/^\/\//.test(uri)) {
|
138 |
|
139 | resourceUrl = url.resolve(`http${secure ? 's' : ''}://te.st`, uri);
|
140 | }
|
141 |
|
142 | debug(`Fetching resource: ${resourceUrl}`);
|
143 | const options = {rejectUnauthorized: false, headers: {}};
|
144 | if (user && pass) {
|
145 | options.headers.Authorization = `Basic ${token(user, pass)}`;
|
146 | }
|
147 |
|
148 | if (userAgent) {
|
149 | options.headers['User-Agent'] = userAgent;
|
150 | }
|
151 |
|
152 | return got(resourceUrl, options).catch(error => {
|
153 | if (secure) {
|
154 | debug(`${error.message} - trying again over http`);
|
155 | return requestAsync(uri, false, opts);
|
156 | }
|
157 |
|
158 | debug(`${resourceUrl} failed: ${error.message}`);
|
159 | return Promise.resolve(error);
|
160 | });
|
161 | }
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 | function guessBasePath(opts) {
|
169 | if (opts.src && !isExternal(opts.src) && !isVinyl(opts.src)) {
|
170 | return path.dirname(opts.src);
|
171 | }
|
172 |
|
173 | if (opts.src && isVinyl(opts.src)) {
|
174 | return opts.src.dirname;
|
175 | }
|
176 |
|
177 | return process.cwd();
|
178 | }
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 | function isVinyl(file) {
|
186 | return File.isVinyl(file) ||
|
187 | file instanceof File ||
|
188 | (file && /function File\(/.test(file.constructor.toString()) && file.contents && file.path);
|
189 | }
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | function assertLocal(filePath, opts = {}) {
|
198 | if (!isExternal(filePath)) {
|
199 | return new Bluebird(resolve => {
|
200 | resolve(filePath);
|
201 | });
|
202 | }
|
203 |
|
204 | return requestAsync(filePath, true, opts)
|
205 | .then(response => temp(response));
|
206 | }
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 | function resourcePath(htmlfile, opts) {
|
215 | return filepath => {
|
216 | if (isExternal(filepath)) {
|
217 | debug('resourcePath - remote', filepath);
|
218 | return filepath;
|
219 | }
|
220 |
|
221 | if (isExternal(htmlfile.history[0])) {
|
222 | debug('resourcePath - remote', htmlfile.history[0]);
|
223 |
|
224 | return url.resolve(htmlfile.history[0], filepath);
|
225 | }
|
226 |
|
227 | if (/(?:^\/)/.test(filepath)) {
|
228 | return path.join(opts.base, filepath.split('?')[0]);
|
229 | }
|
230 |
|
231 | const folder = path.relative(opts.base, path.dirname(htmlfile.path));
|
232 | if (folder) {
|
233 | debug('resourcePath - folder', folder);
|
234 | }
|
235 |
|
236 | return path.join(path.dirname(htmlfile.path), filepath.split('?')[0]);
|
237 | };
|
238 | }
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 | function generateSourcePath(opts) {
|
247 | const {html} = opts;
|
248 |
|
249 | if (typeof opts.src !== 'undefined') {
|
250 | return path.dirname(opts.src);
|
251 | }
|
252 |
|
253 | if (typeof opts.folder !== 'undefined') {
|
254 | const folder = path.isAbsolute(opts.folder) ? opts.folder : path.join(opts.base, opts.folder);
|
255 | opts.pathPrefix = path.relative(opts.folder, opts.base);
|
256 | return folder;
|
257 | }
|
258 |
|
259 | if (!opts.pathPrefix) {
|
260 | const links = oust(html, 'stylesheets');
|
261 |
|
262 | debug('generateSourcePath - links', links);
|
263 |
|
264 | const relative = chain(links).omitBy(link => {
|
265 | return link.startsWith('data:') || /(?:^\/)|(?::\/\/)/.test(link);
|
266 | }).toArray().value();
|
267 |
|
268 | debug('generateSourcePath - relative', relative);
|
269 |
|
270 | if (relative.length === 0) {
|
271 | process.stderr.write([
|
272 | chalk.red('Warning:'),
|
273 | 'Missing html source path. Consider \'folder\' option.',
|
274 | 'https://goo.gl/PwvFVb',
|
275 | os.EOL
|
276 | ].join(' '));
|
277 |
|
278 | opts.pathPrefix = '/';
|
279 | return opts.base;
|
280 | }
|
281 |
|
282 | const dots = map(relative, link => {
|
283 | const match = /^(\.\.\/)+/.exec(link);
|
284 | return first(match);
|
285 | });
|
286 |
|
287 | opts.pathPrefix = chain(dots).sortBy('length').last().value() || '';
|
288 | debug('generateSourcePath', opts.pathPrefix.replace(/\.\./g, '~'));
|
289 | }
|
290 |
|
291 | return path.join(opts.base, opts.pathPrefix.replace(/\.\./g, '~'));
|
292 | }
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 | function getVinylPromise(opts) {
|
302 | if (!(opts.src || opts.html) || !opts.base) {
|
303 | return Bluebird.reject(new Error('A valid source and base path are required.'));
|
304 | }
|
305 |
|
306 | if (isVinyl(opts.src)) {
|
307 | return new Bluebird(resolve => {
|
308 | resolve(opts.src);
|
309 | });
|
310 | }
|
311 |
|
312 | const file = new File({
|
313 | base: opts.base
|
314 | });
|
315 |
|
316 | if (opts.src && isExternal(opts.src)) {
|
317 | file.remotePath = opts.src;
|
318 | } else if (opts.src) {
|
319 | file.path = opts.src;
|
320 | }
|
321 |
|
322 | if (opts.html) {
|
323 | const folder = generateSourcePath(opts);
|
324 | debug('hacky source path folder', folder);
|
325 |
|
326 |
|
327 | return Promise.resolve()
|
328 | .then(() => {
|
329 | const filepath = tempy.file({extension: 'html'});
|
330 | file.path = filepath;
|
331 | file.path = path.join(folder, path.basename(filepath));
|
332 | file.base = folder;
|
333 | file.contents = Buffer.from(opts.html);
|
334 | debug(file);
|
335 | gc.addFile(filepath);
|
336 | return fs.outputFile(filepath, file.contents).then(() => {
|
337 | return file;
|
338 | });
|
339 | });
|
340 | }
|
341 |
|
342 |
|
343 | return assertLocal(opts.src, opts)
|
344 | .then(data => {
|
345 |
|
346 | if (opts.src !== path.resolve(data) && !isExternal(opts.src)) {
|
347 | file.path = path.join(opts.base, opts.src);
|
348 | } else {
|
349 | file.path = path.relative(process.cwd(), data);
|
350 | }
|
351 |
|
352 | return fs.readFile(file.path).then(contents => {
|
353 | file.contents = contents;
|
354 | return file;
|
355 | });
|
356 | });
|
357 | }
|
358 |
|
359 | exports.normalizePath = normalizePath;
|
360 | exports.isExternal = isExternal;
|
361 | exports.isVinyl = isVinyl;
|
362 | exports.replaceAssetPaths = replaceAssetPaths;
|
363 | exports.getPenthouseUrl = getPenthouseUrl;
|
364 | exports.guessBasePath = guessBasePath;
|
365 | exports.resourcePath = resourcePath;
|
366 | exports.assertLocal = assertLocal;
|
367 | exports.getVinylPromise = getVinylPromise;
|
368 | exports.token = token;
|