@teamscale/javascript-instrumenter 0.0.1-beta.9 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -22,10 +26,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
27
  };
24
28
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.expandToFileSet = exports.isDirectoryEmpty = exports.ensureExistingDirectory = exports.isExistingDirectory = exports.isExistingFile = void 0;
29
+ exports.sourceMapFromMapFile = exports.expandToFileSet = exports.isDirectoryEmpty = exports.findSubFolders = exports.ensureExistingDirectory = exports.isExistingDirectory = exports.isExistingFile = void 0;
26
30
  const commons_1 = require("@cqse/commons");
27
31
  const fs = __importStar(require("fs"));
28
- const mkdirp_1 = __importDefault(require("mkdirp"));
32
+ const mkdirp = __importStar(require("mkdirp"));
29
33
  const path_1 = __importDefault(require("path"));
30
34
  const glob_1 = require("glob");
31
35
  /**
@@ -47,13 +51,32 @@ exports.isExistingDirectory = isExistingDirectory;
47
51
  */
48
52
  function ensureExistingDirectory(path) {
49
53
  if (!fs.existsSync(path)) {
50
- mkdirp_1.default.sync(path);
54
+ mkdirp.sync(path);
51
55
  }
52
56
  if (!fs.lstatSync(path).isDirectory()) {
53
57
  throw new commons_1.InvalidConfigurationException(`The specified path '${path}' does not point to an existing directory!`);
54
58
  }
55
59
  }
56
60
  exports.ensureExistingDirectory = ensureExistingDirectory;
61
+ /**
62
+ * Given a root folder find a folder with a given name.
63
+ */
64
+ function findSubFolders(startFromFolder, folderName) {
65
+ const matches = [];
66
+ const entries = fs.readdirSync(startFromFolder, { withFileTypes: true });
67
+ for (const entry of entries) {
68
+ const fullPath = path_1.default.join(startFromFolder, entry.name);
69
+ if (entry.name === folderName && entry.isDirectory()) {
70
+ matches.push(fullPath);
71
+ }
72
+ if (entry.isDirectory()) {
73
+ const nestedMatches = findSubFolders(fullPath, folderName);
74
+ matches.push(...nestedMatches);
75
+ }
76
+ }
77
+ return matches;
78
+ }
79
+ exports.findSubFolders = findSubFolders;
57
80
  /**
58
81
  * Is the given directory empty?
59
82
  */
@@ -80,3 +103,13 @@ function expandToFileSet(toExpand) {
80
103
  return glob_1.glob.sync(globPattern, { nodir: true });
81
104
  }
82
105
  exports.expandToFileSet = expandToFileSet;
106
+ /**
107
+ * Read a source map from a source map file.
108
+ *
109
+ * @param mapFilePath
110
+ */
111
+ function sourceMapFromMapFile(mapFilePath) {
112
+ const content = fs.readFileSync(mapFilePath, 'utf8');
113
+ return JSON.parse(content);
114
+ }
115
+ exports.sourceMapFromMapFile = sourceMapFromMapFile;
@@ -1,6 +1,8 @@
1
- import { CollectorSpecifier, InstrumentationTask, OriginSourcePattern, TaskElement, TaskResult } from './Task';
2
- import { Logger } from 'winston';
3
- export declare const IS_INSTRUMENTED_TOKEN = "/** $IS_JS_PROFILER_INSTRUMENTED=true **/";
1
+ import { CollectorSpecifier, FileExcludePattern, InstrumentationTask, OriginSourcePattern, SourceMapReference, TaskElement, TaskResult } from './Task';
2
+ import { RawSourceMap, SourceMapConsumer } from 'source-map';
3
+ import { Optional } from 'typescript-optional';
4
+ import Logger from 'bunyan';
5
+ export declare const IS_INSTRUMENTED_TOKEN = "$IS_JS_PROFILER_INSTRUMENTED=true";
4
6
  /**
5
7
  * An instrumenter that can conduct a {@code InstrumentationTask}.
6
8
  */
