UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

215 lines (189 loc) 6.3 kB
'use strict'; /* @noflow */ /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ /* NON-TRANSPILED FILE */ /* eslint-disable babel/func-params-comma-dangle, prefer-object-spread/prefer-object-spread */ /* eslint-disable no-console */ //------------------------------------------------------------------------------ // NodeTranspiler is a wrapper around babel with: // * Nuclide specific configuration, that must be shared among several // independent transpile systems. // * Lazy-loading of expensive libs like babel. // * Support for externally loaded babel. //------------------------------------------------------------------------------ const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const os = require('os'); const PREFIXES = ["'use babel'", '"use babel"', '/* @flow */', '/** @babel */']; const PREFIX_LENGTH = Math.max(...PREFIXES.map(x => x.length)); // Atom defaults: https://github.com/atom/atom/blob/v1.6.2/static/babelrc.json // { // "breakConfig": true, // "sourceMap": "inline", // "blacklist": ["es6.forOf", "useStrict"], // "optional": ["asyncToGenerator"], // "stage": 0 // } const BABEL_OPTIONS = { breakConfig: true, // TODO(asuarez): Remove path information if source maps are enabled for builds. // sourceMap: 'inline', blacklist: [ 'es3.memberExpressionLiterals', 'es6.forOf', 'useStrict', ], optional: [ 'asyncToGenerator', ], // TODO(asuarez): Improve perf by explicitly running only the transforms we use. stage: 1, plugins: [ require.resolve('./remove-use-babel-tr'), require.resolve('./use-minified-libs-tr'), require.resolve('./inline-imports-tr'), ], // comments: false, // compact: true, // externalHelpers: true, // loose: [ // 'es6.classes', // 'es6.destructuring', // 'es6.forOf', // 'es6.modules', // 'es6.properties.computed', // 'es6.spread', // 'es6.templateLiterals', // ], }; class NodeTranspiler { static shouldCompile(bufferOrString) { const start = bufferOrString.slice(0, PREFIX_LENGTH).toString(); return PREFIXES.some(prefix => start.startsWith(prefix)); } constructor(babelVersion, getBabel) { if (babelVersion) { assert(typeof babelVersion === 'string'); assert(typeof getBabel === 'function'); this._babelVersion = babelVersion; this._getBabel = getBabel; } else { this._babelVersion = require('babel-core/package.json').version; this._getBabel = () => require('babel-core'); } this._babel = null; this._cacheDir = null; this._configDigest = null; } getConfigDigest() { if (!this._configDigest) { // Keep the digest consistent regardless of what directory we're in. const optsOnly = Object.assign({}, BABEL_OPTIONS, {plugins: null}); const hash = crypto .createHash('sha1') .update('babel-core', 'utf8') .update('\0', 'utf8') .update(this._babelVersion, 'utf8') .update('\0', 'utf8') .update(JSON.stringify(optsOnly), 'utf8'); // The source of this file and that of plugins is used as part of the // hash as a way to version our transforms. [__filename] .concat(BABEL_OPTIONS.plugins) .filter(Boolean) .forEach(pluginFile => { hash .update(fs.readFileSync(pluginFile)) .update('\0', 'utf8'); }); this._configDigest = hash.digest('hex'); } return this._configDigest; } getFileDigest(src, filename) { assert(typeof filename === 'string'); const hash = crypto .createHash('sha1') // Buffers are fast, but strings work too. .update(src, Buffer.isBuffer(src) ? undefined : 'utf8'); if (BABEL_OPTIONS.sourceMap) { // Sourcemaps encode the filename. hash .update('\0', 'utf8') .update(filename, 'utf8'); } const fileDigest = hash.digest('hex'); return fileDigest; } transform(src, filename) { assert(typeof filename === 'string'); if (!this._babel) { this._babel = this._getBabel(); } try { const input = Buffer.isBuffer(src) ? src.toString() : src; const opts = BABEL_OPTIONS.sourceMap ? Object.assign({filename}, BABEL_OPTIONS) : BABEL_OPTIONS; const output = this._babel.transform(input, opts).code; return output; } catch (err) { console.error(`Error transpiling "${filename}"`); throw err; } } transformWithCache(src, filename) { assert(typeof filename === 'string'); const cacheFilename = this._getCacheFilename(src, filename); if (fs.existsSync(cacheFilename)) { const cached = fs.readFileSync(cacheFilename, 'utf8'); return cached; } const output = this.transform(src, filename); this._cacheWriteSync(cacheFilename, output); return output; } _getCacheFilename(src, filename) { if (!this._cacheDir) { this._cacheDir = path.join( os.tmpdir(), 'nuclide-node-transpiler', this.getConfigDigest() ); } const fileDigest = this.getFileDigest(src, filename); const cacheFilename = path.join(this._cacheDir, fileDigest + '.js'); return cacheFilename; } _cacheWriteSync(cacheFilename, src) { // Write the file to a temp file first and then move it so the write to the // cache is atomic. Although Node is single-threaded, there could be // multiple Node processes running simultaneously that are using the cache. const mkdirp = require('mkdirp'); const uuid = require('uuid'); const basedir = path.dirname(cacheFilename); const tmpName = path.join(basedir, '.' + uuid.v4()); try { mkdirp.sync(basedir); } catch (err) { console.error(`Cache mkdirp failed. ${err}`); return; } try { fs.writeFileSync(tmpName, src); fs.renameSync(tmpName, cacheFilename); } catch (err) { console.error(`Cache write failed. ${err}`); try { fs.unlinkSync(tmpName); } catch (err_) {} } } } module.exports = NodeTranspiler;