UNPKG

polyfill-service

Version:
269 lines (229 loc) 7.72 kB
'use strict'; const fs = require('graceful-fs'); const path = require('path'); const uglify = require('uglify-js'); const babel = require("babel-core"); const mkdirp = require('mkdirp'); const tsort = require('tsort'); const denodeify = require('denodeify'); const vm = require('vm'); const writeFile = denodeify(fs.writeFile); const readFile = denodeify(fs.readFile); const makeDirectory = denodeify(mkdirp); function validateSource(code, label) { try { new vm.Script(code); } catch (error) { throw { name: "Parse error", message: `Error parsing source code for ${label}`, error }; } } function flattenPolyfillDirectories(directory) { // Recursively discover all subfolders and produce a flattened list. // Directories prefixed with '__' are not polyfill features and are not included. let results = []; for (const item of fs.readdirSync(directory)) { const joined = path.join(directory, item); if (fs.lstatSync(joined).isDirectory() && item.indexOf('__') !== 0) { results = results .concat(flattenPolyfillDirectories(joined)) .concat(joined); } } return results; } function checkForCircularDependencies(polyfills) { const graph = tsort(); for (const polyfill of polyfills) { for (const dependency of polyfill.dependencies) { graph.add(dependency, polyfill.name); } } try { graph.sort(); return Promise.resolve(); } catch (err) { return Promise.reject('\nThere is a circle in the dependency graph.\nCheck the `dependencies` property of polyfill config files that have recently changed, and ensure that they do not form a circle of references.' + err); } } function writeAliasFile(polyfills, dir) { const aliases = {}; for (const polyfill of polyfills) { for (const alias of polyfill.aliases) { aliases[alias] = (aliases[alias] || []).concat(polyfill.name); } } return writeFile(path.join(dir, 'aliases.json'), JSON.stringify(aliases)); } class Polyfill { constructor(absolute, relative) { this.path = { absolute, relative }; this.name = relative.replace(/(\/|\\)/g, '.'); this.config = {}; this.sources = {}; } get aliases() { return this.config.aliases || []; } get dependencies() { return this.config.dependencies || []; } get configPath() { return path.join(this.path.absolute, 'config.json'); } get detectPath() { return path.join(this.path.absolute, 'detect.js'); } get sourcePath() { return path.join(this.path.absolute, 'polyfill.js'); } get hasConfigFile() { return fs.existsSync(this.configPath); } updateConfig() { this.config.size = this.sources.min.length; } loadConfig() { return readFile(this.configPath) .catch(error => { throw { name: "Invalid config", message: `Unable to read config from ${this.configPath}`, error }; }) .then(data => { this.config = JSON.parse(data); this.config.detectSource = ''; this.config.baseDir = this.path.relative; if ('licence' in this.config) { throw `Incorrect spelling of license property in ${this.name}`; } this.config.hasTests = fs.existsSync(path.join(this.path.absolute, 'tests.js')); this.config.isTestable = !('test' in this.config && 'ci' in this.config.test && this.config.test.ci === false); this.config.isPublic = this.name.indexOf('_') !== 0; if (fs.existsSync(this.detectPath)) { this.config.detectSource = fs.readFileSync(this.detectPath, 'utf8').replace(/\s*$/, '') || ''; validateSource(`if (${this.config.detectSource}) true;`, `${this.name} feature detect from ${this.detectPath}`); } }); } loadSources() { return readFile(this.sourcePath, 'utf8') .catch(error => { throw { name: "Invalid source", message: `Unable to read source from ${this.sourcePath}`, error }; }) .then(raw => this.transpile(raw)) .catch(error => { throw { message: `Error transpiling ${this.name}`, error }; }) .then(transpiled => this.minify(transpiled)) .catch(error => { throw { message: `Error minifying ${this.name}`, error }; }) .then(this.removeSourceMaps) .then(sources => { this.sources = sources; }); } transpile(source) { // At time of writing no current browsers support the full ES6 language syntax, // so for simplicity, polyfills written in ES6 will be transpiled to ES5 in all // cases (also note that uglify currently cannot minify ES6 syntax). When browsers // start shipping with complete ES6 support, the ES6 source versions should be served // where appropriate, which will require another set of variations on the source properties // of the polyfill. At this point it might be better to create a collection of sources with // different properties, eg config.sources = [{code:'...', esVersion:6, minified:true},{...}] etc. if (this.config.esversion && this.config.esversion > 5) { if (this.config.esversion === 6) { const transpiled = babel.transform(source, { presets: ["es2015"] }); // Don't add a "use strict" // Super annoying to have to drop the preset and list all babel plugins individually, so hack to remove the "use strict" added by Babel (see also http://stackoverflow.com/questions/33821312/how-to-remove-global-use-strict-added-by-babel) return transpiled.code.replace(/^\s*"use strict";\s*/i, ''); } else { throw { name: "Unsupported ES version", message: `Feature ${this.name} uses ES${this.config.esversion} but no transpiler is available for that version` }; } } return source; } minify(source) { const raw = `\n// ${this.name}\n${source}`; if (this.config.build && this.config.build.minify === false) { // skipping any validation or minification process since // the raw source is supposed to be production ready. // Add a line break in case the final line is a comment return { raw: raw + '\n', min: source + '\n' }; } else { validateSource(source, `${this.name} from ${this.sourcePath}`); const minified = uglify.minify(source, { fromString: true, compress: { screw_ie8: false }, mangle: { screw_ie8: false }, output: { screw_ie8: false, beautify: false } }); return { raw, min: minified.code }; } } removeSourceMaps(source) { const re = /^\/\/#\ssourceMappingURL(.+)$/gm; return { raw: source.raw.replace(re, ''), min: source.min.replace(re, '') }; } writeOutput(root) { const dest = path.join(root, this.name); const files = [ ['meta.json', JSON.stringify(this.config)], ['raw.js', this.sources.raw], ['min.js', this.sources.min] ]; return makeDirectory(dest) .then(() => Promise.all(files .map(([name, contents]) => [path.join(dest, name), contents]) .map(([path, contents]) => writeFile(path, contents)))); } } const src = path.join(__dirname, '../../polyfills'); const dest = path.join(src, '__dist'); console.log(`Writing compiled polyfill sources to ${dest}/...`); Promise.resolve() .then(() => Promise.all(flattenPolyfillDirectories(src) .map(absolute => new Polyfill(absolute, path.relative(src, absolute))) .filter(polyfill => polyfill.hasConfigFile) .map(polyfill => polyfill.loadConfig() .then(() => polyfill.loadSources()) .then(() => polyfill.updateConfig()) .then(() => polyfill) ) )) .then(polyfills => checkForCircularDependencies(polyfills) .then(() => makeDirectory(dest)) .then(() => console.log('Waiting for files to be written to disk...')) .then(() => writeAliasFile(polyfills, dest)) .then(() => Promise.all( polyfills.map(polyfill => polyfill.writeOutput(dest)) )) ) .then(() => console.log('Sources built successfully')) .catch(e => { console.log(e); process.exit(1); }) ;