@@ -17,16 +19,16 @@ export interface IInstrumenter {
17
19
  */
18
20
  export declare class IstanbulInstrumenter implements IInstrumenter {
19
21
  /**
20
- * The path to the vaccine to inject. The vaccine is a JavaScript
22
+ * The vaccine to inject. The vaccine is a JavaScript
21
23
  * file with the code to forward the coverage information
22
24
  * produced by the Istanbul instrumentation.
23
25
  */
24
- private readonly vaccineFilePath;
26
+ private readonly vaccineSource;
25
27
  /**
26
28
  * The logger instance to log to.
27
29
  */
28
30
  private logger;
29
- constructor(vaccineFilePath: string, logger: Logger);
31
+ constructor(vaccineFilePath: string, logger: Logger, collector: CollectorSpecifier);
30
32
  /**
31
33
  * {@inheritDoc #IInstrumenter.instrument}
32
34
  */
@@ -36,24 +38,19 @@ export declare class IstanbulInstrumenter implements IInstrumenter {
36
38
  *
37
39
  * @param collector - The collector to send the coverage information to.
38
40
  * @param taskElement - The task element to perform the instrumentation for.
41
+ * @param excludeBundles - A exclude pattern to restrict which bundles should be instrumented
39
42
  * @param sourcePattern - A pattern to restrict the instrumentation to only a fraction of the task element.
43
+ * @param dumpOriginsFile - A file path where all origins from the source map should be dumped in json format, or undefined if no origins should be dumped
40
44
  */
41
- instrumentOne(collector: CollectorSpecifier, taskElement: TaskElement, sourcePattern: OriginSourcePattern): TaskResult;
45
+ instrumentOne(taskElement: TaskElement, excludeBundles: FileExcludePattern, sourcePattern: OriginSourcePattern, dumpOriginsFile: string | undefined): Promise<TaskResult>;
46
+ private instrumentBundle;
47
+ private writeBundleFile;
42
48
  /**
43
49
  * Loads the vaccine from the vaccine file and adjusts some template parameters.
44
50
  *
45
51
  * @param collector - The collector to send coverage information to.
46
52
  */
47
53
  private loadVaccine;
48
- /**
49
- * Should the given file be excluded from the instrumentation,
50
- * based on the source files that have been transpiled into it?
51
- *
52
- * @param pattern - The pattern to match the origin source files.
53
- * @param sourceFile - The bundle file name.
54
- * @param originSourceFiles - The list of files that were transpiled into the bundle.
55
- */
56
- private shouldExcludeFromInstrumentation;
57
54
  /**
58
55
  * @returns whether the given file is supported for instrumentation.
59
56
  */
@@ -63,12 +60,31 @@ export declare class IstanbulInstrumenter implements IInstrumenter {
63
60
  * given task element.
64
61
  */
65
62
  private configurationAlternativesFor;
66
- /**
67
- * Given a source code file and the task element, load the corresponding sourcemap.
68
- *
69
- * @param inputSource - The source code that might contain sourcemap comments.
70
- * @param taskElement - The task element that can have a reference to an external sourcemap.
71
- */
72
- private loadInputSourceMap;
63
+ /** Appends all origins from the source map to a given file. Creates the file if it does not exist yet. */
64
+ private dumpOrigins;
65
+ /** Clears the dump origins file if it exists, such that it is now ready to be appended for every instrumented file. */
66
+ private clearDumpOriginsFileIfNeeded;
73
67
  }
68
+ /**
69
+ * Extract the sourcemap from the given source code.
70
+ *
71
+ * @param instrumentedSource - The source code.
72
+ * @param instrumentedSourceFileName - The file name to assume for the file name.
73
+ */
74
+ export declare function loadSourceMap(instrumentedSource: string, instrumentedSourceFileName: string): Promise<SourceMapConsumer | undefined>;
75
+ /**
76
+ * Given a source code file, load the corresponding sourcemap.
77
+ *
78
+ * @param inputSource - The source code that might contain sourcemap comments.
79
+ * @param taskFile - The name of the file the `inputSource` is from.
80
+ * @param externalSourceMapFile - An external source map file to consider.
81
+ */
82
+ export declare function loadInputSourceMap(inputSource: string, taskFile: string, externalSourceMapFile: Optional<SourceMapReference>): RawSourceMap | undefined;
83
+ /**
84
+ * Extract a sourcemap for a given code comment.
85
+ *
86
+ * @param sourcecode - The source code that is scanned for source map comments.
87
+ * @param sourceFilePath - The file name the code was loaded from.
88
+ */
89
+ export declare function sourceMapFromCodeComment(sourcecode: string, sourceFilePath: string): RawSourceMap | undefined;
74
90
  //# sourceMappingURL=Instrumenter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Instrumenter.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/Instrumenter.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EAEnB,WAAW,EACX,UAAU,EACV,MAAM,QAAQ,CAAC;AAOhB,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,eAAO,MAAM,qBAAqB,8CAA8C,CAAC;AAEjF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC3D;AAED;;GAEG;AACH,qBAAa,oBAAqB,YAAW,aAAa;IACzD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IAEzC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAS;gBAEX,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IASnD;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IAW1D;;;;;;OAMG;IACH,aAAa,CACZ,SAAS,EAAE,kBAAkB,EAC7B,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,mBAAmB,GAChC,UAAU;IAuFb;;;;OAIG;IACH,OAAO,CAAC,WAAW;IASnB;;;;;;;OAOG;IACH,OAAO,CAAC,gCAAgC;IAQxC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAK3B;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAcpC;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;CAW1B"}
1
+ {"version":3,"file":"Instrumenter.d.ts","sourceRoot":"","sources":["../../../src/instrumenter/Instrumenter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,kBAAkB,EAClB,kBAAkB,EAElB,mBAAmB,EACnB,mBAAmB,EAEnB,kBAAkB,EAElB,WAAW,EACX,UAAU,EACV,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAM7D,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAQ5B,eAAO,MAAM,qBAAqB,sCAAsC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CAC3D;AA0BD;;GAEG;AACH,qBAAa,oBAAqB,YAAW,aAAa;IACzD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC;;OAEG;IACH,OAAO,CAAC,MAAM,CAAS;gBAEX,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB;IAUlF;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IAqBhE;;;;;;;;OAQG;IACG,aAAa,CAClB,WAAW,EAAE,WAAW,EACxB,cAAc,EAAE,kBAAkB,EAClC,aAAa,EAAE,mBAAmB,EAClC,eAAe,EAAE,MAAM,GAAG,SAAS,GACjC,OAAO,CAAC,UAAU,CAAC;YAsER,gBAAgB;IA8C9B,OAAO,CAAC,eAAe;IA0BvB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IAMnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAS3B;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAepC,0GAA0G;IAC1G,OAAO,CAAC,WAAW;IASnB,uHAAuH;IACvH,OAAO,CAAC,4BAA4B;CASpC;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAClC,kBAAkB,EAAE,MAAM,EAC1B,0BAA0B,EAAE,MAAM,GAChC,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAUxC;AAsBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,qBAAqB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GACjD,YAAY,GAAG,SAAS,CAU1B;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAwC7G"}
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -18,135 +22,194 @@ var __importStar = (this && this.__importStar) || function (mod) {
18
22
  __setModuleDefault(result, mod);
19
23
  return result;
20
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
21
28
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.IstanbulInstrumenter = exports.IS_INSTRUMENTED_TOKEN = void 0;
29
+ exports.sourceMapFromCodeComment = exports.loadInputSourceMap = exports.loadSourceMap = exports.IstanbulInstrumenter = exports.IS_INSTRUMENTED_TOKEN = void 0;
23
30
  const Task_1 = require("./Task");
24
31
  const commons_1 = require("@cqse/commons");
25
- const istanbul = __importStar(require("istanbul-lib-instrument"));
32
+ const source_map_1 = require("source-map");
33
+ const istanbul = __importStar(require("@teamscale/lib-instrument"));
26
34
  const fs = __importStar(require("fs"));
35
+ const mkdirp = __importStar(require("mkdirp"));
27
36
  const path = __importStar(require("path"));
28
37
  const convertSourceMap = __importStar(require("convert-source-map"));
29
- exports.IS_INSTRUMENTED_TOKEN = '/** $IS_JS_PROFILER_INSTRUMENTED=true **/';
38
+ const typescript_optional_1 = require("typescript-optional");
39
+ const async_1 = __importDefault(require("async"));
40
+ const WebToolkit_1 = require("./WebToolkit");
41
+ const FileSystem_1 = require("./FileSystem");
42
+ exports.IS_INSTRUMENTED_TOKEN = '$IS_JS_PROFILER_INSTRUMENTED=true';
43
+ function readBundle(bundleContent, taskElement) {
44
+ const baseNameMatch = path.basename(taskElement.fromFile).match('^([a-zA-Z0-9]*).cache.js');
45
+ const directories = path.dirname(taskElement.fromFile).split(path.sep);
46
+ if (baseNameMatch && directories.length > 1) {
47
+ const fragmentId = (0, WebToolkit_1.determineGwtFileUid)(taskElement.fromFile);
48
+ const functionCall = bundleContent.match('^([^(]+)\\((.*)\\);*\\s*$');
49
+ if (fragmentId && functionCall && functionCall.length === 3) {
50
+ const callInfos = (0, WebToolkit_1.extractGwtCallInfos)(bundleContent);
51
+ if (callInfos) {
52
+ return {
53
+ type: 'gwt',
54
+ content: bundleContent,
55
+ fragmentId,
56
+ functionName: callInfos.functionName,
57
+ codeAsArrayArgument: callInfos.codeAsArrayArgument,
58
+ codeArguments: callInfos.codeArguments
59
+ };
60
+ }
61
+ }
62
+ }
63
+ return { type: 'javascript', content: bundleContent, codeArguments: [bundleContent] };
64
+ }
30
65
  /**
31
66
  * An instrumenter based on the IstanbulJs instrumentation and coverage framework.
32
67
  */
33
68
  class IstanbulInstrumenter {
34
- constructor(vaccineFilePath, logger) {
35
- this.vaccineFilePath = commons_1.Contract.requireNonEmpty(vaccineFilePath);
69
+ constructor(vaccineFilePath, logger, collector) {
36
70
  commons_1.Contract.require(fs.existsSync(vaccineFilePath), `The vaccine file to inject "${vaccineFilePath}" must exist!\nCWD:${process.cwd()}`);
71
+ this.vaccineSource = this.loadVaccine(commons_1.Contract.requireNonEmpty(vaccineFilePath), collector);
37
72
  this.logger = logger;
38
73
  }
39
74
  /**
40
75
  * {@inheritDoc #IInstrumenter.instrument}
41
76
  */
42
- instrument(task) {
43
- fs.existsSync(this.vaccineFilePath);
44
- // ATTENTION: Here is potential for parallelization. Maybe we can
45
- // run several instrumentation workers in parallel?
46
- const result = task.elements
47
- .map(e => this.instrumentOne(task.collector, e, task.originSourcePattern))
48
- .reduce((prev, current) => current.withIncrement(prev), Task_1.TaskResult.neutral());
49
- return Promise.resolve(result);
77
+ async instrument(task) {
78
+ this.clearDumpOriginsFileIfNeeded(task.dumpOriginsFile);
79
+ // We limit the number of instrumentations in parallel to one to
80
+ // not overuse memory (NodeJS has only limited mem to use).
81
+ return async_1.default
82
+ .mapLimit(task.elements, 1, async (taskElement) => {
83
+ return await this.instrumentOne(taskElement, task.excludeFilesPattern, task.originSourcePattern, task.dumpOriginsFile);
84
+ })
85
+ .then(results => {
86
+ return results.reduce((prev, curr) => {
87
+ return prev.withIncrement(curr);
88
+ }, Task_1.TaskResult.neutral());
89
+ });
50
90
  }
51
91
  /**
52
92
  * Perform the instrumentation for one given task element (file to instrument).
53
93
  *
54
94
  * @param collector - The collector to send the coverage information to.
55
95
  * @param taskElement - The task element to perform the instrumentation for.
96
+ * @param excludeBundles - A exclude pattern to restrict which bundles should be instrumented
56
97
  * @param sourcePattern - A pattern to restrict the instrumentation to only a fraction of the task element.
98
+ * @param dumpOriginsFile - A file path where all origins from the source map should be dumped in json format, or undefined if no origins should be dumped
57
99
  */
58
- instrumentOne(collector, taskElement, sourcePattern) {
59
- var _a, _b, _c;
60
- const inputFileSource = fs.readFileSync(taskElement.fromFile, 'utf8');
100
+ async instrumentOne(taskElement, excludeBundles, sourcePattern, dumpOriginsFile) {
101
+ // Not all file types are supported by the instrumenter
102
+ if (!this.isFileTypeSupported(taskElement.fromFile)) {
103
+ if (!taskElement.isInPlace()) {
104
+ copyToFile(taskElement.toFile, taskElement.fromFile);
105
+ }
106
+ return new Task_1.TaskResult(0, 0, 0, 0, 1, 0, 0);
107
+ }
108
+ // Read the (supported) file
109
+ const bundleContent = fs.readFileSync(taskElement.fromFile, 'utf8');
110
+ const inputBundle = readBundle(bundleContent, taskElement);
61
111
  // We skip files that we have already instrumented
62
- if (inputFileSource.startsWith(exports.IS_INSTRUMENTED_TOKEN)) {
112
+ if (inputBundle.content.substring(0, Math.min(inputBundle.content.length, 100)).includes(exports.IS_INSTRUMENTED_TOKEN)) {
63
113
  if (!taskElement.isInPlace()) {
64
- fs.writeFileSync(taskElement.toFile, inputFileSource);
114
+ writeToFile(taskElement.toFile, inputBundle.content);
65
115
  }
66
- return new Task_1.TaskResult(0, 0, 1, 0, 0, 0);
116
+ return new Task_1.TaskResult(0, 0, 0, 1, 0, 0, 0);
67
117
  }
68
- // Not all file types are supported by the instrumenter
69
- if (!this.isFileTypeSupported(taskElement.fromFile)) {
70
- return new Task_1.TaskResult(0, 0, 0, 1, 0, 0);
118
+ // We might want to skip the instrumentation of the file
119
+ if (excludeBundles.isExcluded(taskElement.fromFile)) {
120
+ if (!taskElement.isInPlace()) {
121
+ copyToFile(taskElement.toFile, taskElement.fromFile);
122
+ }
123
+ return new Task_1.TaskResult(0, 1, 0, 0, 0, 0, 0);
71
124
  }
72
125
  // Report progress
73
126
  this.logger.info(`Instrumenting "${path.basename(taskElement.fromFile)}"`);
74
- let finalSourceMap;
75
- let instrumentedSource;
127
+ // Load the bundles source maps
128
+ const inputSourceMaps = loadInputSourceMaps(taskElement.fromFile, inputBundle, taskElement.externalSourceMapFile);
129
+ if (inputSourceMaps.length === 0) {
130
+ return Task_1.TaskResult.warning(`Failed loading input source map for ${taskElement.fromFile}.`);
131
+ }
76
132
  // We try to perform the instrumentation with different
77
133
  // alternative configurations of the instrumenter.
78
134
  const configurationAlternatives = this.configurationAlternativesFor(taskElement);
79
135
  for (let i = 0; i < configurationAlternatives.length; i++) {
80
- let inputSourceMap;
136
+ const configurationAlternative = configurationAlternatives[i];
81
137
  try {
82
- const instrumenter = istanbul.createInstrumenter(configurationAlternatives[i]);
83
- inputSourceMap = this.loadInputSourceMap(inputFileSource, taskElement);
84
- // Based on the source maps of the file to instrument, we can now
85
- // decide if we should NOT write an instrumented version of it
86
- // and use the original code instead and write it to the target path.
87
- //
88
- if (this.shouldExcludeFromInstrumentation(sourcePattern, taskElement.fromFile, (_b = (_a = instrumenter.lastSourceMap()) === null || _a === void 0 ? void 0 : _a.sources) !== null && _b !== void 0 ? _b : [])) {
89
- fs.writeFileSync(taskElement.toFile, inputFileSource);
90
- return new Task_1.TaskResult(1, 0, 0, 0, 0, 0);
91
- }
92
- // The main instrumentation (adding coverage statements) is performed now:
93
- instrumentedSource = instrumenter
94
- .instrumentSync(inputFileSource, taskElement.fromFile, inputSourceMap)
95
- .replace(/return actualCoverage/g, 'return makeCoverageInterceptor(actualCoverage)')
96
- .replace(/new Function\("return this"\)\(\)/g, "typeof window === 'object' ? window : this");
97
- this.logger.debug('Instrumentation source maps to:', (_c = instrumenter.lastSourceMap()) === null || _c === void 0 ? void 0 : _c.sources);
98
- // The process also can result in a new source map that we will append in the result.
99
- //
100
- // `lastSourceMap` === Sourcemap for the last file that was instrumented.
101
- finalSourceMap = convertSourceMap.fromObject(instrumenter.lastSourceMap()).toComment();
102
- break;
138
+ return await this.instrumentBundle(taskElement, inputBundle, inputSourceMaps, configurationAlternative, dumpOriginsFile, sourcePattern);
103
139
  }
104
140
  catch (e) {
105
141
  // If also the last configuration alternative failed,
106
142
  // we emit a corresponding warning or signal an error.
107
143
  if (i === configurationAlternatives.length - 1) {
108
- if (!inputSourceMap) {
109
- return Task_1.TaskResult.warning(`Failed loading input source map for ${taskElement.fromFile}: ${e.message}`);
110
- }
111
- fs.writeFileSync(taskElement.toFile, inputFileSource);
144
+ writeToFile(taskElement.toFile, inputBundle.content);
112
145
  return Task_1.TaskResult.error(e);
113
146
  }
114
147
  }
115
148
  }
116
- // We now can glue together the final version of the instrumented file.
149
+ return new Task_1.TaskResult(0, 0, 0, 0, 0, 1, 0);
150
+ }
151
+ async instrumentBundle(taskElement, inputBundle, inputSourceMaps, configurationAlternative, dumpOriginsFile, sourcePattern) {
152
+ var _a;
153
+ // Based on the source maps of the file to instrument, we can now
154
+ // decide if we should NOT write an instrumented version of it
155
+ // and use the original code instead and write it to the target path.
117
156
  //
118
- const vaccineSource = this.loadVaccine(collector);
119
- fs.writeFileSync(taskElement.toFile, `${exports.IS_INSTRUMENTED_TOKEN} ${vaccineSource} ${instrumentedSource} \n${finalSourceMap}`);
120
- return new Task_1.TaskResult(1, 0, 0, 0, 0, 0);
157
+ for (const inputSourceMap of inputSourceMaps.filter(map => map)) {
158
+ const originSourceFiles = (_a = inputSourceMap === null || inputSourceMap === void 0 ? void 0 : inputSourceMap.sources) !== null && _a !== void 0 ? _a : [];
159
+ if (dumpOriginsFile) {
160
+ this.dumpOrigins(dumpOriginsFile, originSourceFiles);
161
+ }
162
+ }
163
+ // The main instrumentation (adding coverage statements) is performed now:
164
+ const instrumenter = istanbul.createInstrumenter(configurationAlternative);
165
+ const instrumentedSources = [];
166
+ for (let i = 0; i < inputBundle.codeArguments.length; i++) {
167
+ const shouldInstrument = (node, originLocation) => {
168
+ if (!originLocation.filename) {
169
+ return false;
170
+ }
171
+ return sourcePattern.isIncluded(originLocation.filename);
172
+ };
173
+ const instrumented = await instrumenter.instrument(inputBundle.codeArguments[i], taskElement.fromFile, inputSourceMaps[i], shouldInstrument);
174
+ instrumentedSources.push(instrumented);
175
+ }
176
+ this.writeBundleFile(taskElement.toFile, inputBundle, instrumentedSources);
177
+ return new Task_1.TaskResult(1, 0, 0, 0, 0, 0, 0);
178
+ }
179
+ writeBundleFile(toFile, inputBundle, instrumentedSources) {
180
+ if (inputBundle.type === 'gwt') {
181
+ commons_1.Contract.require(instrumentedSources.length === 1, 'Assuming only one code fragment to be passed as argument.');
182
+ const processedCodeString = JSON.stringify(`${this.vaccineSource} ${instrumentedSources[0]}`);
183
+ if (inputBundle.codeAsArrayArgument) {
184
+ writeToFile(toFile, `${exports.IS_INSTRUMENTED_TOKEN} ${inputBundle.functionName}([${processedCodeString}]);`);
185
+ }
186
+ else {
187
+ writeToFile(toFile, `${exports.IS_INSTRUMENTED_TOKEN} ${inputBundle.functionName}(${processedCodeString});`);
188
+ }
189
+ }
190
+ else {
191
+ commons_1.Contract.require(instrumentedSources.length === 1, 'Assuming only one code fragment to be passed as argument for JavaScript bundles.');
192
+ writeToFile(toFile, instrumentedSources[0]);
193
+ }
121
194
  }
122
195
  /**
123
196
  * Loads the vaccine from the vaccine file and adjusts some template parameters.
124
197
  *
125
198
  * @param collector - The collector to send coverage information to.
126
199
  */
127
- loadVaccine(collector) {
128
- // We first replace some of the parameters in the file with the
200
+ loadVaccine(filePath, collector) {
201
+ // We first replace parameters in the file with the
129
202
  // actual values, for example, the collector to send the coverage information to.
130
- return fs
131
- .readFileSync(this.vaccineFilePath, 'utf8')
132
- .replace(/\$REPORT_TO_HOST/g, collector.host)
133
- .replace(/\$REPORT_TO_PORT/g, `${collector.port}`);
134
- }
135
- /**
136
- * Should the given file be excluded from the instrumentation,
137
- * based on the source files that have been transpiled into it?
138
- *
139
- * @param pattern - The pattern to match the origin source files.
140
- * @param sourceFile - The bundle file name.
141
- * @param originSourceFiles - The list of files that were transpiled into the bundle.
142
- */
143
- shouldExcludeFromInstrumentation(pattern, sourceFile, originSourceFiles) {
144
- return !pattern.isAnyIncluded(originSourceFiles);
203
+ return fs.readFileSync(filePath, 'utf8').replace(/\$REPORT_TO_URL/g, collector.url);
145
204
  }
146
205
  /**
147
206
  * @returns whether the given file is supported for instrumentation.
148
207
  */
149
208
  isFileTypeSupported(fileName) {
209
+ if (fileName.endsWith('.devmode.js') || fileName.endsWith('.nocache.js')) {
210
+ // GWT dev mode files.
211
+ return false;
212
+ }
150
213
  const ext = path.extname(fileName).toLowerCase();
151
214
  return ext === '.js' || ext === '.mjs';
152
215
  }
@@ -157,34 +220,82 @@ class IstanbulInstrumenter {
157
220
  configurationAlternativesFor(taskElement) {
158
221
  this.logger.debug(`Determining configuration alternatives for ${taskElement.fromFile}`);
159
222
  const baseConfig = {
160
- coverageVariable: '__coverage__',
161
- produceSourceMap: true
223
+ isInstrumentedToken: exports.IS_INSTRUMENTED_TOKEN,
224
+ produceSourceMap: 'external',
225
+ codeToPrepend: this.vaccineSource
162
226
  };
163
227
  return [
164
228
  { ...baseConfig, ...{ esModules: true } },
165
229
  { ...baseConfig, ...{ esModules: false } }
166
230
  ];
167
231
  }
168
- /**
169
- * Given a source code file and the task element, load the corresponding sourcemap.
170
- *
171
- * @param inputSource - The source code that might contain sourcemap comments.
172
- * @param taskElement - The task element that can have a reference to an external sourcemap.
173
- */
174
- loadInputSourceMap(inputSource, taskElement) {
175
- if (taskElement.externalSourceMapFile.isPresent()) {
176
- const sourceMapOrigin = taskElement.externalSourceMapFile.get();
177
- if (!(sourceMapOrigin instanceof Task_1.SourceMapFileReference)) {
178
- throw new commons_1.IllegalArgumentException('Type of source map not yet supported!');
232
+ /** Appends all origins from the source map to a given file. Creates the file if it does not exist yet. */
233
+ dumpOrigins(dumpOriginsFile, originSourceFiles) {
234
+ const jsonContent = JSON.stringify(originSourceFiles, null, 2);
235
+ fs.writeFile(dumpOriginsFile, jsonContent + '\n', { flag: 'a' }, error => {
236
+ if (error) {
237
+ this.logger.warn('Could not dump origins file');
238
+ }
239
+ });
240
+ }
241
+ /** Clears the dump origins file if it exists, such that it is now ready to be appended for every instrumented file. */
242
+ clearDumpOriginsFileIfNeeded(dumpOriginsFile) {
243
+ if (dumpOriginsFile && fs.existsSync(dumpOriginsFile)) {
244
+ try {
245
+ fs.unlinkSync(dumpOriginsFile);
246
+ }
247
+ catch (err) {
248
+ this.logger.warn('Could not clear origins file: ' + err);
179
249
  }
180
- return sourceMapFromMapFile(sourceMapOrigin.sourceMapFilePath);
181
- }
182
- else {
183
- return sourceMapFromCodeComment(inputSource, taskElement.fromFile);
184
250
  }
185
251
  }
186
252
  }
187
253
  exports.IstanbulInstrumenter = IstanbulInstrumenter;
254
+ /**
255
+ * Extract the sourcemap from the given source code.
256
+ *
257
+ * @param instrumentedSource - The source code.
258
+ * @param instrumentedSourceFileName - The file name to assume for the file name.
259
+ */
260
+ async function loadSourceMap(instrumentedSource, instrumentedSourceFileName) {
261
+ const instrumentedSourceMap = loadInputSourceMap(instrumentedSource, instrumentedSourceFileName, typescript_optional_1.Optional.empty());
262
+ if (instrumentedSourceMap) {
263
+ return await new source_map_1.SourceMapConsumer(instrumentedSourceMap);
264
+ }
265
+ return undefined;
266
+ }
267
+ exports.loadSourceMap = loadSourceMap;
268
+ function loadInputSourceMaps(taskFile, bundleFile, externalSourceMapFile) {
269
+ if ((0, WebToolkit_1.isGwtBundle)(bundleFile)) {
270
+ return (0, WebToolkit_1.loadInputSourceMapsGwt)(taskFile, bundleFile);
271
+ }
272
+ else {
273
+ return loadInputSourceMapsStandard(taskFile, bundleFile, externalSourceMapFile);
274
+ }
275
+ }
276
+ function loadInputSourceMapsStandard(taskFile, bundleFile, externalSourceMapFile) {
277
+ return bundleFile.codeArguments.map(code => loadInputSourceMap(code, taskFile, externalSourceMapFile));
278
+ }
279
+ /**
280
+ * Given a source code file, load the corresponding sourcemap.
281
+ *
282
+ * @param inputSource - The source code that might contain sourcemap comments.
283
+ * @param taskFile - The name of the file the `inputSource` is from.
284
+ * @param externalSourceMapFile - An external source map file to consider.
285
+ */
286
+ function loadInputSourceMap(inputSource, taskFile, externalSourceMapFile) {
287
+ if (externalSourceMapFile.isPresent()) {
288
+ const sourceMapOrigin = externalSourceMapFile.get();
289
+ if (!(sourceMapOrigin instanceof Task_1.SourceMapFileReference)) {
290
+ throw new commons_1.IllegalArgumentException('Type of source map not yet supported!');
291
+ }
292
+ return (0, FileSystem_1.sourceMapFromMapFile)(sourceMapOrigin.sourceMapFilePath);
293
+ }
294
+ else {
295
+ return sourceMapFromCodeComment(inputSource, taskFile);
296
+ }
297
+ }
298
+ exports.loadInputSourceMap = loadInputSourceMap;
188
299
  /**
189
300
  * Extract a sourcemap for a given code comment.
190
301
  *
@@ -210,7 +321,9 @@ function sourceMapFromCodeComment(sourcecode, sourceFilePath) {
210
321
  }
211
322
  else {
212
323
  result = convertSourceMap
213
- .fromMapFileComment(sourceMapComment, path.dirname(sourceFilePath))
324
+ .fromMapFileComment(sourceMapComment, function (filename) {
325
+ return fs.readFileSync(path.resolve(path.dirname(sourceFilePath), filename), 'utf-8');
326
+ })
214
327
  .toObject();
215
328
  }
216
329
  }
@@ -218,11 +331,11 @@ function sourceMapFromCodeComment(sourcecode, sourceFilePath) {
218
331
  // One JS file can refer to several source map files in its comments.
219
332
  failedLoading++;
220
333
  }
221
- if (result) {
222
- return result;
223
- }
224
334
  }
225
335
  } while (matched);
336
+ if (result) {
337
+ return result;
338
+ }
226
339
  if (failedLoading > 0) {
227
340
  throw new commons_1.IllegalArgumentException('None of the referenced source map files loaded!');
228
341
  }
@@ -230,12 +343,12 @@ function sourceMapFromCodeComment(sourcecode, sourceFilePath) {
230
343
  return undefined;
231
344
  }
232
345
  }
233
- /**
234
- * Read a source map from a source map file.
235
- *
236
- * @param mapFilePath
237
- */
238
- function sourceMapFromMapFile(mapFilePath) {
239
- const content = fs.readFileSync(mapFilePath, 'utf8');
240
- return JSON.parse(content);
346
+ exports.sourceMapFromCodeComment = sourceMapFromCodeComment;
347
+ function writeToFile(filePath, fileContent) {
348
+ mkdirp.sync(path.dirname(filePath));
349
+ fs.writeFileSync(filePath, fileContent);
350
+ }
351
+ function copyToFile(targetFilePath, sourceFilePath) {
352
+ mkdirp.sync(path.dirname(targetFilePath));
353
+ fs.copyFileSync(sourceFilePath, targetFilePath);
241
354
  }