/ src / package-transpilation-registry.js
package-transpilation-registry.js
  1  'use strict';
  2  // This file is required by compile-cache, which is required directly from
  3  // apm, so it can only use the subset of newer JavaScript features that apm's
  4  // version of Node supports. Strict mode is required for block scoped declarations.
  5  
  6  const crypto = require('crypto');
  7  const fs = require('fs');
  8  const path = require('path');
  9  
 10  const minimatch = require('minimatch');
 11  
 12  let Resolve = null;
 13  
 14  class PackageTranspilationRegistry {
 15    constructor() {
 16      this.configByPackagePath = {};
 17      this.specByFilePath = {};
 18      this.transpilerPaths = {};
 19    }
 20  
 21    addTranspilerConfigForPath(packagePath, packageName, packageMeta, config) {
 22      this.configByPackagePath[packagePath] = {
 23        name: packageName,
 24        meta: packageMeta,
 25        path: packagePath,
 26        specs: config.map(spec => Object.assign({}, spec))
 27      };
 28    }
 29  
 30    removeTranspilerConfigForPath(packagePath) {
 31      delete this.configByPackagePath[packagePath];
 32      const packagePathWithSep = packagePath.endsWith(path.sep)
 33        ? path.join(packagePath)
 34        : path.join(packagePath) + path.sep;
 35      Object.keys(this.specByFilePath).forEach(filePath => {
 36        if (path.join(filePath).startsWith(packagePathWithSep)) {
 37          delete this.specByFilePath[filePath];
 38        }
 39      });
 40    }
 41  
 42    // Wraps the transpiler in an object with the same interface
 43    // that falls back to the original transpiler implementation if and
 44    // only if a package hasn't registered its desire to transpile its own source.
 45    wrapTranspiler(transpiler) {
 46      return {
 47        getCachePath: (sourceCode, filePath) => {
 48          const spec = this.getPackageTranspilerSpecForFilePath(filePath);
 49          if (spec) {
 50            return this.getCachePath(sourceCode, filePath, spec);
 51          }
 52  
 53          return transpiler.getCachePath(sourceCode, filePath);
 54        },
 55  
 56        compile: (sourceCode, filePath) => {
 57          const spec = this.getPackageTranspilerSpecForFilePath(filePath);
 58          if (spec) {
 59            return this.transpileWithPackageTranspiler(
 60              sourceCode,
 61              filePath,
 62              spec
 63            );
 64          }
 65  
 66          return transpiler.compile(sourceCode, filePath);
 67        },
 68  
 69        shouldCompile: (sourceCode, filePath) => {
 70          if (this.transpilerPaths[filePath]) {
 71            return false;
 72          }
 73          const spec = this.getPackageTranspilerSpecForFilePath(filePath);
 74          if (spec) {
 75            return true;
 76          }
 77  
 78          return transpiler.shouldCompile(sourceCode, filePath);
 79        }
 80      };
 81    }
 82  
 83    getPackageTranspilerSpecForFilePath(filePath) {
 84      if (this.specByFilePath[filePath] !== undefined)
 85        return this.specByFilePath[filePath];
 86  
 87      let thisPath = filePath;
 88      let lastPath = null;
 89      // Iterate parents from the file path to the root, checking at each level
 90      // to see if a package manages transpilation for that directory.
 91      // This means searching for a config for `/path/to/file/here.js` only
 92      // only iterates four times, even if there are hundreds of configs registered.
 93      while (thisPath !== lastPath) {
 94        // until we reach the root
 95        let config = this.configByPackagePath[thisPath];
 96        if (config) {
 97          const relativePath = path.relative(thisPath, filePath);
 98          if (
 99            relativePath.startsWith(`node_modules${path.sep}`) ||
100            relativePath.indexOf(`${path.sep}node_modules${path.sep}`) > -1
101          ) {
102            return false;
103          }
104          for (let i = 0; i < config.specs.length; i++) {
105            const spec = config.specs[i];
106            if (minimatch(filePath, path.join(config.path, spec.glob))) {
107              spec._config = config;
108              this.specByFilePath[filePath] = spec;
109              return spec;
110            }
111          }
112        }
113  
114        lastPath = thisPath;
115        thisPath = path.join(thisPath, '..');
116      }
117  
118      this.specByFilePath[filePath] = null;
119      return null;
120    }
121  
122    getCachePath(sourceCode, filePath, spec) {
123      const transpilerPath = this.getTranspilerPath(spec);
124      const transpilerSource =
125        spec._transpilerSource || fs.readFileSync(transpilerPath, 'utf8');
126      spec._transpilerSource = transpilerSource;
127      const transpiler = this.getTranspiler(spec);
128  
129      let hash = crypto
130        .createHash('sha1')
131        .update(JSON.stringify(spec.options || {}))
132        .update(transpilerSource, 'utf8')
133        .update(sourceCode, 'utf8');
134  
135      if (transpiler && transpiler.getCacheKeyData) {
136        const meta = this.getMetadata(spec);
137        const additionalCacheData = transpiler.getCacheKeyData(
138          sourceCode,
139          filePath,
140          spec.options || {},
141          meta
142        );
143        hash.update(additionalCacheData, 'utf8');
144      }
145  
146      return path.join(
147        'package-transpile',
148        spec._config.name,
149        hash.digest('hex')
150      );
151    }
152  
153    transpileWithPackageTranspiler(sourceCode, filePath, spec) {
154      const transpiler = this.getTranspiler(spec);
155  
156      if (transpiler) {
157        const meta = this.getMetadata(spec);
158        const result = transpiler.transpile(
159          sourceCode,
160          filePath,
161          spec.options || {},
162          meta
163        );
164        if (result === undefined || (result && result.code === undefined)) {
165          return sourceCode;
166        } else if (result.code) {
167          return result.code.toString();
168        } else {
169          throw new Error(
170            'Could not find a property `.code` on the transpilation results of ' +
171              filePath
172          );
173        }
174      } else {
175        const err = new Error(
176          "Could not resolve transpiler '" +
177            spec.transpiler +
178            "' from '" +
179            spec._config.path +
180            "'"
181        );
182        throw err;
183      }
184    }
185  
186    getMetadata(spec) {
187      return {
188        name: spec._config.name,
189        path: spec._config.path,
190        meta: spec._config.meta
191      };
192    }
193  
194    getTranspilerPath(spec) {
195      Resolve = Resolve || require('resolve');
196      return Resolve.sync(spec.transpiler, {
197        basedir: spec._config.path,
198        extensions: Object.keys(require.extensions)
199      });
200    }
201  
202    getTranspiler(spec) {
203      const transpilerPath = this.getTranspilerPath(spec);
204      if (transpilerPath) {
205        const transpiler = require(transpilerPath);
206        this.transpilerPaths[transpilerPath] = true;
207        return transpiler;
208      }
209    }
210  }
211  
212  module.exports = PackageTranspilationRegistry;