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;