1 | var path = require('path');
|
2 | var mkdirp = require('mkdirp');
|
3 | var async = require('async');
|
4 | var fs = require('fs');
|
5 | var libInstrument = require('istanbul-lib-instrument');
|
6 | var SlJsInfra = require('sl-js-infra').SlJsInfra;
|
7 | var fileAndFolderUtils = require('../file-and-folder-utils');
|
8 | var utils = require('../utils');
|
9 | var ValidationUtils = SlJsInfra.Utils.ValidationUtils;
|
10 | var FileInstrumenter = require("./file-instrumenter");
|
11 | var MarkupFileInstrumenter = require("./markup-file-instrumenter");
|
12 | var IgnoredFilesHandler = SlJsInfra.IgnoredFilesHandler;
|
13 | var CiaEnvVars = SlJsInfra.SlEnvVars.CIA;
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | function 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 |
|
36 | BrowserInstrumenter.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 |
|
64 | BrowserInstrumenter.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 |
|
75 | BrowserInstrumenter.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 |
|
89 | BrowserInstrumenter.prototype._onFileCopied = function (inputFile) {
|
90 | this.files.notInstrumented.push(inputFile);
|
91 | }
|
92 |
|
93 |
|
94 | BrowserInstrumenter.prototype._onFileCopyError = function (inputFile, errorMsg) {
|
95 | this.files.withCopyErrors.push({
|
96 | file: inputFile,
|
97 | error: errorMsg
|
98 | })
|
99 | }
|
100 |
|
101 | BrowserInstrumenter.prototype._onFileWithInstrumentationError = function (inputFile, errorMsg) {
|
102 | this.files.withInstrumentationErrors.push({
|
103 | file: inputFile,
|
104 | error: errorMsg
|
105 | })
|
106 | }
|
107 |
|
108 | BrowserInstrumenter.prototype._onFileInstrumented = function (inputFile) {
|
109 | this.files.instrumented.push(inputFile)
|
110 | }
|
111 |
|
112 | BrowserInstrumenter.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 |
|
153 | return null;
|
154 | }
|
155 |
|
156 | function 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 |
|
166 | BrowserInstrumenter.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,
|
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 |
|
213 | BrowserInstrumenter.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 |
|
226 | BrowserInstrumenter.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 |
|
236 | BrowserInstrumenter.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 |
|
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 |
|
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 |
|
311 | BrowserInstrumenter.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 |
|
318 | BrowserInstrumenter.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 |
|
329 | module.exports = BrowserInstrumenter;
|