UNPKG

11.7 kBJavaScriptView Raw
1var path = require('path');
2var mkdirp = require('mkdirp');
3var async = require('async');
4var fs = require('fs');
5var libInstrument = require('istanbul-lib-instrument');
6var SlJsInfra = require('sl-js-infra').SlJsInfra;
7var fileAndFolderUtils = require('../file-and-folder-utils');
8var utils = require('../utils');
9var ValidationUtils = SlJsInfra.Utils.ValidationUtils;
10var FileInstrumenter = require("./file-instrumenter");
11var MarkupFileInstrumenter = require("./markup-file-instrumenter");
12var IgnoredFilesHandler = SlJsInfra.IgnoredFilesHandler;
13var CiaEnvVars = SlJsInfra.SlEnvVars.CIA;
14/**
15 * * This class responsible to instrument a set of files with Sealights information.
16 * @param {* Instrumentation options} opts
17 * @param {* A logger} logger
18 */
19function BrowserInstrumenter(opts, logger, slMapper) {
20 ValidationUtils.verifyNotNullOrEmpty(opts, "opts");
21 ValidationUtils.verifyNotNullOrEmpty(logger, "logger");
22 ValidationUtils.verifyNotNullOrEmpty(slMapper, "slMapper");
23
24 this.opts = opts;
25 this.logger = logger;
26 this.slMapper = slMapper;
27 this.files = {
28 instrumented: [],
29 notInstrumented: [],
30 withInstrumentationErrors: [],
31 withCopyErrors: []
32 }
33}
34
35
36BrowserInstrumenter.prototype.instrument = function (callback) {
37 ValidationUtils.verifyNotNullOrEmpty(callback, "callback");
38
39 try {
40 var err = this._validate(callback);
41 if (err) {
42 this.logger.error("Instrumentation failed due to invalid input. Error: ", err);
43 return callback(err);
44 }
45
46 var hasPhysicalFiles = this.opts.files && this.opts.files.length > 0;
47 if (!hasPhysicalFiles) {
48 this.logger.warn("Didn't find any files which require instrumentation. Skipping browser instrumentation.")
49 return callback();
50 }
51
52 this.logger.info("Starting instrumentation process.");
53 var _this = this;
54 this._processFiles(function () {
55 _this._onComplete(callback);
56 });
57 } catch (e) {
58 this.logger.error("Failed during instrumentation. Error: ", e);
59 return callback(e);
60 }
61
62}
63
64BrowserInstrumenter.prototype._printSummaries = function () {
65 var summary = "Instrumentation summary:" +
66 "\n\tInstrumented files: " + this.files.instrumented.length +
67 "\n\tFiles copied as is (not instrumented): " + this.files.notInstrumented.length +
68 "\n\tFiles with instrumentation errors: " + this.files.withInstrumentationErrors.length +
69 "\n\tFiles with copy errors: " + this.files.withCopyErrors.length;
70
71 this.logger.info(summary);
72}
73
74
75BrowserInstrumenter.prototype._onComplete = function (callback) {
76 var _this = this;
77 if (this.opts.copyAllFilesToOutput) {
78 fileAndFolderUtils.copyDir(this.opts.sourceRoot, this.opts.outputPath, this.opts.files, function (err) {
79 _this.logger.info("Finished copying all files to output. Error: ", err);
80 _this._printSummaries();
81 return callback(err);
82 });
83 } else {
84 _this._printSummaries();
85 return callback(null);
86 }
87}
88
89BrowserInstrumenter.prototype._onFileCopied = function (inputFile) {
90 this.files.notInstrumented.push(inputFile);
91}
92
93
94BrowserInstrumenter.prototype._onFileCopyError = function (inputFile, errorMsg) {
95 this.files.withCopyErrors.push({
96 file: inputFile,
97 error: errorMsg
98 })
99}
100
101BrowserInstrumenter.prototype._onFileWithInstrumentationError = function (inputFile, errorMsg) {
102 this.files.withInstrumentationErrors.push({
103 file: inputFile,
104 error: errorMsg
105 })
106}
107
108BrowserInstrumenter.prototype._onFileInstrumented = function (inputFile) {
109 this.files.instrumented.push(inputFile)
110}
111
112BrowserInstrumenter.prototype._validate = function (callback) {
113 var opts = this.opts;
114
115 if (opts.instrumentationType != "browser") {
116 return new Error("instrumentationType must be 'browser'");
117 }
118
119 if (!opts.extensionsToInstrument || opts.extensionsToInstrument.length === 0) {
120 return new Error("'extensionsToInstrument' must specified");
121 }
122
123 var hasPhysicalFiles = opts.files && opts.files.length > 0;
124
125 if (hasPhysicalFiles) {
126 if (!opts.outputPath) {
127 return new Error("outputPath was not specified");
128 }
129
130 if (!opts.sourceRoot) {
131 return new Error("sourceRoot was not specified");
132 }
133 }
134
135 if (!opts.server) {
136 return new Error("server was not specified");
137 }
138
139 if (!opts.token) {
140 return new Error("token was not specified");
141 }
142
143 if (!opts.customerId) {
144 return new Error("customerId was not specified");
145 }
146
147 if (!opts.workspacepath) {
148 return new Error("workspacepath was not specified");
149 }
150
151
152 //All good.
153 return null;
154}
155
156function addHtmlTag(preambleHeader, sourceUrl, loadedFlag) {
157 preambleHeader.push("if(!window.$Sealights." + loadedFlag + ") {",
158 " var script = document.createElement(\"script\");",
159 " script.type = \"text/javascript\";",
160 " script.src = \"" + sourceUrl + "\";",
161 " var head = document.head || document.getElementsByTagName && document.getElementsByTagName('head')[0]",
162 " if (head) { head.appendChild(script); window.$Sealights." + loadedFlag + " = true; } else { /* Unsupported/restricted browser */ }",
163 "}");
164}
165
166BrowserInstrumenter.prototype._createPreamble = function () {
167 var opts = this.opts;
168 var agentUrl = opts.server;
169 if (agentUrl[agentUrl.length] != '/')
170 agentUrl += '/';
171 agentUrl += 'v1/agents/browser/recommended?redirect=1&customerId=' + encodeURIComponent(opts.customerId);
172 var workspacePath = opts.workspacepath;
173 if (CiaEnvVars.getSourceRoot() != null){
174 workspacePath = CiaEnvVars.getSourceRoot()
175 this.logger.info("Overriding 'workspacepath' with 'Source Root'. Old: '" + opts.workspacepath + "', New: '" + workspacePath + "'.");
176 }
177 var lastChar = workspacePath[workspacePath.length - 1];
178 if (lastChar != "\\" && lastChar != "/") {
179 workspacePath = workspacePath + "/";
180 }
181 var preambleHeader = [
182 "if (!window.$Sealights) window.$Sealights = " + JSON.stringify({
183 customerId: opts.customerId,
184 appName: opts.appName,
185 buildName: opts.build,
186 branchName: opts.branch,
187 server: opts.server,
188 token: opts.token,
189 buildSessionId: opts.buildsessionid,
190 labId: opts.labId,
191 enabled: true,
192 workspacepath: workspacePath,
193 maxItemsInQueue: 500,
194 registerShutdownHook: true,
195 interval: 10, //in seconds
196 resolveWithoutHash: opts.resolveWithoutHash,
197 delayShutdownInSeconds: opts.delayShutdownInSeconds,
198 isUseNewUniqueId: SlJsInfra.SlEnvVars.isUseNewUniqueId()
199 }, null, ' ') + ";"];
200 if(this.slMapper && opts.slMappingUrl){
201 this.slMapper.writeToFile();
202 addHtmlTag(preambleHeader, opts.slMappingUrl, "slMappingAdded");
203 }
204
205 if (opts.downloadAgent == true) {
206 addHtmlTag(preambleHeader, agentUrl, "scriptAdded");
207 }
208
209
210 return preambleHeader;
211}
212
213BrowserInstrumenter.prototype._createInstrumenter = function () {
214 var config = {
215 coverageVariable: "$SealightsCoverage",
216 embedSource: false,
217 compact: true,
218 preserveComments: true,
219 esModules: !!this.opts.esModules
220 }
221 var instrumenter = libInstrument.createInstrumenter(config);
222
223 return instrumenter;
224}
225
226BrowserInstrumenter.prototype._createFileInstrumentor = function( preambleHeader, name, slIgnoreWrapper) {
227 var extension = path.extname(name);
228 if(utils.isMarkupFile(extension)){
229 this.logger.debug("'MarkupFileInstrumenter' is active");
230 return new MarkupFileInstrumenter(this.opts, preambleHeader, this.instrumenter, name, slIgnoreWrapper, this.logger)
231 }
232 this.logger.debug("'FileInstrumenter' is active");
233 return new FileInstrumenter(this.opts, preambleHeader, this.instrumenter, name, slIgnoreWrapper, this.logger);
234}
235
236BrowserInstrumenter.prototype._processFiles = function (callback) {
237 var inputDir = this.opts.sourceRoot;
238 var outputDir = this.opts.outputPath;
239 var relativeNames = this.opts.files;
240 var preambleHeader = this._createPreamble()
241 this.instrumenter = this._createInstrumenter();
242 this.logger.info("Starting to process files. Number of files to process: " + relativeNames.length);
243 var IgnoredFilesHandler = this._createIgnoreHandler(SlJsInfra.IgnoredFilesHandler.SCAN_IGNORE_FILE_NAME);
244
245 var extensions = {};
246 this.opts.extensionsToInstrument.map(function(ext){
247 extensions[ext] = true;
248 });
249
250
251 this.opts.extensionsToInstrument = extensions;
252 this.opts.onFileWithInstrumentationError = this._onFileWithInstrumentationError.bind(this);
253 this.opts.onFileCopied = this._onFileCopied.bind(this);
254 this.opts.onFileInstrumented = this._onFileInstrumented.bind(this);
255 this.opts.onFileCopyError = this._onFileCopyError.bind(this);
256 var _this = this;
257
258 //Verify the output folder exists
259 if(this.opts.outputPath) {
260 mkdirp.sync(this.opts.outputPath);
261 }
262 var processor = function (name, callback) {
263 var fi = _this._createFileInstrumentor(preambleHeader, name, IgnoredFilesHandler);
264 fi.processSingleFile(callback);
265 },
266
267 q = async.queue(processor, 10),
268 errors = [],
269 count = 0,
270 startTime = new Date().getTime();
271
272 q.push(relativeNames, function (err, name) {
273 var inputFile, outputFile;
274 inputFile = path.resolve(inputDir, name);
275 outputFile = path.resolve(outputDir, name);
276 if (err) {
277 errors.push({
278 file: name,
279 error: err.message || err.toString()
280 });
281 fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
282 _this.logger.warn("Had error while processing '" + inputFile + "'. File is copied as is. ");
283 } else {
284 _this.logger.debug("Processed '" + inputFile + "'.")
285 }
286 // WTF??
287 var verbose = false;
288 if (verbose) {
289 console.log('Processed: ' + name);
290 } else {
291 if (count % 100 === 0) {
292 process.stdout.write('.');
293 }
294 }
295 count += 1;
296 });
297
298 q.drain = function () {
299 _this.logger.info("Draining the Q.");
300 var endTime = new Date().getTime();
301 _this.logger.info('\nProcessed [' + count + '] files in ' + Math.floor((endTime - startTime) / 1000) + ' secs');
302 if (errors.length > 0) {
303 _this.logger.warn('The following ' + errors.length + ' file(s) had errors and were copied as-is:\n' + _this._formatErrorMessage(errors));
304 }
305 if (callback) {
306 callback();
307 }
308 };
309}
310
311BrowserInstrumenter.prototype._createIgnoreHandler = function (filename) {
312 var workspacePath = this.opts.workspacepath;
313 var slIgnoreScanFilePath = fileAndFolderUtils.findFileRecursively(filename, workspacePath, true, this.logger);
314 var slIgnore = new IgnoredFilesHandler(slIgnoreScanFilePath, this.opts.outputpath, filename, this.logger);
315 return slIgnore;
316}
317
318BrowserInstrumenter.prototype._formatErrorMessage = function (fileWithErrors) {
319 var messageFormatter = new SlJsInfra.MessageFormatter();
320 var counter = 1;
321 fileWithErrors.forEach(function (f) {
322 var msg = "[" + counter++ + "] '" + f.file + "'. Error: '" + f.error;
323 messageFormatter.addContent(msg);
324 })
325
326 return messageFormatter.content;
327}
328
329module.exports = BrowserInstrumenter;