diff --git a/.gitignore b/.gitignore index df99ae0..32efc03 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ -/node_modules/ -tmp/ -test/index.html -test/.generated -bin/ -dist/ +node_modules/ +test/results diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index 6e08d11..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,33 +0,0 @@ -function config(name) { - return require('./tasks/options/' + name); -} - -module.exports = function(grunt) { - var path = require('path'); - - // Load node modules providing grunt tasks. - require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); - - grunt.initConfig({ - clean : config('clean'), - transpile : config('transpile'), - browserify : config('browserify'), - es6ify : config('es6ify'), - concat : config('concat'), - uglify : config('uglify'), - jshint : config('jshint'), - - simplemocha : config('simplemocha'), - features : config('features') - }); - - // Load local tasks. - grunt.task.loadTasks('./tasks'); - - grunt.registerTask('build', - ['clean', 'transpile', 'es6ify', 'browserify', 'concat', 'uglify']); - - grunt.registerTask('test', ['features', 'simplemocha']); - - grunt.registerTask('default', ['test']); -}; diff --git a/LICENSE b/LICENSE index c38a291..cabe814 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2013 Square Inc. +Copyright 2014 Square Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/bin/compile-modules b/bin/compile-modules new file mode 100755 index 0000000..04c9c89 --- /dev/null +++ b/bin/compile-modules @@ -0,0 +1,84 @@ +#!/usr/bin/env node + +var Path = require('path'); +var exe = Path.basename(process.argv[1]); +var getopt = require('posix-getopt'); +var parser = new getopt.BasicParser('h(help)v(version)', process.argv); +var option; + +function usage(puts) { + puts(exe + ' [--help] [--version] []'); + puts(); + puts('Commands'); + puts(); + puts(' convert Converts modules from `import`/`export` to an ES5 equivalent.'); + puts(' help Display help for a given command.'); +} + +function makeWriteLine(stream) { + return function(line) { + if (!line || line[line.length - 1] !== '\n') { + line = (line || '') + '\n'; + } + stream.write(line); + }; +} + +var puts = makeWriteLine(process.stdout); +var eputs = makeWriteLine(process.stderr); + +while ((option = parser.getopt()) !== undefined) { + if (option.error) { + usage(); + process.exit(1); + } + + switch (option.option) { + case 'h': + usage(puts); + process.exit(0); + break; + + case 'v': + puts(exe + ' v' + require(Path.join(__dirname, '../package.json')).version); + process.exit(0); + break; + } +} + +var args = process.argv; +var offset = parser.optind(); + +var commandName = args[offset]; +if (commandName === 'help') { + commandName = args[offset + 1]; + args = ['--help'].concat(args.slice(offset + 2 /* skip 'help' and command name */)); +} else { + args = args.slice(offset + 1); +} + +if (typeof commandName !== 'string') { + usage(puts); + process.exit(1); +} + +var command; + +try { + command = require(Path.join('../lib/cli', commandName)); +} catch (ex) { + usage(eputs); + process.exit(1); +} + +try { + var exitCode = command.run(args, puts, eputs); + process.exit(exitCode); +} catch (ex) { + if (ex.constructor.name === 'AssertionError') { + eputs('error: ' + exe + ' ' + commandName + ' -- ' + ex.message); + process.exit(1); + } else { + throw ex; + } +} diff --git a/lib/abstract_compiler.js b/lib/abstract_compiler.js deleted file mode 100644 index b2d4525..0000000 --- a/lib/abstract_compiler.js +++ /dev/null @@ -1,118 +0,0 @@ -import CompileError from './compile_error'; -import { isEmpty, array, forEach } from './utils'; - -class AbstractCompiler { - constructor(compiler, options) { - this.compiler = compiler; - - this.exports = compiler.exports; - this.exportDefault = compiler.exportDefault; - this.imports = compiler.imports; - this.directives = compiler.directives; - - this.moduleName = compiler.moduleName; - this.lines = compiler.lines; - this.string = compiler.string; - - this.options = options; - - var allDependencies = this.imports.concat(this.exports.filter(function(export_) { - return export_.source !== null; - })); - - this.dependencyNames = array.uniq(allDependencies.map(function(dep) { - return dep.source.value; - })); - } - - buildImports() { - var imports = this.imports, - moduleImports = this.moduleImports, - source = this.source; - - for (var idx = 0; idx < imports.length; idx++) { - var import_ = imports[idx], - replacement = ""; - - var dependencyName = import_.source.value; - - if (import_.type === "ModuleDeclaration" && import_.source.type === "Literal") { - replacement = this.doModuleImport(import_.id.name, dependencyName, idx); - } else if (import_.type === "ImportDeclaration") { - if (import_.kind === "default") { - // var name = __dependencyX__; - var specifier = import_.specifiers[0]; - replacement = this.doDefaultImport(specifier.id.name, dependencyName, idx); - } else if (import_.kind === "named") { - // var one = __dependencyX__.one; - // var two = __dependencyX__.two; - replacement = this.doImportSpecifiers(import_, idx); - } else if (import_.kind === undefined) { - replacement = this.doBareImport(import_.source.value); - } - } - source.replace(import_.range[0], import_.range[1], replacement); - } - } - - buildExports() { - var source = this.source, - exports_ = this.exports; - - for (var export_ of exports_) { - var replacement = ""; - - if (export_.default) { - var identifier = export_.declaration.name || null; - source.replace(export_.range[0], - export_.declaration.range[0] - 1, - this.doDefaultExport(identifier)); - } else if (export_.specifiers) { - var reexport; - if (export_.source) { - reexport = export_.source.value; - } - for (var specifier of export_.specifiers) { - replacement += this.doExportSpecifier(specifier.id.name, reexport); - } - source.replace(export_.range[0], export_.range[1], replacement); - } else if (export_.declaration) { - - if (export_.declaration.type === "VariableDeclaration") { - var name = export_.declaration.declarations[0].id.name; - - // remove the "export" keyword - source.replace(export_.range[0], export_.declaration.range[0] -1, ""); - // add a new line - replacement = this.doExportDeclaration(name); - source.replace(export_.range[1], export_.range[1], replacement); - } else if (export_.declaration.type === "FunctionDeclaration") { - var name = export_.declaration.id.name; - - source.replace(export_.range[0], export_.declaration.range[0] - 1, ""); - - replacement = this.doExportDeclaration(name); - source.replace(export_.range[1] + 1, export_.range[1] + 1, replacement); - } else if (export_.declaration.type === "Identifier") { - var name = export_.declaration.name; - - replacement = this.doExportDeclaration(name); - source.replace(export_.range[0], export_.range[1] - 1, replacement); - } - - } - } - } - - indentLines(indent = " ") { - var innerLines = this.source.toString().split("\n"); - var inner = innerLines.reduce(function(acc, item) { - if (item === "") return acc + "\n"; - return acc + indent + item + "\n"; - }, ""); - - return inner.replace(/\s+$/, ""); - } -} - -export default AbstractCompiler; diff --git a/lib/amd_compiler.js b/lib/amd_compiler.js deleted file mode 100644 index 0d9e633..0000000 --- a/lib/amd_compiler.js +++ /dev/null @@ -1,106 +0,0 @@ -import AbstractCompiler from './abstract_compiler'; -import SourceModifier from './source_modifier'; - -class AMDCompiler extends AbstractCompiler { - stringify() { - var string = this.string.toString(); // string is actually a node buffer - this.source = new SourceModifier(string); - - this.map = []; - var out = this.buildPreamble(this.exports.length > 0); - - // build* mutates this.source - this.buildImports(); - this.buildExports(); - - out += this.indentLines(" "); - out += "\n });"; - - return out; - } - - buildPreamble(hasExports) { - var out = "", - dependencyNames = this.dependencyNames; - - if (hasExports) dependencyNames.push("exports"); - - out += "define("; - if (this.moduleName) out += `"${this.moduleName}",`; - out += "\n ["; - - // build preamble - var idx; - for (idx = 0; idx < dependencyNames.length; idx++) { - var name = dependencyNames[idx]; - out += `"${name}"`; - if (!(idx === dependencyNames.length - 1)) out += ","; - } - - out += "],\n function("; - - for (idx = 0; idx < dependencyNames.length; idx++) { - if (dependencyNames[idx] === "exports") { - out += "__exports__"; - } else { - out += `__dependency${idx+1}__`; - this.map[dependencyNames[idx]] = idx+1; - } - if (!(idx === dependencyNames.length - 1)) out += ", "; - } - - out += ") {\n"; - - out += ' "use strict";\n'; - - return out; - } - - doModuleImport(name, dependencyName, idx) { - return `var ${name} = __dependency${this.map[dependencyName]}__;\n`; - } - - doBareImport(name) { - return ""; - } - - doDefaultImport(name, dependencyName, idx) { - if (this.options.compatFix === true) { - return `var ${name} = __dependency${this.map[dependencyName]}__["default"] || __dependency${this.map[dependencyName]}__;\n`; - } else { - return `var ${name} = __dependency${this.map[dependencyName]}__["default"];\n`; - } - } - - doNamedImport(name, dependencyName, alias) { - return `var ${alias} = __dependency${this.map[dependencyName]}__.${name};\n`; - } - - doExportSpecifier(name, reexport) { - if (reexport) { - return `__exports__.${name} = __dependency${this.map[reexport]}__.${name};\n`; - } - return `__exports__.${name} = ${name};\n`; - } - - doExportDeclaration(name) { - return `\n__exports__.${name} = ${name};`; - } - - doDefaultExport() { - return `__exports__["default"] = `; - } - - doImportSpecifiers(import_, idx) { - var dependencyName = import_.source.value; - var replacement = ""; - for (var specifier of import_.specifiers) { - var alias = specifier.name ? specifier.name.name : specifier.id.name; - replacement += this.doNamedImport(specifier.id.name, dependencyName, alias); - } - return replacement; - } - -} - -export default AMDCompiler; diff --git a/lib/cjs_compiler.js b/lib/cjs_compiler.js deleted file mode 100644 index a81875f..0000000 --- a/lib/cjs_compiler.js +++ /dev/null @@ -1,120 +0,0 @@ -import AbstractCompiler from './abstract_compiler'; -import SourceModifier from './source_modifier'; -import { string } from './utils'; - -const SAFE_WARN_NAME = "__es6_transpiler_warn__"; -const SAFE_WARN_SOURCE = string.ltrim(string.unindent(` - function ${SAFE_WARN_NAME}(warning) { - if (typeof console === 'undefined') { - } else if (typeof console.warn === "function") { - console.warn(warning); - } else if (typeof console.log === "function") { - console.log(warning); - } - }`)); - -const MODULE_OBJECT_BUILDER_NAME = "__es6_transpiler_build_module_object__"; -const MODULE_OBJECT_BUILDER_SOURCE = string.ltrim(string.unindent(` - function ${MODULE_OBJECT_BUILDER_NAME}(name, imported) { - var moduleInstanceObject = Object.create ? Object.create(null) : {}; - if (typeof imported === "function") { - ${SAFE_WARN_NAME}("imported module '"+name+"' exported a function - this may not work as expected"); - } - for (var key in imported) { - if (Object.prototype.hasOwnProperty.call(imported, key)) { - moduleInstanceObject[key] = imported[key]; - } - } - if (Object.freeze) { - Object.freeze(moduleInstanceObject); - } - return moduleInstanceObject; - }`)); - -class CJSCompiler extends AbstractCompiler { - stringify() { - var string = this.string.toString(); // string is actually a node buffer - this.source = new SourceModifier(string); - this.prelude = []; - - this.buildImports(); - this.buildExports(); - - var out = `"use strict";\n`; - for (var source of this.prelude) { - out += source + "\n"; - } - out += this.source.toString(); - out = out.trim(); - return out; - } - - doModuleImport(name, dependencyName, idx) { - this.ensureHasModuleObjectBuilder(); - // NOTE: Don't be tempted to move `require("${dependencyName}")` into the builder. - // This require call is here so that browserify and the like will be able - // to statically analyze the file's requirements. - return `var ${name} = ${MODULE_OBJECT_BUILDER_NAME}("${name}", require("${dependencyName}"));\n`; - } - - ensureHasModuleObjectBuilder() { - this.ensureHasSafeWarn(); - this.ensureInPrelude(MODULE_OBJECT_BUILDER_NAME, MODULE_OBJECT_BUILDER_SOURCE); - } - - ensureHasSafeWarn() { - this.ensureInPrelude(SAFE_WARN_NAME, SAFE_WARN_SOURCE); - } - - ensureInPrelude(name, source) { - if (!this.prelude[name]) { - this.prelude[name] = true; - this.prelude.push(source); - } - } - - doBareImport(name) { - return `require("${name}");`; - } - - doDefaultImport(name, dependencyName, idx) { - if (this.options.compatFix === true) { - return `var ${name} = require("${dependencyName}")["default"] || require("${dependencyName}");\n`; - } else { - return `var ${name} = require("${dependencyName}")["default"];\n`; - } - } - - doNamedImport(name, dependencyName, alias) { - return `var ${alias} = require("${dependencyName}").${name};\n`; - } - - doExportSpecifier(name, reexport) { - if (reexport) { - return `exports.${name} = require("${reexport}").${name};\n`; - } - return `exports.${name} = ${name};\n`; - } - - doExportDeclaration(name) { - return `\nexports.${name} = ${name};`; - } - - doDefaultExport() { - return `exports["default"] = `; - } - - doImportSpecifiers(import_, idx) { - var dependencyName = import_.source.value; - var replacement = ""; - - for (var specifier of import_.specifiers) { - var alias = specifier.name ? specifier.name.name : specifier.id.name; - replacement += this.doNamedImport(specifier.id.name, dependencyName, alias); - } - return replacement; - } - -} - -export default CJSCompiler; diff --git a/lib/cli/convert.js b/lib/cli/convert.js new file mode 100644 index 0000000..a5a6083 --- /dev/null +++ b/lib/cli/convert.js @@ -0,0 +1,146 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); +var Path = require('path'); +var recast = require('recast'); + +var formatters = require('../formatters'); +var FileResolver = require('../file_resolver'); +var Container = require('../container'); +var FileResolver = require('../file_resolver'); + +var getopt = require('posix-getopt'); +var exe = Path.basename(process.argv[1]); + +exports.run = function(args, puts, eputs) { + var offset = 0; + + var files = []; + var includePaths = [process.cwd()]; + var output; + var formatter = formatters[formatters.DEFAULT]; + var resolverClasses = [FileResolver]; + + while (offset < args.length) { + var parser = new getopt.BasicParser('h(help)o:(output)I:(include)f:(format)r:(resolver)', ['', ''].concat(args.slice(offset))); + var option; + + while ((option = parser.getopt()) !== undefined) { + if (option.error) { + usage(eputs); + return 1; + } + + switch (option.option) { + case 'h': + usage(puts); + return 0; + + case 'o': + output = option.optarg; + break; + + case 'I': + includePaths.push(option.optarg); + break; + + case 'f': + formatter = formatters[option.optarg]; + if (!formatter) { + try { formatter = require(option.optarg); } + catch (ex) {} + } + if (!formatter) { + usage(eputs); + return 1; + } + break; + + case 'r': + try { + resolverClasses.push(require(option.optarg)); + } + catch (ex) { + usage(eputs); + return 1; + } + break; + } + } + + for (offset += parser.optind() - 2; args[offset] && args[offset][0] !== '-'; offset++) { + files.push(args[offset]); + } + } + + assert.ok( + files.length > 0, + 'Please provide at least one file to convert.' + ); + + if (typeof formatter === 'function') { + formatter = new formatter(); + } + + var resolvers = resolverClasses.map(function(resolverClass) { + return new resolverClass(includePaths); + }); + var container = new Container({ + formatter: formatter, + resolvers: resolvers + }); + + files.forEach(function(file) { + container.getModule(file); + }); + + if (output) { + container.write(output); + } else { + var outputs = container.convert(); + assert.equal( + outputs.length, 1, + 'Cannot output ' + outputs.length + ' files to stdout. ' + + 'Please use the --output flag to specify where to put the ' + + 'files or choose a formatter that concatenates.' + ); + process.stdout.write(recast.print(outputs[0]).code); + } + +}; + +function bold(string) { + return '\x1b[01m' + string + '\x1b[0m'; +} + +function usage(puts) { + puts(exe + ' convert [-I ] [-o ] [-f ] [-r ] [ ...]'); + puts(); + puts(bold('Description')); + puts(); + puts(' Converts the given modules by changing `import`/`export` statements to an ES5 equivalent.'); + puts(); + puts(bold('Options')); + puts(); + puts(' -I, --include Check the given path for imported modules (usable multiple times).'); + puts(' -o, --output File or directory to output converted files.'); + puts(' -f, --format Path to custom formatter or choose from built-in formats.'); + puts(' -r, --resolver Path to custom resolver (usable multiple times).'); + puts(' -h, --help Show this help message.'); + puts(); + puts(bold('Formats')); + puts(); + puts(' commonjs - convert modules to files using CommonJS `require` and `exports` objects.'); + puts(' module-variable - concatenate modules optimizing for runtime performance.'); + puts(' export-variable - concatenate modules optimizing for compressibility (enables better tree-shaking).'); + puts(); + puts(' You may provide custom a formatter by passing the path to your module to the `--format` option. See the'); + puts(' source of any of the built-in formatters for details on how to build your own.'); + puts(); + puts(bold('Resolvers')); + puts(); + puts(' Resolvers resolve import paths to modules. The default resolver will search the include paths provided'); + puts(' by `--include` arguments and the current working directory. To provide custom resolver logic, pass the'); + puts(' path to your resolver module providing a `resolveModule` function or class with an instance method with'); + puts(' this signature: `resolveModule(importedPath:String, fromModule:?Module, container:Container): Module`.'); +} diff --git a/lib/compile-modules.js b/lib/compile-modules.js deleted file mode 100644 index 058d08e..0000000 --- a/lib/compile-modules.js +++ /dev/null @@ -1,212 +0,0 @@ -import optimist from 'optimist'; -import fs from 'fs'; -import path from 'path'; -import through from 'through'; - -function extend(target, ...sources) { - var toString = {}.toString; - - sources.forEach(function(source) { - for (var key in source) { - target[key] = source[key]; - } - }); - - return target; -} - -class CLI { - constructor(Compiler, stdin=process.stdin, stdout=process.stdout, fs_=fs) { - this.Compiler = Compiler; - this.stdin = stdin; - this.stdout = stdout; - this.fs = fs_; - } - - start(argv) { - var options = this.parseArgs(argv); - - if (options.help) { - this.argParser(argv).showHelp(); - } else if (options.stdio) { - this.processStdio(options); - } else { - for (var i = 2; i < options._.length; i++) { - var filename = options._[i]; - this.processPath(filename, options); - } - } - } - - parseArgs(argv) { - var args = this.argParser(argv).argv; - - if (args.imports) { - var imports = {}; - args.imports.split(',').forEach(function(pair) { - var [requirePath, global] = pair.split(':'); - imports[requirePath] = global; - }); - args.imports = imports; - } - - if (args.global) { - args.into = args.global; - } - - return args; - } - - argParser(argv) { - return optimist(argv).usage('compile-modules usage:\n\n Using files:\n compile-modules INPUT --to DIR [--infer-name] [--type TYPE] [--imports PATH:GLOBAL]\n\n Using stdio:\n compile-modules --stdio [--type TYPE] [--imports PATH:GLOBAL] [--module-name MOD]').options({ - type: { - "default": 'amd', - describe: 'The type of output (one of "amd", "yui", "cjs", or "globals")' - }, - to: { - describe: 'A directory in which to write the resulting files' - }, - imports: { - describe: 'A list of path:global pairs, comma separated (e.g. jquery:$,ember:Ember)' - }, - 'infer-name': { - "default": false, - type: 'boolean', - describe: 'Automatically generate names for AMD and YUI modules' - }, - 'module-name': { - describe: 'The name of the outputted module', - alias: 'm' - }, - stdio: { - "default": false, - type: 'boolean', - alias: 's', - describe: 'Use stdin and stdout to process a file' - }, - global: { - describe: 'When the type is `globals`, the name of the global to export into' - }, - help: { - "default": false, - type: 'boolean', - alias: 'h', - describe: 'Shows this help message' - } - }).check(({type}) => type === 'amd' || type === 'yui' || type === 'cjs' || type === 'globals') - .check(args => !args['infer-name'] || !args.m) - .check(args => (args.stdio && args.type === 'amd') ? !args['infer-name'] : true) - .check(args => (args.stdio && args.type === 'yui') ? !args['infer-name'] : true) - .check(args => args.stdio || args.to || args.help) - .check(args => args.imports ? args.type === 'globals' : args.type !== 'globals'); - } - - processStdio(options) { - this.processIO(this.stdin, this.stdout, options); - } - - processIO(input, output, options) { - var data = '', - self = this; - - function write(chunk) { - data += chunk; - } - - function end() { - /* jshint -W040 */ - this.queue(self._compile(data, options.m, options.type, options)); - this.queue(null); - } - - input.pipe(through(write, end)).pipe(output); - } - - processPath(filename, options) { - this.fs.stat(filename, function(err, stat) { - if (err) { - throw new Error(err); - } else if (stat.isDirectory()) { - this.processDirectory(filename, options); - } else { - this.processFile(filename, options); - } - }.bind(this)); - } - - processDirectory(dirname, options) { - this.fs.readdir(dirname, function(err, children) { - if (err) { - console.error(err.message); - process.exit(1); - } - children.forEach(function(child) { - this.processPath(path.join(dirname, child), options); - }.bind(this)); - }.bind(this)); - } - - processFile(filename, options) { - function normalizePath(p) { - return p.replace(/\\/g, '/'); - } - - var ext = path.extname(filename), - basenameNoExt = path.basename(filename, ext), - dirname = path.dirname(filename), - pathNoExt = normalizePath(path.join(dirname, basenameNoExt)), - output, - outputFilename = normalizePath(path.join(options.to, filename)), - moduleName = options['infer-name'] ? pathNoExt : null; - - options = extend({}, options, {m: moduleName}); - this._mkdirp(path.dirname(outputFilename)); - - this.processIO( - this.fs.createReadStream(filename), - this.fs.createWriteStream(outputFilename), - options - ); - } - - _compile(input, moduleName, type, options) { - var compiler, method; - type = { - amd: 'AMD', - yui: 'YUI', - cjs: 'CJS', - globals: 'Globals' - }[type]; - compiler = new this.Compiler(input, moduleName, options); - method = "to" + type; - return compiler[method](); - } - - _mkdirp(directory) { - var prefix; - if (this.fs.existsSync(directory)) { - return; - } - prefix = path.dirname(directory); - if (prefix !== '.' && prefix !== '/') { - this._mkdirp(prefix); - } - return this.fs.mkdirSync(directory); - } -} - -CLI.start = function(Compiler, argv, stdin=process.stdin, stdout=process.stdout, fs_=fs) { - return new CLI(Compiler, stdin, stdout, fs_).start(argv); -}; - -function requireMain() { - var root = path.join(__dirname, '..'), - pkgPath = path.join(root, 'package.json'), - pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - - return require(path.join(root, pkg.main)); -} - -let Compiler = requireMain().Compiler; - -CLI.start(Compiler, process.argv); diff --git a/lib/compile_error.js b/lib/compile_error.js deleted file mode 100644 index ad66130..0000000 --- a/lib/compile_error.js +++ /dev/null @@ -1,4 +0,0 @@ -class CompileError extends Error { -} - -export default CompileError; diff --git a/lib/compiler.js b/lib/compiler.js deleted file mode 100644 index e5847d9..0000000 --- a/lib/compiler.js +++ /dev/null @@ -1,82 +0,0 @@ -import AMDCompiler from './amd_compiler'; -import YUICompiler from './yui_compiler'; -import CJSCompiler from './cjs_compiler'; -import GlobalsCompiler from './globals_compiler'; -import { Unique } from './utils'; -import Parser from './parser'; - -/** - * Public interface to the transpiler. - * - * @class Compiler - * @constructor - * @param {String} string Input string. - * @param {String} moduleName The name of the module to output. - * @param {Object} options Configuration object. - */ -class Compiler { - constructor(string, moduleName, options) { - if (moduleName == null) { - moduleName = null; - } - - if (options == null) { - options = {}; - } - - this.string = string; - this.moduleName = moduleName; - this.options = options; - - this.inBlockComment = false; - this.reExportUnique = new Unique('reexport'); - - this.parse(); - } - - parse() { - var parser = new Parser(this.string); - this.imports = parser.imports; - this.exports = parser.exports; - this.exportDefault = parser.exportDefault; - this.directives = parser.directives; - } - - /** - * Transpiles an ES6 module to AMD. - * @method toAMD - * @return {String} The transpiled output - */ - toAMD() { - return new AMDCompiler(this, this.options).stringify(); - } - - /** - * Transpiles an ES6 module to YUI. - * @method toYUI - * @return {String} The transpiled output - */ - toYUI() { - return new YUICompiler(this, this.options).stringify(); - } - - /** - * Transpiles an ES6 module to CJS. - * @method toCJS - * @return {String} The transpiled output - */ - toCJS() { - return new CJSCompiler(this, this.options).stringify(); - } - - /** - * Transpiles an ES6 module to IIFE-wrapped globals. - * @method toGlobals - * @return {String} The transpiled output - */ - toGlobals() { - return new GlobalsCompiler(this, this.options).stringify(); - } -} - -export default Compiler; diff --git a/lib/container.js b/lib/container.js new file mode 100644 index 0000000..92aa494 --- /dev/null +++ b/lib/container.js @@ -0,0 +1,150 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); + +var Rewriter = require('./rewriter'); +var Writer = require('./writer'); + +/** + * Represents a container of modules for the given options. + * + * @constructor + * @param {{resolver: Resolver}} options + */ +function Container(options) { + var formatter = options && options.formatter; + if (typeof formatter === 'function') { + formatter = new formatter(); + } + + assert.ok( + formatter, + 'missing required option `formatter`' + ); + assert.equal( + typeof formatter.reference, 'function', + 'option `formatter` must have function `reference`' + ); + assert.equal( + typeof formatter.build, 'function', + 'option `formatter` must have function `build`' + ); + + var resolvers = options && options.resolvers; + + assert.ok( + resolvers && resolvers.length > 0, + 'at least one resolver is required' + ); + resolvers.forEach(function(resolver) { + assert.equal( + typeof resolver.resolveModule, 'function', + '`resolver` must have `resolveModule` function: ' + resolver + ); + }); + + Object.defineProperties(this, { + modules: { + value: Object.create(null), + enumerable: false + }, + + formatter: { + value: formatter, + enumerable: false + }, + + resolvers: { + value: resolvers, + enumerable: false + }, + + options: { + value: options, + enumerable: false + } + }); +} + +/** + * Gets a module by resolving `path`. If `path` is resolved to the same path + * as a previous call, the same object will be returned. + * + * @param {string} importedPath + * @param {?Module} fromModule + * @return {Module} + */ +Container.prototype.getModule = function(importedPath, fromModule) { + for (var i = 0, length = this.resolvers.length; i < length; i++) { + var resolvedModule = this.resolvers[i].resolveModule( + importedPath, + fromModule, + this + ); + + if (resolvedModule) { + this.modules[resolvedModule.path] = resolvedModule; + return resolvedModule; + } + } + + throw new Error( + 'missing module import' + + (fromModule ? 'from ' + fromModule.relativePath : '') + + ' for path: ' + importedPath + ); +}; + +/** + * Get a cached module by a resolved path. + * + * @param {string} resolvedPath + * @return {?Module} + */ +Container.prototype.getCachedModule = function(resolvedPath) { + return this.modules[resolvedPath]; +}; + +Container.prototype.write = function(target) { + var files = this.convert(); + var writer = new Writer(target); + writer.write(files); +}; + +Container.prototype.convert = function() { + if (this.formatter.beforeConvert) { + this.formatter.beforeConvert(this); + } + + var modules = this.getModules(); + + var rewriter = new Rewriter(this.formatter); + rewriter.rewrite(modules); + + var formatter = this.formatter; + return formatter.build(modules); +}; + +Container.prototype.findImportedModules = function() { + var knownModules; + var lastModuleCount = 0; + + while ((knownModules = this.getModules()).length !== lastModuleCount) { + lastModuleCount = knownModules.length; + for (var i = 0; i < lastModuleCount; i++) { + // Force loading of imported modules. + /* jshint expr:true */ + knownModules[i].imports.modules; + /* jshint expr:false */ + } + } +}; + +Container.prototype.getModules = function() { + var modules = this.modules; + return Object.keys(modules).map(function(key) { + return modules[key]; + }); +}; + +module.exports = Container; diff --git a/lib/declaration_info.js b/lib/declaration_info.js new file mode 100644 index 0000000..c0e897d --- /dev/null +++ b/lib/declaration_info.js @@ -0,0 +1,72 @@ +/* jshint node:true, undef:true, unused:true */ + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; + +/** + * Represents information about a declaration that creates a local binding + * represented by `identifier`. For example, given that `declaration` is the + * following variable declaration: + * + * var a = 1; + * + * Then `identifier` references the `a` node in the variable declaration's + * first declarator. Likewise, given that `declaration` is this function + * declaration: + * + * function add(a, b) {} + * + * Then `identifier` references the `add` node, the declaration's `id`. + * + * @constructor + * @param {ast-types.Node} declaration + * @param {ast-types.Identifier} identifier + */ +function DeclarationInfo(declaration, identifier) { + /** + * @type {ast-types.Node} + * @property declaration + */ + this.declaration = declaration; + /** + * @type {ast-types.Identifier} + * @property identifier + */ + this.identifier = identifier; +} + +/** + * Get the declaration info for the given identifier path, if the identifier is + * actually part of a declaration. + * + * @param {ast-types.NodePath} identifierPath + * @return {?DeclarationInfo} + */ +DeclarationInfo.forIdentifierPath = function(identifierPath) { + if (n.VariableDeclarator.check(identifierPath.parent.node)) { + return new DeclarationInfo( + identifierPath.parent.parent.node, + identifierPath.node + ); + } else if (n.ClassDeclaration.check(identifierPath.parent.node)) { + return new DeclarationInfo( + identifierPath.parent.node, + identifierPath.node + ); + } else if (n.FunctionDeclaration.check(identifierPath.parent.node)) { + return new DeclarationInfo( + identifierPath.parent.node, + identifierPath.node + ); + } else if (n.ImportSpecifier.check(identifierPath.parent.node)) { + return new DeclarationInfo( + identifierPath.parent.parent.node, + identifierPath.node + ); + } else { + return null; + } +}; + +module.exports = DeclarationInfo; diff --git a/lib/exports.js b/lib/exports.js new file mode 100644 index 0000000..91438e9 --- /dev/null +++ b/lib/exports.js @@ -0,0 +1,319 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; + +var ModuleBindingList = require('./module_binding_list'); +var ModuleBindingDeclaration = require('./module_binding_declaration'); +var ModuleBindingSpecifier = require('./module_binding_specifier'); +var DeclarationInfo = require('./declaration_info'); + +var utils = require('./utils'); +var memo = utils.memo; +var extend = utils.extend; +var sourcePosition = utils.sourcePosition; + +/** + * Represents a list of the exports for the given module. + * + * @constructor + * @param {Module} mod + */ +function ExportDeclarationList(mod) { + ModuleBindingList.call(this, mod); +} +extend(ExportDeclarationList, ModuleBindingList); + +/** + * @private + * @param {ast-types.Node} node + * @return {boolean} + */ +ExportDeclarationList.prototype.isMatchingBinding = function(node) { + return n.ExportDeclaration.check(node); +}; + +/** + * Gets an export declaration for the given `node`. + * + * @private + * @param {ast-types.ExportDeclaration} node + * @return {Import} + */ +ExportDeclarationList.prototype.declarationForNode = function(node) { + if (node.default) { + return new DefaultExportDeclaration(this.module, node); + } else if (n.VariableDeclaration.check(node.declaration)) { + return new VariableExportDeclaration(this.module, node); + } else if (n.FunctionDeclaration.check(node.declaration)) { + return new FunctionExportDeclaration(this.module, node); + } else if (n.ExportBatchSpecifier.check(node.specifiers[0])) { + throw new Error( + '`export *` found at ' + sourcePosition(this.module, node) + + ' is not supported, please use `export { … }` instead' + ); + } else { + return new NamedExportDeclaration(this.module, node); + } +}; + +/** + * @param {ast-types.NodePath} referencePath + * @return {?ExportSpecifier} + */ +ExportDeclarationList.prototype.findSpecifierForReference = function(referencePath) { + if (n.ExportSpecifier.check(referencePath.parent.node) && referencePath.parent.parent.node.source) { + // This is a direct export from another module, e.g. `export { foo } from 'foo'`. + return this.findSpecifierByIdentifier(referencePath.node); + } + + var declaration = this.findDeclarationForReference(referencePath); + + if (!declaration) { + return null; + } + + var specifier = this.findSpecifierByName(declaration.node.name); + assert.ok( + specifier, + 'no specifier found for `' + referencePath.node.name + '`! this should not happen!' + ); + return specifier; +}; + +/** + * Contains information about an export declaration. + * + * @constructor + * @abstract + * @extends ModuleBindingDeclaration + * @param {Module} mod + * @param {ast-types.ExportDeclaration} node + */ +function ExportDeclaration(mod, node) { + assert.ok( + n.ExportDeclaration.check(node), + 'expected an export declaration, got ' + (node && node.type) + ); + + ModuleBindingDeclaration.call(this, mod, node); +} +extend(ExportDeclaration, ModuleBindingDeclaration); + +/** + * Returns a string description suitable for debugging. + * + * @return {string} + */ +ExportDeclaration.prototype.inspect = function() { + return recast.print(this.node).code; +}; + +/** + * @alias inspect + */ +ExportDeclaration.prototype.toString = ExportDeclaration.prototype.inspect; + +/** + * Represents an export declaration of the form: + * + * export default foo; + * + * @constructor + * @extends ExportDeclaration + * @param {Module} mod + * @param {ast-types.ExportDeclaration} node + */ +function DefaultExportDeclaration(mod, node) { + ExportDeclaration.call(this, mod, node); +} +extend(DefaultExportDeclaration, ExportDeclaration); + +/** + * Contains a list of specifier name information for this export. + * + * @type {Array.} + * @property specifiers + */ +memo(DefaultExportDeclaration.prototype, 'specifiers', function() { + var specifier = new DefaultExportSpecifier(this, this.node.declaration); + return [specifier]; +}); + +/** + * Represents an export declaration of the form: + * + * export { foo, bar }; + * + * @constructor + * @extends ExportDeclaration + * @param {Module} mod + * @param {ast-types.ExportDeclaration} node + */ +function NamedExportDeclaration(mod, node) { + ExportDeclaration.call(this, mod, node); +} +extend(NamedExportDeclaration, ExportDeclaration); + +/** + * Contains a list of specifier name information for this export. + * + * @type {Array.} + * @property specifiers + */ +memo(NamedExportDeclaration.prototype, 'specifiers', function() { + var self = this; + return this.node.specifiers.map(function(specifier) { + return new ExportSpecifier(self, specifier); + }); +}); + +/** + * Represents an export declaration of the form: + * + * export var foo = 1; + * + * @constructor + * @extends ExportDeclaration + * @param {Module} mod + * @param {ast-types.ExportDeclaration} node + */ +function VariableExportDeclaration(mod, node) { + ExportDeclaration.call(this, mod, node); +} +extend(VariableExportDeclaration, ExportDeclaration); + +/** + */ +memo(VariableExportDeclaration.prototype, 'specifiers', function() { + var self = this; + return this.node.declaration.declarations.map(function(declarator) { + return new ExportSpecifier(self, declarator); + }); +}); + +/** + * Represents an export declaration of the form: + * + * export function foo() {} + * + * @constructor + * @extends ExportDeclaration + * @param {Module} mod + * @param {ast-types.ExportDeclaration} node + */ +function FunctionExportDeclaration(mod, node) { + ExportDeclaration.call(this, mod, node); +} +extend(FunctionExportDeclaration, ExportDeclaration); + +/** + */ +memo(FunctionExportDeclaration.prototype, 'specifiers', function() { + return [new ExportSpecifier(this, this.node.declaration)]; +}); + +/** + * Represents an export specifier in an export declaration. + * + * @constructor + * @extends ModuleBindingSpecifier + * @param {ExportDeclaration} declaration + * @param {ast-types.ExportSpecifier} node + */ +function ExportSpecifier(declaration, node) { + ModuleBindingSpecifier.call(this, declaration, node); +} +extend(ExportSpecifier, ModuleBindingSpecifier); + +/** + * Contains the local declaration info for this export specifier. For example, + * in this module: + * + * var a = 1; + * export { a }; + * + * The module declaration info for the `a` export specifier is the variable + * declaration plus the `a` identifier in its first declarator. + * + * @type {DeclarationInfo} + * @property moduleDeclaration + */ +memo(ExportSpecifier.prototype, 'moduleDeclaration', function() { + if (this.declaration.source) { + // This is part of a direct export, e.g. `export { ... } from '...'`, so + // there is no declaration as part of this module. + return null; + } + + var bindings = this.moduleScope.getBindings(); + var identifierPaths = bindings[this.from]; + assert.ok( + identifierPaths && identifierPaths.length === 1, + 'expected exactly one declaration for export `' + + this.from + '` at ' + sourcePosition(this.module, this.node) + + ', found ' + (identifierPaths ? identifierPaths.length : 'none') + ); + + var identifierPath = identifierPaths[0]; + var declarationInfo = DeclarationInfo.forIdentifierPath(identifierPath); + + assert.ok( + declarationInfo, + 'cannot detect declaration for `' + + identifierPath.node.name + '`, found parent.type `' + + identifierPath.parent.node.type + '`' + ); + + return declarationInfo; +}); + +/** + * Represents an export specifier in a default export declaration. + * + * @constructor + * @extends ExportSpecifier + * @param {ExportDeclaration} declaration + * @param {ast-types.ExportSpecifier} node + */ +function DefaultExportSpecifier(declaration, node) { + ExportSpecifier.call(this, declaration, node); +} +extend(DefaultExportSpecifier, ExportSpecifier); + +/** + * Default export specifier names are always "default". + * + * @type {string} + * @property name + */ +DefaultExportSpecifier.prototype.name = 'default'; + +/** + * Default export specifiers do not bind to a local identifier. + * + * @type {?ast-types.Identifier} + * @property identifier + */ +DefaultExportSpecifier.prototype.identifier = null; + +/** + * Default export specifiers do not have a local bound name. + * + * @type {?string} + * @property from + */ +DefaultExportSpecifier.prototype.from = null; + +/** + * Default export specifiers do not have a local declaration. + * + * @type {?DeclarationInfo} + * @property moduleDeclaration + */ +DefaultExportSpecifier.prototype.moduleDeclaration = null; + +module.exports = ExportDeclarationList; diff --git a/lib/file_resolver.js b/lib/file_resolver.js new file mode 100644 index 0000000..139ae93 --- /dev/null +++ b/lib/file_resolver.js @@ -0,0 +1,78 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); +var Path = require('path'); +var fs = require('fs'); + +var Module = require('./module'); + +/** + * Provides resolution of absolute paths from module import sources. + * + * @constructor + */ +function FileResolver(paths) { + assert.ok( + paths && paths.length > 0, + 'missing required argument `paths`' + ); + + this.paths = paths.map(function(path) { + return Path.resolve(path); + }); +} + +/** + * Resolves `importedPath` imported by the given module `fromModule` to a + * module. + * + * @param {string} importedPath + * @param {?Module} fromModule + * @param {Container} container + * @return {?Module} + */ +FileResolver.prototype.resolveModule = function(importedPath, fromModule, container) { + var resolvedPath = this.resolvePath(importedPath, fromModule); + if (resolvedPath) { + var cachedModule = container.getCachedModule(resolvedPath); + if (cachedModule) { + return cachedModule; + } else { + return new Module(resolvedPath, importedPath, container); + } + } else { + return null; + } +}; + +/** + * Resolves `importedPath` against the importing module `fromModule`, if given, + * within this resolver's paths. + * + * @private + * @param {string} importedPath + * @param {?Module} fromModule + * @return {string} + */ +FileResolver.prototype.resolvePath = function(importedPath, fromModule) { + var paths = this.paths; + + if (importedPath[0] === '.' && fromModule) { + paths = [Path.dirname(fromModule.path)]; + } + + for (var i = 0, length = paths.length; i < length; i++) { + var includePath = paths[i]; + var resolved = Path.resolve(includePath, importedPath); + if (resolved.slice(-3).toLowerCase() !== '.js') { + resolved += '.js'; + } + if (fs.existsSync(resolved)) { + return resolved; + } + } + + return null; +}; + +module.exports = FileResolver; diff --git a/lib/formatters.js b/lib/formatters.js new file mode 100644 index 0000000..a447e9b --- /dev/null +++ b/lib/formatters.js @@ -0,0 +1,6 @@ +/* jshint node:true, undef:true, unused:true */ + +exports.DEFAULT = 'module-variable'; +exports['module-variable'] = require('./formatters/module_variable_formatter'); +exports['export-variable'] = require('./formatters/export_variable_formatter'); +exports.commonjs = require('./formatters/commonjs_formatter'); diff --git a/lib/formatters/commonjs_formatter.js b/lib/formatters/commonjs_formatter.js new file mode 100644 index 0000000..a7e7ce5 --- /dev/null +++ b/lib/formatters/commonjs_formatter.js @@ -0,0 +1,333 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; +var b = types.builders; + +var Replacement = require('../replacement'); + +/** + * The 'commonjs' setting for referencing exports aims to produce code that can + * be used in environments using the CommonJS module system, such as Node.js. + * + * @constructor + */ +function CommonJSFormatter() {} + +/** + * Returns an expression which globally references the export named by + * `identifier` for the given module `mod`. For example: + * + * // rsvp/defer.js, export default + * rsvp$defer$$.default + * + * // rsvp/utils.js, export function isFunction + * rsvp$utils$$.isFunction + * + * @param {Module} mod + * @param {ast-types.Identifier} identifier + * @return {ast-types.MemberExpression} + */ +CommonJSFormatter.prototype.reference = function(mod, identifier) { + return b.memberExpression( + b.identifier(mod.id), + n.Identifier.check(identifier) ? identifier : b.identifier(identifier), + false + ); +}; + +/** + * Process a variable declaration found at the top level of the module. Since + * we do not need to rewrite exported variables, we can leave variable + * declarations alone. + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Array.} + */ +CommonJSFormatter.prototype.processVariableDeclaration = function(/* mod, nodePath */) { + return null; +}; + +/** + * Process a variable declaration found at the top level of the module. Since + * we do not need to rewrite exported functions, we can leave function + * declarations alone. + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @returns {Array.} + */ +CommonJSFormatter.prototype.processFunctionDeclaration = function(mod, nodePath) { + return null; +}; + +/** + * Because exported references are captured via a closure as part of a getter + * on the `exports` object, there's no need to rewrite local references to + * exported values. For example, `value` in this example can stay as is: + * + * // a.js + * export var value = 1; + * + * @param {Module} mod + * @param {ast-types.NodePath} referencePath + * @return {ast-types.Expression} + */ +CommonJSFormatter.prototype.exportedReference = function(/* mod, referencePath */) { + return null; +}; + +/** + * Gets a reference to an imported binding by getting the value from the + * required module on demand. For example, this module: + * + * // b.js + * import { value } from './a'; + * console.log(value); + * + * Would be rewritten to look something like this: + * + * var a$$ = require('./a'); + * console.log(a$$.value): + * + * If the given reference does not refer to an imported binding then no + * rewriting is required and `null` will be returned. + * + * @param {Module} mod + * @param {ast-types.NodePath} referencePath + * @return {?ast-types.Expression} + */ +CommonJSFormatter.prototype.importedReference = function(mod, referencePath) { + var specifier = mod.imports.findSpecifierForReference(referencePath); + + if (specifier) { + return this.reference( + specifier.declaration.source, + specifier.from + ); + } else { + return null; + } +}; + +/** + * @param {Module} mod + * @param {ast-types.Expression} declaration + * @return {ast-types.Statement} + */ +CommonJSFormatter.prototype.defaultExport = function(mod, declaration) { + return b.expressionStatement( + b.assignmentExpression( + '=', + b.memberExpression( + b.identifier('exports'), + b.identifier('default'), + false + ), + declaration + ) + ); +}; + +/** + * Replaces non-default exports. For declarations we simply remove the `export` + * keyword. For export declarations that just specify bindings, e.g. + * + * export { a, b }; + * + * we remove them entirely since they'll be handled when we define properties on + * the `exports` object. + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Replacement} + */ +CommonJSFormatter.prototype.processExportDeclaration = function(mod, nodePath) { + var node = nodePath.node; + + if (n.FunctionDeclaration.check(node.declaration)) { + return Replacement.swaps(nodePath, node.declaration); + } else if (n.VariableDeclaration.check(node.declaration)) { + return Replacement.swaps(nodePath, node.declaration); + } else if (n.ClassDeclaration.check(node.declaration)) { + return Replacement.swaps(nodePath, node.declaration); + } else if (node.declaration) { + throw new Error('unexpected export style, found a declaration of type: ' + node.declaration.type); + } else { + return Replacement.removes(nodePath); + } +}; + +/** + * Since import declarations only control how we rewrite references we can just + * remove them -- they don't turn into any actual statements. + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Replacement} + */ +CommonJSFormatter.prototype.processImportDeclaration = function(mod, nodePath) { + return Replacement.removes(nodePath); +}; + +/** + * Convert a list of ordered modules into a list of files. + * + * @param {Array.} modules Modules in execution order. + * @return {Array.= 0) { + return; + } + requiredModules.push(sourceModule); + + var matchingDeclaration; + declarations.declarations.some(function(declaration) { + if (declaration.source === sourceModule) { + matchingDeclaration = declaration; + return true; + } + }); + + assert.ok( + matchingDeclaration, + 'no matching declaration for source module: ' + sourceModule.relativePath + ); + + // `(import|export) { ... } from 'math'` -> `math$$ = require('math')` + declarators.push(b.variableDeclarator( + b.identifier(sourceModule.id), + b.callExpression( + b.identifier('require'), + [b.literal(matchingDeclaration.sourcePath)] + ) + )); + }); + }); + + if (declarators.length > 0) { + return b.variableDeclaration('var', declarators); + } else { + return b.emptyStatement(); + } +}; + +/** + * @private + * @param {Module} mod + * @return {ast-types.Statement} + */ +CommonJSFormatter.prototype.buildExports = function(mod) { + var self = this; + var properties = []; + + mod.exports.names.forEach(function(name) { + var specifier = mod.exports.findSpecifierByName(name); + + assert.ok( + specifier, + 'no export specifier found for export name `' + + name + '` from ' + mod.relativePath + ); + + if (!specifier.from) { + return; + } + + var from = + specifier.importSpecifier ? + self.reference( + specifier.importSpecifier.declaration.source, + specifier.importSpecifier.from + ) : + specifier.declaration.source ? + self.reference( + specifier.declaration.source, + specifier.name + ) : + b.identifier(specifier.from); + + properties.push(b.property( + 'init', + b.identifier(name), + b.objectExpression([ + // Simulate named export bindings with a getter. + b.property( + 'init', + b.identifier('get'), + b.functionExpression( + null, + [], + b.blockStatement([b.returnStatement(from)]) + ) + ), + b.property( + 'init', + b.identifier('enumerable'), + b.literal(true) + ) + ]) + )); + }); + + var exportObject = b.identifier('exports'); + + if (properties.length > 0) { + exportObject = b.callExpression( + b.memberExpression( + b.identifier('Object'), + b.identifier('defineProperties'), + false + ), + [ + exportObject, + b.objectExpression(properties) + ] + ); + } + + return b.expressionStatement( + b.callExpression( + b.memberExpression( + b.identifier('Object'), + b.identifier('seal'), + false + ), + [exportObject] + ) + ); +}; + +module.exports = CommonJSFormatter; diff --git a/lib/formatters/export_variable_formatter.js b/lib/formatters/export_variable_formatter.js new file mode 100644 index 0000000..57cb189 --- /dev/null +++ b/lib/formatters/export_variable_formatter.js @@ -0,0 +1,102 @@ +/* jshint node:true, undef:true, unused:true */ + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; +var b = types.builders; + +var utils = require('../utils'); +var extend = utils.extend; + +var VariableFormatterBase = require('./variable_formatter_base'); + +/** + * The 'export-variable' setting for referencing exports aims to increase the + * compressability of the generated source, especially by tools such as Google + * Closure Compiler or UglifyJS. For example, given these modules: + * + * // a.js + * import { b } from './b'; + * console.log(b); + * + * // b.js + * export var b = 3; + * export var b2 = 6; + * + * The final output will be a single file looking something like this: + * + * (function() { + * var b$$b, b$$b2; + * + * (function() { + * // b.js + * b$$b = 3; + * b$$b2 = 6; + * })(); + * + * (function() { + * // a.js + * console.log(b$$b); + * })(); + * })(); + * + * @constructor + * @extends VariableFormatterBase + */ +function ExportVariableFormatter() {} +extend(ExportVariableFormatter, VariableFormatterBase); + +/** + * Returns an expression which globally references the export named by + * `identifier` for the given module `mod`. For example: + * + * // rsvp/defer.js, export default + * rsvp$defer$$default + * + * // rsvp/utils.js, export function isFunction + * rsvp$utils$$isFunction + * + * @param {Module} mod + * @param {ast-types.Identifier|string} identifier + * @return {ast-types.Identifier} + */ +ExportVariableFormatter.prototype.reference = function(mod, identifier) { + return b.identifier( + mod.id + (n.Identifier.check(identifier) ? identifier.name : identifier) + ); +}; + +/** + * Returns a declaration for the exports for the given module. In this case, + * it will have one declarator per exported name, prefixed with the module's + * id. For example: + * + * // rsvp/defer.js, export default + * var rsvp$defer$$default; + * + * // rsvp/utils.js, export function isFunction + * var rsvp$utils$$isFunction; + * + * @param {Array.} modules + * @return {ast-types.VariableDeclaration} + */ +ExportVariableFormatter.prototype.variableDeclaration = function(modules) { + var declarators = []; + var self = this; + + modules.forEach(function(mod) { + mod.exports.names.forEach(function(name) { + declarators.push( + b.variableDeclarator(self.reference(mod, name), null) + ); + }); + }); + + if (declarators.length > 0) { + return b.variableDeclaration('var', declarators); + } else { + return b.emptyStatement(); + } +}; + +module.exports = ExportVariableFormatter; diff --git a/lib/formatters/module_variable_formatter.js b/lib/formatters/module_variable_formatter.js new file mode 100644 index 0000000..ec503f9 --- /dev/null +++ b/lib/formatters/module_variable_formatter.js @@ -0,0 +1,97 @@ +/* jshint node:true, undef:true, unused:true */ + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; +var b = types.builders; + +var utils = require('../utils'); +var extend = utils.extend; + +var VariableFormatterBase = require('./variable_formatter_base'); + +/** + * The 'module-variable' setting for referencing exports aims to reduce the + * number of variables in the outermost IFFE scope. This avoids potentially + * quadratic performance degradations as shown by + * https://gist.github.com/joliss/9331281. For example, given these modules: + * + * // a.js + * import { b } from './b'; + * console.log(b); + * + * // b.js + * export var b = 3; + * export var b2 = 6; + * + * The final output will be a single file looking something like this: + * + * (function() { + * var b$$ = {}; + * + * (function() { + * // b.js + * b$$.b = 3; + * b$$.b2 = 6; + * })(); + * + * (function() { + * // a.js + * console.log(b$$.b); + * })(); + * })(); + * + * @constructor + * @extends VariableFormatterBase + */ +function ModuleVariableFormatter() {} +extend(ModuleVariableFormatter, VariableFormatterBase); + +/** + * Returns an expression which globally references the export named by + * `identifier` for the given module `mod`. For example: + * + * // rsvp/defer.js, export default + * rsvp$defer$$.default + * + * // rsvp/utils.js, export function isFunction + * rsvp$utils$$.isFunction + * + * @param {Module} mod + * @param {ast-types.Identifier} identifier + * @return {ast-types.MemberExpression} + */ +ModuleVariableFormatter.prototype.reference = function(mod, identifier) { + return b.memberExpression( + b.identifier(mod.id), + n.Identifier.check(identifier) ? identifier : b.identifier(identifier), + false + ); +}; + +/** + * Returns a declaration for the exports for the given modules. In this case, + * this is always just the the module objects (e.g. `var rsvp$defer$$ = {};`). + * + * @param {Array.} modules + * @return {ast-types.VariableDeclaration} + */ +ModuleVariableFormatter.prototype.variableDeclaration = function(modules) { + var declarators = []; + + modules.forEach(function(mod) { + if (mod.exports.names.length > 0) { + declarators.push( + b.variableDeclarator(b.identifier(mod.id), b.objectExpression([])) + ); + } + }); + + if (declarators.length > 0) { + return b.variableDeclaration('var', declarators); + } else { + return b.emptyStatement(); + } +}; + +module.exports = ModuleVariableFormatter; diff --git a/lib/formatters/variable_formatter_base.js b/lib/formatters/variable_formatter_base.js new file mode 100644 index 0000000..d0d0f23 --- /dev/null +++ b/lib/formatters/variable_formatter_base.js @@ -0,0 +1,396 @@ +/* jshint node:true, undef:true, unused:true */ + +var recast = require('recast'); +var types = recast.types; +var b = types.builders; +var n = types.namedTypes; + +var Replacement = require('../replacement'); +var utils = require('../utils'); +var IFFE = utils.IFFE; +var sort = require('../sorting').sort; + + +/** + * The variable class of export strategies concatenate all modules together, + * isolating their local variables. All exports and imports are rewritten to + * reference variables in a shared parent scope. For example, given these + * modules: + * + * // a.js + * import { b } from './b'; + * + * // b.js + * export var b = 3; + * + * The final output will be a single file looking something like this: + * + * (function() { + * // variable declarations here + * + * (function() { + * // b.js, sets variables declared above + * })(); + * + * (function() { + * // a.js, references variables declared above + * })(); + * })(); + * + * @constructor + */ +function VariableFormatterBase() {} + +/** + * This hook is called by the container before it converts its modules. We use + * it to ensure all of the imports are included because we need to know about + * them at compile time. + * + * @param {Container} container + */ +VariableFormatterBase.prototype.beforeConvert = function(container) { + container.findImportedModules(); +}; + +/** + * Returns an expression which globally references the export named by + * `identifier` for the given module `mod`. + * + * @param {Module} mod + * @param {ast-types.Identifier|string} identifier + * @return {ast-types.MemberExpression} + */ +VariableFormatterBase.prototype.reference = function(/* mod, identifier */) { + throw new Error('#reference must be implemented in subclasses'); +}; + +/** + * Process a variable declaration found at the top level of the module. We need + * to ensure that exported variables are rewritten appropriately, so we may + * need to rewrite some or all of this variable declaration. For example: + * + * var a = 1, b, c = 3; + * ... + * export { a, b }; + * + * We turn those being exported into assignments as needed, e.g. + * + * var c = 3; + * mod$$a = 1; + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Replacement} + */ +VariableFormatterBase.prototype.processVariableDeclaration = function(mod, nodePath) { + var exports = mod.exports; + var node = nodePath.node; + var self = this; + var declarators = []; + var assignments = []; + var exportSpecifier; + + node.declarations.forEach(function(declarator) { + exportSpecifier = exports.findSpecifierByName(declarator.id.name); + if (exportSpecifier) { + // This variable is exported, turn it into a normal assignment. + if (declarator.init) { + // But only if we have something to assign with. + assignments.push( + b.assignmentExpression( + '=', + self.reference(mod, declarator.id), + declarator.init + ) + ); + } + } else { + // This variable is not exported, so keep it. + declarators.push(declarator); + } + }); + + // Don't bother replacing it if we kept all declarators. + if (node.declarations.length === declarators.length) { + return null; + } + + var nodes = []; + + // Do we need a variable declaration at all? + if (declarators.length > 0) { + nodes.push( + b.variableDeclaration(node.kind, declarators) + ); + } + + // Do we have any assignments to add? + if (assignments.length > 0) { + nodes.push(b.expressionStatement( + b.sequenceExpression(assignments) + )); + } + return Replacement.swaps(nodePath, nodes); +}; + +/** + * Appends an assignment after a function declaration if that function is exported. For example, + * + * function foo() {} + * // ... + * export { foo }; + * + * Becomes e.g. + * + * function foo() {} + * mod$$foo = foo; + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Replacement} + */ +VariableFormatterBase.prototype.processFunctionDeclaration = function(mod, nodePath) { + var exports = mod.exports; + var node = nodePath.node; + var exportSpecifier = exports.findSpecifierByName(node.id.name); + + // No need for an assignment to an export if it isn't exported. + if (!exportSpecifier) { + return null; + } + + // Add an assignment, e.g. `mod$$foo = foo`. + var assignment = b.expressionStatement( + b.assignmentExpression( + '=', + this.reference(mod, exportSpecifier.name), + node.id + ) + ); + + return Replacement.adds(nodePath, assignment); +}; + +/** + * Replaces non-default exports. Since we mostly rewrite references we may need + * to deconstruct variable declarations. Exported bindings do not need to be + * replaced with actual statements since they only control reference rewrites. + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Replacement} + */ +VariableFormatterBase.prototype.processExportDeclaration = function(mod, nodePath) { + var node = nodePath.node; + var self = this; + if (n.FunctionDeclaration.check(node.declaration)) { + /** + * Function exports are declarations and so should not be + * re-assignable, so we can safely turn remove the `export` keyword and + * add an assignment. For example: + * + * export function add(a, b) { return a + b; } + * + * Becomes: + * + * function add(a, b) { return a + b; } + * mod$$.add = add; + */ + return Replacement.swaps( + nodePath, + [ + node.declaration, + b.expressionStatement( + b.assignmentExpression( + '=', + this.reference(mod, node.declaration.id), + node.declaration.id + ) + ) + ] + ); + } else if (n.VariableDeclaration.check(node.declaration)) { + /** + * Variable exports can be re-assigned, so we need to rewrite the + * names. For example: + * + * export var a = 1, b = 2; + * + * Becomes: + * + * mod$$.a = 1, mod$$.b = 2; + */ + return Replacement.swaps( + nodePath, + b.expressionStatement( + b.sequenceExpression( + node.declaration.declarations.reduce(function(assignments, declarator) { + if (declarator.init) { + assignments.push( + b.assignmentExpression( + '=', + self.reference(mod, declarator.id), + declarator.init + ) + ); + } + return assignments; + }, []) + ) + ) + ); + } else if (node.declaration) { + throw new Error('unexpected export style, found a declaration of type: ' + node.declaration.type); + } else { + /** + * For exports with a named specifier list we handle them by re-writing + * their declaration (if it's in this module) and their references, so + * the export declaration can be safely removed. + */ + return Replacement.removes(nodePath); + } +}; + +/** + * Since import declarations only control how we rewrite references we can just + * remove them -- they don't turn into any actual statements. + * + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {?Replacement} + */ +VariableFormatterBase.prototype.processImportDeclaration = function(mod, nodePath) { + return Replacement.removes(nodePath); +}; + +/** + * Get a reference to the original exported value referenced in `mod` at + * `referencePath`. If the given reference path does not correspond to an + * export, we do not need to rewrite the reference. For example, since `value` + * is not exported it does not need to be rewritten: + * + * // a.js + * var value = 99; + * console.log(value); + * + * If `value` was exported then we would need to rewrite it: + * + * // a.js + * export var value = 3; + * console.log(value); + * + * In this case we re-write both `value` references to someting like + * `a$$value`. The tricky part happens when we re-export an imported binding: + * + * // a.js + * export var value = 11; + * + * // b.js + * import { value } from './a'; + * export { value }; + * + * // c.js + * import { value } from './b'; + * console.log(value); + * + * The `value` reference in a.js will be rewritten as something like `a$$value` + * as expected. The `value` reference in c.js will not be rewritten as + * `b$$value` despite the fact that it is imported from b.js. This is because + * we must follow the binding through to its import from a.js. Thus, our + * `value` references will both be rewritten to `a$$value` to ensure they + * match. + * + * @param {Module} mod + * @param {ast-types.NodePath} referencePath + * @return {ast-types.Expression} + */ +VariableFormatterBase.prototype.exportedReference = function(mod, referencePath) { + var specifier = mod.exports.findSpecifierForReference(referencePath); + if (specifier) { + specifier = specifier.terminalExportSpecifier; + return this.reference(specifier.module, specifier.name); + } else { + return null; + } +}; + +/** + * Get a reference to the original exported value referenced in `mod` at + * `referencePath`. This is very similar to {#exportedReference} in its + * approach. + * + * @param {Module} mod + * @param {ast-types.NodePath} referencePath + * @return {ast-types.Expression} + * @see {#exportedReference} + */ +VariableFormatterBase.prototype.importedReference = function(mod, referencePath) { + var specifier = mod.imports.findSpecifierForReference(referencePath); + if (specifier) { + specifier = specifier.terminalExportSpecifier; + return this.reference(specifier.module, specifier.name); + } else { + return null; + } +}; + +/** + * Convert a list of ordered modules into a list of files. + * + * @param {Array.} modules Modules in execution order. + * @return {Array.} modules + * @return {ast-types.VariableDeclaration} + */ +VariableFormatterBase.prototype.variableDeclaration = function(modules) { + var declarators = []; + + modules.forEach(function(mod) { + if (mod.exports.names.length > 0) { + declarators.push( + b.variableDeclarator(b.identifier(mod.id), b.objectExpression([])) + ); + } + }); + + if (declarators.length > 0) { + return b.variableDeclaration('var', declarators); + } else { + return b.emptyStatement(); + } +}; + +module.exports = VariableFormatterBase; diff --git a/lib/globals_compiler.js b/lib/globals_compiler.js deleted file mode 100644 index 8680902..0000000 --- a/lib/globals_compiler.js +++ /dev/null @@ -1,123 +0,0 @@ -import AbstractCompiler from './abstract_compiler'; -import SourceModifier from './source_modifier'; - -class GlobalsCompiler extends AbstractCompiler { - stringify() { - var string = this.string.toString(); // string is actually a node buffer - this.source = new SourceModifier(string); - - this.map = []; - var out = this.buildPreamble(this.exports.length > 0); - - this.buildImports(); - this.buildExports(); - - if (!this.options.imports) this.options.imports = {}; - if (!this.options.global) this.options.global = "window"; - - out += this.indentLines(); - out += "\n})"; - out += this.buildSuffix(); - out += ";"; - - return out; - } - - buildPreamble() { - var out = "", - dependencyNames = this.dependencyNames; - - out += "(function("; - - if (this.exports.length > 0) { - out += "__exports__"; - if (this.dependencyNames.length > 0) { - out += ', '; - } - } - - for (var idx = 0; idx < dependencyNames.length; idx++) { - out += `__dependency${idx+1}__`; - this.map[dependencyNames[idx]] = idx+1; - if (!(idx === dependencyNames.length - 1)) out += ", "; - } - - out += ") {\n"; - - out += ' "use strict";\n'; - - return out; - } - - buildSuffix() { - var dependencyNames = this.dependencyNames; - var out = "("; - - if (this.exports.length > 0) { - if (this.options.into) { - out += `${this.options.global}.${this.options.into} = {}`; - } else { - out += this.options.global; - } - if (this.dependencyNames.length > 0) { - out += ', '; - } - } - - for (var idx = 0; idx < dependencyNames.length; idx++) { - var name = dependencyNames[idx]; - out += `${this.options.global}.${this.options.imports[name] || name}`; - if (!(idx === dependencyNames.length - 1)) out += ", "; - } - - out += ")"; - return out; - } - - doModuleImport(name, dependencyName, idx) { - return `var ${name} = __dependency${this.map[dependencyName]}__;\n`; - } - - doBareImport(name) { - return ""; - } - - doDefaultImport(name, dependencyName, idx) { - return `var ${name} = __dependency${this.map[dependencyName]}__;\n`; - } - - doNamedImport(name, dependencyName, alias) { - return `var ${alias} = __dependency${this.map[dependencyName]}__.${name};\n`; - } - - doExportSpecifier(name, reexport) { - if (reexport) { - return `__exports__.${name} = __dependency${this.map[reexport]}__.${name};\n`; - } - return `__exports__.${name} = ${name};\n`; - } - - doExportDeclaration(name) { - return `\n__exports__.${name} = ${name};`; - } - - doDefaultExport(identifier) { - if (identifier === null) { - throw new Error("The globals compiler does not support anonymous default exports."); - } - return `__exports__.${identifier} = `; - } - - doImportSpecifiers(import_, idx) { - var dependencyName = import_.source.value; - var replacement = ""; - for (var specifier of import_.specifiers) { - var alias = specifier.name ? specifier.name.name : specifier.id.name; - replacement += this.doNamedImport(specifier.id.name, dependencyName, alias); - } - return replacement; - } - -} - -export default GlobalsCompiler; diff --git a/lib/imports.js b/lib/imports.js new file mode 100644 index 0000000..cab799c --- /dev/null +++ b/lib/imports.js @@ -0,0 +1,213 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; + +var ModuleBindingList = require('./module_binding_list'); +var ModuleBindingDeclaration = require('./module_binding_declaration'); +var ModuleBindingSpecifier = require('./module_binding_specifier'); + +var utils = require('./utils'); +var memo = utils.memo; +var extend = utils.extend; +var sourcePosition = utils.sourcePosition; + +/** + * Represents a list of the imports for the given module. + * + * @constructor + * @param {Module} mod + */ +function ImportDeclarationList(mod) { + ModuleBindingList.call(this, mod); +} +extend(ImportDeclarationList, ModuleBindingList); + +/** + * @private + * @param {ast-types.Node} node + * @return {boolean} + */ +ImportDeclarationList.prototype.isMatchingBinding = function(node) { + return n.ImportDeclaration.check(node); +}; + +/** + * Gets an import declaration for the given `node`. + * + * @private + * @param {ast-types.ImportDeclaration} node + * @return {Import} + */ +ImportDeclarationList.prototype.declarationForNode = function(node) { + switch (node.kind) { + case 'default': + return new DefaultImportDeclaration(this.module, node); + + case 'named': + return new NamedImportDeclaration(this.module, node); + + case undefined: + return new BareImportDeclaration(this.module, node); + + default: + assert.ok(false, 'unexpected import kind at ' + sourcePosition(this.module, node) + ': ' + node.kind); + break; + } +}; + +/** + * Contains information about an import declaration. + * + * @constructor + * @abstract + * @param {Module} mod + * @param {ast-types.ImportDeclaration} node + */ +function ImportDeclaration(mod, node) { + assert.ok( + n.ImportDeclaration.check(node), + 'expected an import declaration, got ' + (node && node.type) + ); + + Object.defineProperties(this, { + node: { + value: node, + enumerable: false + }, + + module: { + value: mod, + enumerable: false + } + }); +} +extend(ImportDeclaration, ModuleBindingDeclaration); + +/** + * Represents a default import of the form + * + * import List from 'list'; + * + * @constructor + * @extends ImportDeclaration + * @param {Module} mod + * @param {ast-types.ImportDeclaration} node + */ +function DefaultImportDeclaration(mod, node) { + assert.equal(node.kind, 'default'); + assert.ok( + node.specifiers.length === 1 && node.specifiers[0], + 'expected exactly one specifier for a default import, got ' + + node.specifiers.length + ); + + ImportDeclaration.call(this, mod, node); +} +extend(DefaultImportDeclaration, ImportDeclaration); + +/** + * Gets a reference to the exported value from this import's module that + * corresponds to the local binding created by this import with the name given + * by `identfier`. + * + * @param {ast-types.Identifier|string} identifier + * @return {ast-types.Expression} + */ +DefaultImportDeclaration.prototype.getExportReference = function(identifier) { + var name = n.Identifier.check(identifier) ? identifier.name : identifier; + assert.equal( + name, + this.node.specifiers[0].id.name, + 'no export specifier found for `' + name + '`' + ); + return this.module.getExportReference('default'); +}; + +/** + * Contains a list of specifier name information for this import. + * + * @type {Array.} + * @property specifiers + */ +memo(DefaultImportDeclaration.prototype, 'specifiers', function() { + var specifier = new ImportSpecifier(this, this.node.specifiers[0]); + specifier.from = 'default'; + assert.equal(specifier.from, 'default'); + return [specifier]; +}); + +/** + * Represents a named import of the form + * + * import { sin, cos } from 'math'; + * + * @constructor + * @extends ImportDeclaration + * @param {Module} mod + * @param {ast-types.ImportDeclaration} node + */ +function NamedImportDeclaration(mod, node) { + assert.equal(node.kind, 'named'); + ImportDeclaration.call(this, mod, node); +} +extend(NamedImportDeclaration, ImportDeclaration); + +/** + * Contains a list of specifier name information for this import. + * + * @type {Array.} + * @property specifiers + */ +memo(NamedImportDeclaration.prototype, 'specifiers', function() { + var self = this; + return this.node.specifiers.map(function(specifier) { + return new ImportSpecifier(self, specifier); + }); +}); + +/** + * Represents an import with no bindings created in the local scope. These + * imports are of the form `import 'path/to/module'` and are generally included + * only for their side effects. + * + * @constructor + * @extends ImportDeclaration + * @param {Module} mod + * @param {ast-types.ImportDeclaration} node + */ +function BareImportDeclaration(mod, node) { + assert.ok( + node.kind === undefined && node.specifiers.length === 0, + 'expected a bare import at ' + sourcePosition(mod, node) + + ', got one with kind=' + node.kind + ' and ' + + node.specifiers.length + ' specifier(s)' + ); + ImportDeclaration.call(this, mod, node); +} +extend(BareImportDeclaration, ImportDeclaration); + +/** + * Returns an empty set of specifiers. + * + * @type {Array.} + * @property specifiers + */ +memo(BareImportDeclaration.prototype, 'specifiers', function() { + return []; +}); + +/** + */ +function ImportSpecifier(declaration, node) { + assert.ok( + declaration instanceof ImportDeclaration, + 'expected an instance of ImportDeclaration' + ); + ModuleBindingSpecifier.call(this, declaration, node); +} +extend(ImportSpecifier, ModuleBindingSpecifier); + +module.exports = ImportDeclarationList; diff --git a/lib/index.js b/lib/index.js index 6cba436..0465ab1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,11 @@ -import Compiler from './compiler'; +/* jshint node:true, undef:true, unused:true */ -import AbstractCompiler from './abstract_compiler'; -import AmdCompiler from './amd_compiler'; -import YuiCompiler from './yui_compiler'; -import CjsCompiler from './cjs_compiler'; -import GlobalsCompiler from './globals_compiler'; -import SourceModifier from './source_modifier'; +var Container = require('./container'); +var FileResolver = require('./file_resolver'); -export { Compiler }; +exports.FileResolver = FileResolver; +exports.Container = Container; -// Building blocks/subclassing APIs -export { AbstractCompiler, AmdCompiler, YuiCompiler, CjsCompiler, GlobalsCompiler, SourceModifier }; +exports.makeContainer = function(options) { + return new Container(options); +}; diff --git a/lib/module.js b/lib/module.js new file mode 100644 index 0000000..aa0e877 --- /dev/null +++ b/lib/module.js @@ -0,0 +1,193 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); +var fs = require('fs'); + +var esprima = require('esprima'); +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; +var b = types.builders; +var NodePath = recast.types.NodePath; + +var ImportDeclarationList = require('./imports'); +var ExportDeclarationList = require('./exports'); +var utils = require('./utils'); +var memo = utils.memo; +var endsWith = utils.endsWith; + +function Module(path, relativePath, container) { + Object.defineProperties(this, { + path: { + value: path, + enumerable: true, + writable: false + }, + + relativePath: { + value: relativePath, + enumerable: true, + writable: false + }, + + container: { + value: container, + enumerable: true, + writable: false + } + }); +} + +/** + * Clears the cached data for this module. + */ +Module.prototype.reload = function() { + delete this.ast; + delete this.imports; + delete this.exports; + delete this.scope; +}; + +/** + * The list of imports declared by this module. + * + * @type {ImportDeclarationList} + * @property imports + */ +memo(Module.prototype, 'imports', function() { + var result = new ImportDeclarationList(this); + result.readProgram(this.ast.program); + return result; +}); + +/** + * The list of exports declared by this module. + * + * @type {ExportDeclarationList} + * @property exports + */ +memo(Module.prototype, 'exports', function() { + var result = new ExportDeclarationList(this); + result.readProgram(this.ast.program); + return result; +}); + +/** + * This module's scope. + * + * @type {ast-types.Scope} + * @property scope + */ +memo(Module.prototype, 'scope', function() { + return new NodePath(this.ast).get('program').get('body').scope; +}); + +/** + * This module's source code represented as an abstract syntax tree. + * + * @type {ast-types.File} + * @property ast + */ +memo(Module.prototype, 'ast', function() { + return recast.parse( + fs.readFileSync(this.path).toString(), + { esprima: esprima } + ); +}); + +/** + * A reference to the options from this module's container. + * + * @type {object} + * @property options + */ +memo(Module.prototype, 'options', function() { + return this.container.options; +}); + +/** + * This module's relative name, like {#relativePath} but without the extension. + * + * @type {string} + * @property name + */ +memo(Module.prototype, 'name', function() { + var relativePath = this.relativePath; + if (endsWith(relativePath, '.js')) { + return relativePath.slice(0, -3); + } else { + return relativePath; + } +}); + +/** + * A string suitable for a JavaScript identifier named for this module. + * + * @type {string} + * @property id + */ +memo(Module.prototype, 'id', function() { + return this.name.replace(/[^\w$_]/g, '$') + '$$'; +}); + +/** + * Gets a Module by path relative to this module. + * + * @param {string} sourcePath + * @return {Module} + */ +Module.prototype.getModule = function(sourcePath) { + return this.container.getModule(sourcePath, this); +}; + +/** + */ +Module.prototype.getExportReference = function(identifier) { + var name; + if (n.Identifier.check(identifier)) { + name = identifier.name; + } else { + name = identifier; + identifier = null; + } + assert.equal(typeof name, 'string'); + + // TODO: Use constant for 'compression'. + if (this.options.optimize === 'compression') { + return b.identifier(this.id + name); + } else { + return b.memberExpression( + b.identifier(this.id), + identifier || b.identifier(name), + false + ); + } +}; + +/** + * Gets a reference to the original exported value corresponding to the local + * binding created by this import with the name given by `identfier`. + * + * @param {ast-types.NodePath} referencePath + * @return {ast-types.Expression} + */ +Module.prototype.getBindingReference = function(referencePath) { + var imp = this.imports.findImportForReference(referencePath); + return imp; +}; + +/** + * Generate a descriptive string suitable for debugging. + * + * @return {string} + */ +Module.prototype.inspect = function() { + return '#<' + this.constructor.name + ' ' + this.relativePath + '>'; +}; + +/** + * @alias {#inspect} + */ +Module.prototype.toString = Module.prototype.inspect; + +module.exports = Module; diff --git a/lib/module_binding_declaration.js b/lib/module_binding_declaration.js new file mode 100644 index 0000000..1411362 --- /dev/null +++ b/lib/module_binding_declaration.js @@ -0,0 +1,116 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; + +var utils = require('./utils'); +var memo = utils.memo; + +/** + * Contains information about a module binding declaration. This corresponds to + * the shared functionality of `ExportDeclaration` and `ImportDeclaration` in + * the ES6 spec. + * + * @constructor + * @abstract + * @param {Module} mod + * @param {ast-types.ImportDeclaration|ast-types.ExportDeclaration} node + */ +function ModuleBindingDeclaration(mod, node) { + assert.ok( + n.ImportDeclaration.check(node) || n.ExportDeclaration.check(node), + 'expected an import or export declaration, got ' + (node && node.type) + ); + + Object.defineProperties(this, { + node: { + value: node, + enumerable: false + }, + + module: { + value: mod, + enumerable: false + } + }); +} + +/** + * Finds the specifier that creates the local binding given by `name`, if one + * exists. Otherwise `null` is returned. + * + * @private + * @param {string} name + * @return {?ModuleBindingSpecifier} + */ +ModuleBindingDeclaration.prototype.findSpecifierByName = function(name) { + var specifiers = this.specifiers; + + for (var i = 0, length = specifiers.length; i < length; i++) { + var specifier = specifiers[i]; + if (specifier.name === name) { + return specifier; + } + } + + return null; +}; + +/** + * @private + * @param {ast-types.Identifier} identifier + * @return {?ModuleBindingSpecifier} + */ +ModuleBindingDeclaration.prototype.findSpecifierByIdentifier = function(identifier) { + for (var i = 0, length = this.specifiers.length; i < length; i++) { + var specifier = this.specifiers[i]; + if (specifier.identifier === identifier) { + return specifier; + } + } + + return null; +}; + +memo(ModuleBindingDeclaration.prototype, 'sourcePath', function() { + return this.node.source && this.node.source.value; +}); + +/** + * Gets a reference to the module referenced by this declaration. + * + * @type {Module} + * @property source + */ +memo(ModuleBindingDeclaration.prototype, 'source', function() { + return this.sourcePath ? this.module.getModule(this.sourcePath) : null; +}); + +/** + * Gets the module scope. + * + * @type {ast-types.Scope} + * @property scope + */ +memo(ModuleBindingDeclaration.prototype, 'moduleScope', function() { + return this.module.scope; +}); + +/** + * Generate a string representing this object to aid debugging. + * + * @return {string} + */ +ModuleBindingDeclaration.prototype.inspect = function() { + return recast.print(this.node).code; +}; + +/** + * @alias {#inspect} + */ +ModuleBindingDeclaration.prototype.toString = ModuleBindingDeclaration.prototype.inspect; + +module.exports = ModuleBindingDeclaration; diff --git a/lib/module_binding_list.js b/lib/module_binding_list.js new file mode 100644 index 0000000..ada2b25 --- /dev/null +++ b/lib/module_binding_list.js @@ -0,0 +1,233 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); + +var utils = require('./utils'); +var memo = utils.memo; +var sourcePosition = utils.sourcePosition; + +/** + * Represents a list of bindings for the given module. This corresponds to the + * shared functionality from `ExportsList` and `ImportsList` from the ES6 spec. + * + * @abstract + * @constructor + * @param {Module} mod + */ +function ModuleBindingList(mod) { + Object.defineProperties(this, { + _nodes: { + value: [], + enumerable: false + }, + + module: { + value: mod, + enumerable: false + } + }); +} + +/** + * Add all the binding declarations from the given scope body. Generally this + * should be the Program node's `body` property, an array of statements. + * + * @param {ast-types.Program} program + */ +ModuleBindingList.prototype.readProgram = function(program) { + var body = program.body; + for (var i = 0; i < body.length; i++) { + if (this.isMatchingBinding(body[i])) { + this.addDeclaration(body[i]); + } + } +}; + +/** + * Adds a declaration to the list. + * + * @private + * @param {ast-types.ImportDeclaration|ast-types.ExportDeclaration} node + */ +ModuleBindingList.prototype.addDeclaration = function(node) { + assert.ok( + this.isMatchingBinding(node), + 'expected node to be an declaration, but got ' + + (node && node.type) + ); + this._nodes.push(node); + + // reset the cache + delete this.declarations; + delete this.specifiers; + delete this.modules; +}; + +/** + * Gets the module scope. + * + * @type {ast-types.Scope} + * @property scope + */ +memo(ModuleBindingList.prototype, 'moduleScope', function() { + return this.module.scope; +}); + +/** + * Gets all the modules referenced by the declarations in this list. + * + * @type {Array.} + * @property modules + */ +memo(ModuleBindingList.prototype, 'modules', function() { + var modules = []; + + this.declarations.forEach(function(declaration) { + if (declaration.source && modules.indexOf(declaration.source) < 0) { + modules.push(declaration.source); + } + }); + + return modules; +}); + +/** + * Finds the specifier that creates the local binding given by `name`, if one + * exists. Otherwise `null` is returned. + * + * @private + * @param {string} name + * @return {?ModuleBindingSpecifier} + */ +ModuleBindingList.prototype.findSpecifierByName = function(name) { + for (var i = 0, length = this.declarations.length; i < length; i++) { + var specifier = this.declarations[i].findSpecifierByName(name); + if (specifier) { return specifier; } + } + + return null; +}; + +/** + * @private + * @param {ast-types.Identifier} identifier + * @return {?ModuleBindingSpecifier} + */ +ModuleBindingList.prototype.findSpecifierByIdentifier = function(identifier) { + for (var i = 0, length = this.declarations.length; i < length; i++) { + var specifier = this.declarations[i].findSpecifierByIdentifier(identifier); + if (specifier && specifier.identifier === identifier) { + return specifier; + } + } + + return null; +}; + +/** + * @param {ast-types.NodePath} referencePath + * @return {?ModuleBindingSpecifier} + */ +ModuleBindingList.prototype.findSpecifierForReference = function(referencePath) { + var declaration = this.findDeclarationForReference(referencePath); + + if (!declaration) { + return null; + } + + var specifier = this.findSpecifierByIdentifier(declaration.node); + assert.ok( + specifier, + 'no specifier found for `' + referencePath.node.name + '`! this should not happen!' + ); + return specifier; +}; + +/** + * @private + */ +ModuleBindingList.prototype.findDeclarationForReference = function(referencePath) { + // Check names to avoid traversing scopes for all references. + if (this.names.indexOf(referencePath.node.name) < 0) { + return null; + } + + var node = referencePath.node; + var declaringScope = referencePath.scope.lookup(node.name); + assert.ok( + declaringScope, + '`' + node.name + '` at ' + sourcePosition(this.module, node) + + ' cannot be bound if it is not declared' + ); + + // Bindings are at the top level, so if this isn't then it's shadowing. + if (!declaringScope.isGlobal) { + return null; + } + + var declarations = declaringScope.getBindings()[node.name]; + assert.ok( + declarations && declarations.length === 1, + 'expected one declaration for `' + node.name + + '`, at ' + sourcePosition(this.module, node) + + ' but found ' + (declarations ? declarations.length : 'none') + ); + + return declarations[0]; +}; + +/** + * Generate a string representing this object to aid debugging. + * + * @return {string} + */ +ModuleBindingList.prototype.inspect = function() { + var result = '#<' + this.constructor.name; + + result += ' module=' + this.module.relativePath; + + if (this.declarations.length > 0) { + result += ' declarations=' + this.declarations.map(function(imp) { + return imp.inspect(); + }).join(', '); + } + + result += '>'; + return result; +}; + +/** + * @alias {#inspect} + */ +ModuleBindingList.prototype.toString = ModuleBindingList.prototype.inspect; + +/** + * Contains a list of declarations. + * + * @type {Array.<(Import|Export)>} + * @property declarations + */ +memo(ModuleBindingList.prototype, 'declarations', function() { + var self = this; + + return this._nodes.map(function(child) { + return self.declarationForNode(child); + }); +}); + +/** + * Contains a combined list of names for all the declarations contained in this + * list. + * + * @type {Array.} + * @property names + */ +memo(ModuleBindingList.prototype, 'names', function() { + return this.declarations.reduce(function(names, decl) { + return names.concat(decl.specifiers.map(function(specifier) { + return specifier.name; + })); + }, []); +}); + +module.exports = ModuleBindingList; diff --git a/lib/module_binding_specifier.js b/lib/module_binding_specifier.js new file mode 100644 index 0000000..56f9134 --- /dev/null +++ b/lib/module_binding_specifier.js @@ -0,0 +1,120 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; + +var utils = require('./utils'); +var memo = utils.memo; +var sourcePosition = utils.sourcePosition; + +/** + */ +function ModuleBindingSpecifier(declaration, node) { + Object.defineProperties(this, { + declaration: { + value: declaration, + enumerable: false + }, + + node: { + value: node, + enumerable: false + } + }); +} + +/** + */ +memo(ModuleBindingSpecifier.prototype, 'module', function() { + return this.declaration.module; +}); + +/** + */ +memo(ModuleBindingSpecifier.prototype, 'moduleScope', function() { + return this.declaration.moduleScope; +}); + +/** + */ +memo(ModuleBindingSpecifier.prototype, 'name', function() { + return this.identifier.name; +}); + +/** + */ +memo(ModuleBindingSpecifier.prototype, 'from', function() { + return this.node.id.name; +}); + +/** + */ +memo(ModuleBindingSpecifier.prototype, 'identifier', function() { + return this.node.name || this.node.id; +}); + +/** + */ +memo(ModuleBindingSpecifier.prototype, 'exportSpecifier', function() { + var source = this.declaration.source; + if (source) { + var exports = source.exports; + return exports.findSpecifierByName(this.from); + } else { + return null; + } +}); + +memo(ModuleBindingSpecifier.prototype, 'importSpecifier', function() { + // This may be an export from this module, so find the declaration. + var localExportDeclarationInfo = this.moduleDeclaration; + + if (localExportDeclarationInfo && n.ImportDeclaration.check(localExportDeclarationInfo.declaration)) { + // It was imported then exported with two separate declarations. + var exportModule = this.module; + return exportModule.imports.findSpecifierByIdentifier(localExportDeclarationInfo.identifier); + } else { + return null; + } +}); + +memo(ModuleBindingSpecifier.prototype, 'terminalExportSpecifier', function() { + if (this.exportSpecifier) { + // This is true for both imports and exports with a source, e.g. + // `import { foo } from 'foo'` or `export { foo } from 'foo'`. + return this.exportSpecifier.terminalExportSpecifier; + } + + // This is an export from this module, so find the declaration. + var importSpecifier = this.importSpecifier; + if (importSpecifier) { + var nextExportSpecifier = importSpecifier.exportSpecifier; + assert.ok( + nextExportSpecifier, + 'expected matching export in ' + importSpecifier.declaration.source.relativePath + + ' for import of `' + importSpecifier.name + '` at ' + + sourcePosition(this.module, this.moduleDeclaration.identifier) + ); + return nextExportSpecifier.terminalExportSpecifier; + } else { + // It was declared in this module, so we are the terminal export specifier. + return this; + } +}); + +/** + */ +ModuleBindingSpecifier.prototype.inspect = function() { + return '#<' + this.constructor.name + + ' module=' + this.declaration.module.relativePath + + ' name=' + this.name + + ' from=' + this.from + + '>'; +}; + +ModuleBindingSpecifier.prototype.toString = ModuleBindingSpecifier.prototype.inspect; + +module.exports = ModuleBindingSpecifier; diff --git a/lib/parser.js b/lib/parser.js deleted file mode 100644 index e0d7a19..0000000 --- a/lib/parser.js +++ /dev/null @@ -1,96 +0,0 @@ -import { parse as esparse } from 'esprima'; - -const LITERAL = 'Literal'; - -class Parser { - constructor(script) { - this.parse(script); - } - - parse(script) { - this.imports = []; - this.exports = []; - this.directives = []; - this.exportDefault = undefined; - this.walk(esparse(script, {range: true, comment: true})); - } - - walk(node) { - if (node.type) { - var processor = this['process'+node.type]; - if (processor) { - var result = processor.call(this, node); - if (result === false) { - return; - } - } - } - - if (node.body && node.body.length > 0) { - node.body.forEach(function(child) { - child.parent = node; - this.walk(child); - }.bind(this)); - } - - // directives have to be top-level - if (node.comments && node.type === "Program") { - for (var comment of node.comments) { - if (comment.value.indexOf("transpile:") !== -1) { - this.directives.push(comment); - } - } - } - } - - processImportDeclaration(node) { - var {kind, source} = node; - - if (source.type !== LITERAL || typeof source.value !== 'string') { - throw new Error('invalid module source: '+source.value); - } - - switch (kind) { - case 'named': - this.processNamedImportDeclaration(node); - break; - - case "default": - this.processDefaultImportDeclaration(node); - break; - - // bare import (i.e. `import "foo";`) - case undefined: - this.processNamedImportDeclaration(node); - break; - - default: - throw new Error('unknown import kind: '+kind); - } - } - - processNamedImportDeclaration(node) { - this.imports.push(node); - } - - processDefaultImportDeclaration(node) { - if (node.specifiers.length !== 1) { - throw new Error('expected one specifier for default import, got '+node.specifiers.length); - } - - this.imports.push(node); - } - - processExportDeclaration(node) { - if (!node.declaration && !node.specifiers) { - throw new Error('expected declaration or specifiers after `export` keyword'); - } - this.exports.push(node); - } - - processModuleDeclaration(node) { - this.imports.push(node); - } -} - -export default Parser; diff --git a/lib/replacement.js b/lib/replacement.js new file mode 100644 index 0000000..8f2aa4e --- /dev/null +++ b/lib/replacement.js @@ -0,0 +1,58 @@ +/* jshint node:true, undef:true, unused:true */ + +/** + * Represents a replacement of a node path with zero or more nodes. + * + * @constructor + * @param {ast-types.NodePath} nodePath + * @param {Array.} nodes + */ +function Replacement(nodePath, nodes) { + this.nodePath = nodePath; + this.nodes = nodes; +} + +/** + * Performs the replacement. + */ +Replacement.prototype.replace = function() { + this.nodePath.replace.apply(this.nodePath, this.nodes); +}; + +/** + * Constructs a Replacement that, when run, will remove the node from the AST. + * + * @param {ast-types.NodePath} nodePath + * @returns {Replacement} + */ +Replacement.removes = function(nodePath) { + return new Replacement(nodePath, []); +}; + +/** + * Constructs a Replacement that, when run, will insert the given nodes after + * the one in nodePath. + * + * @param {ast-types.NodePath} nodePath + * @param {Array.} nodes + * @returns {Replacement} + */ +Replacement.adds = function(nodePath, nodes) { + return new Replacement(nodePath, [nodePath.node].concat(nodes)); +}; + +/** + * Constructs a Replacement that, when run, swaps the node in nodePath with the + * given node or nodes. + * + * @param {ast-types.NodePath} nodePath + * @param {ast-types.Node|Array.} nodes + */ +Replacement.swaps = function(nodePath, nodes) { + if ({}.toString.call(nodes) !== '[object Array]') { + nodes = [nodes]; + } + return new Replacement(nodePath, nodes); +}; + +module.exports = Replacement; diff --git a/lib/rewriter.js b/lib/rewriter.js new file mode 100644 index 0000000..8fc5717 --- /dev/null +++ b/lib/rewriter.js @@ -0,0 +1,171 @@ +/* jshint node:true, undef:true, unused:true */ + +var recast = require('recast'); +var types = recast.types; +var n = types.namedTypes; +var b = types.builders; +var astUtil = require('ast-util'); + +var Replacement = require('./replacement'); + +/** + * Replaces references to local bindings created by `mod`'s imports + * with references to the original value in the source module. + * + * @constructor + * @param {Formatter} formatter + */ +function Rewriter(formatter) { + Object.defineProperties(this, { + formatter: { + value: formatter, + enumerable: false + } + }); +} + +/** + * Rewrites references to all imported and exported bindings according to the + * rules from this rewriter's formatter. For example, this module: + * + * import { sin, cos } from './math'; + * import fib from './math/fib'; + * + * assert.equal(sin(0), 0); + * assert.equal(cos(0), 1); + * assert.equal(fib(1), 1); + * + * has its references to the imported bindings `sin`, `cos`, and `fib` + * rewritten to reference the source module: + * + * assert.equal(math$$.sin(0), 0); + * assert.equal(math$$.cos(0), 1); + * assert.equal(math$fib$$.fib(1), 1); + * + * @param {Array.} modules + */ +Rewriter.prototype.rewrite = function(modules) { + var replacements = []; + + // FIXME: This is just here to ensure that all exports know where they came + // from. We need this because after we re-write the declarations will not be + // there anymore and we'll need to ensure they're cached up front. + modules.forEach(function(mod) { + mod.exports.declarations.forEach(function(declaration) { + declaration.specifiers.forEach(function(specifier) { + return specifier.importSpecifier; + }); + }); + }); + + var self = this; + + for (var i = 0, length = modules.length; i < length; i++) { + var mod = modules[i] + types.traverse(mod.ast.program, function() { + var replacement = self.processNodePath(mod, this); + if (replacement) { + replacements.push(replacement); + } + }); + } + + replacements.forEach(function(replacement) { + if (replacement.replace) { + replacement.replace(); + } else { + var path = replacement.shift(); + path.replace.apply(path, replacement); + } + }); +}; + +/** + * Determines what, if anything, to replace the given nodePath's node with by + * delegating to this rewriter's formatter. + * + * @private + * @param {Module} mod + * @param {ast-types.NodePath} nodePath + * @return {Array.} + */ +Rewriter.prototype.processNodePath = function(mod, nodePath) { + var node = nodePath.node; + var formatter = this.formatter; + var exportReference; + + if (astUtil.isReference(nodePath)) { + exportReference = this.getExportReferenceForReference(mod, nodePath); + if (exportReference) { + return Replacement.swaps(nodePath, exportReference); + } + } else if (n.VariableDeclaration.check(node) && nodePath.scope.isGlobal) { + return formatter.processVariableDeclaration(mod, nodePath); + } else if (n.FunctionDeclaration.check(node) && n.Program.check(nodePath.parent.node)) { + return formatter.processFunctionDeclaration(mod, nodePath); + } else if (n.ExportDeclaration.check(node)) { + if (node.default) { + /** + * Default exports do not create bindings, so we can safely turn these + * into expressions that do something with the exported value. + * + * Make sure that the exported value is replaced if it is a reference + * to an imported binding. For example: + * + * import { foo } from './foo'; + * export default foo; + * + * Might become: + * + * mod$$.default = foo$$.foo; + */ + var declaration = node.declaration; + var declarationPath = nodePath.get('declaration'); + if (astUtil.isReference(declarationPath)) { + exportReference = this.getExportReferenceForReference(mod, declarationPath); + if (exportReference) { + declaration = exportReference; + } + } + return Replacement.swaps(nodePath, formatter.defaultExport(mod, declaration)); + } else { + return formatter.processExportDeclaration(mod, nodePath); + } + } else if (n.ImportDeclaration.check(node)) { + return formatter.processImportDeclaration(mod, nodePath); + } else { + return null; + } +}; + +/** + * @private + */ +Rewriter.prototype.getExportReferenceForReference = function(mod, referencePath) { + if (n.ExportSpecifier.check(referencePath.parent.node) && !referencePath.parent.node.default) { + // Do not rewrite non-default export specifiers. + return null; + } + + /** + * We need to replace references to variables that are imported or + * exported with the correct export expression. The export expression + * should be named for the original export for a variable. + * + * That is, imports must be followed to their export. If that exported + * value came from an import then repeat the process until you find a + * declaration of the exported value. + */ + var exportSpecifier = mod.exports.findSpecifierByName(referencePath.node.name); + if (exportSpecifier && exportSpecifier.declaration.source && exportSpecifier.node !== referencePath.parent.node) { + // This is a direct export from another module, e.g. `export { foo } from + // 'foo'`. There are no local bindings created by this, so there is no + // associated export for this reference and no need to rewrite it. + return null; + } + + return this.formatter.exportedReference(mod, referencePath) || + this.formatter.importedReference(mod, referencePath); +}; + +module.exports = Rewriter; diff --git a/lib/sorting.js b/lib/sorting.js new file mode 100644 index 0000000..2a5d162 --- /dev/null +++ b/lib/sorting.js @@ -0,0 +1,45 @@ +/** + * Determines the execution order of the given modules. This function resolves + * cycles by preserving the order in which the modules are visited. + * + * @param {Array.} modules + * @return {Array.} + */ +function sort(modules) { + var result = []; + var state = {}; + + modules.forEach(function(mod) { + visit(mod, result, state); + }); + + return result; +} +exports.sort = sort; + +/** + * Visits the given module, adding it to `result` after visiting all of the + * modules it imports, recursively. The `state` argument is private and maps + * module ids to the current visit state. + * + * @private + * @param {Module} mod + * @param {Array.} result + * @param {Object.} state + */ +function visit(mod, result, state) { + if (state[mod.id] === 'added') { + // already in the list, ignore it + return; + } + if (state[mod.id] === 'seen') { + // cycle found, just ignore it + return; + } + state[mod.id] = 'seen'; + mod.imports.modules.forEach(function(mod) { + visit(mod, result, state); + }); + state[mod.id] = 'added'; + result.push(mod); +} diff --git a/lib/source_modifier.js b/lib/source_modifier.js deleted file mode 100644 index 96a7d89..0000000 --- a/lib/source_modifier.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Borrowed this wonderful utility class from the ES6 Loader Polyfill - */ - -class SourceModifier { - constructor(source) { - this.source = source; - this.rangeOps = []; - } - - mapIndex(index) { - // apply the range operations in order to the index - for (var i = 0; i < this.rangeOps.length; i++) { - var curOp = this.rangeOps[i]; - if (curOp.start >= index) - continue; - if (curOp.end <= index) { - index += curOp.diff; - continue; - } - throw 'Source location ' + index + ' has already been transformed!'; - } - return index; - } - - replace(start, end, replacement) { - var diff = replacement.length - (end - start + 1); - - start = this.mapIndex(start); - end = this.mapIndex(end); - - this.source = this.source.substr(0, start) + replacement + this.source.substr(end + 1); - - this.rangeOps.push({ - start: start, - end: end, - diff: diff - }); - } - - getRange(start, end) { - return this.source.substr(this.mapIndex(start), this.mapIndex(end)); - } - - toString() { - return this.source; - } -} - -export default SourceModifier; diff --git a/lib/utils.js b/lib/utils.js index ddf036b..e8d2388 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,95 +1,73 @@ -var hasOwnProp = {}.hasOwnProperty; - -function isEmpty(object) { - for (var foo in object) { - if (Object.prototype.hasOwnProperty.call(object, foo)) { - return false; +/* jshint node:true, undef:true, unused:true */ + +var recast = require('recast'); +var esprima = require('esprima'); + +var proto = '__proto__'; + +function memo(object, property, getter) { + Object.defineProperty(object, property, { + get: function() { + this[property] = getter.call(this); + return this[property]; + }, + + set: function(value) { + Object.defineProperty(this, property, { + value: value, + configurable: true, + writable: false, + enumerable: false + }); } - } - return true; -} - -function uniq(array) { - var result = []; - - for (var i = 0; i < array.length; i++) { - var item = array[i]; - if (result.indexOf(item) === -1) { - result.push(item); - } - } - - return result; + }); } +exports.memo = memo; -var array = { uniq: uniq }; - -function forEach(enumerable, callback) { - if (enumerable !== null && enumerable !== undefined && typeof enumerable.forEach === 'function') { - enumerable.forEach(callback); - return; - } - - for (var key in enumerable) { - if (hasOwnProp.call(enumerable, key)) { - callback(enumerable[key], key); - } - } +function startsWith(string, substring) { + return string.lastIndexOf(substring, 0) === 0; } +exports.startsWith = startsWith; -function isWhitespace(str) { - return !str || /^\s*$/.test(str); +function endsWith(string, substring) { + var expected = string.length - substring.length; + return string.indexOf(substring, expected) === expected; } +exports.endsWith = endsWith; -function indent(lines, level, indentString=' ') { - return lines.map(function(line) { - if (!isWhitespace(line)) { - for (var i = 0; i < level; i++) { - line = indentString + line; - } - } - return line; - }); +function extend(subclass, superclass) { + subclass[proto] = superclass; + subclass.prototype = Object.create(superclass.prototype); + subclass.prototype.constructor = subclass; } - -var WHITESPACE_ONLY = /^\s*$/; -var LEADING_WHITESPACE = /^\s*/; - -function unindent(string) { - var minIndent = null; - var lines = string.split('\n'); - - for (var line of lines) { - if (!WHITESPACE_ONLY.test(line)) { - var match = line.match(LEADING_WHITESPACE); - if (match) { - if (minIndent !== null) { - minIndent = Math.min(match[0].length, minIndent); - } else { - minIndent = match[0].length; - } - } - } +exports.extend = extend; + +function sourcePosition(mod, node) { + var loc = node && node.loc; + if (loc) { + return mod.relativePath + ':' + loc.start.line + ':' + (loc.start.column + 1); + } else { + return mod.relativePath; } - - return lines.map(line => line.slice(minIndent)).join('\n'); } +exports.sourcePosition = sourcePosition; -function ltrim(string) { - return string.replace(LEADING_WHITESPACE, ''); -} +function IFFE() { + if (!IFFE.AST) { + IFFE.AST = JSON.stringify(recast.parse('(function(){})()', { esprima: esprima })); + } -var string = { indent, unindent, ltrim }; + var result = JSON.parse(IFFE.AST); + var body = result.program.body[0].expression.callee.body.body; -class Unique { - constructor(prefix) { - this.prefix = prefix; - this.index = 1; - } + Array.prototype.slice.call(arguments).forEach(function(arg) { + if (Object.prototype.toString.call(arg) === '[object Array]') { + body.push.apply(body, arg); + } else { + body.push(arg); + } + }); - next() { - return ['__', this.prefix, this.index++, '__'].join(''); - } + return result.program.body[0].expression; } - -export { isEmpty, Unique, array, forEach, string }; +exports.IFFE = IFFE; diff --git a/lib/writer.js b/lib/writer.js new file mode 100644 index 0000000..6ba2479 --- /dev/null +++ b/lib/writer.js @@ -0,0 +1,64 @@ +/* jshint node:true, undef:true, unused:true */ + +var assert = require('assert'); +var recast = require('recast'); +var fs = require('fs'); +var Path = require('path'); +var mkdirp = require('mkdirp'); + +function Writer(target) { + this.target = target; +} + +Writer.prototype.write = function(files) { + var target = this.target; + + switch (files.length) { + case 0: + throw new Error('expected at least one file to write, got zero'); + + case 1: + // We got a single file, so `target` should refer to either a file or a + // directory, but only if the file has a name. + var isDirectory = false; + try { + isDirectory = fs.statSync(target).isDirectory(); + } catch (ex) {} + + assert.ok( + !isDirectory || files[0].filename, + 'unable to determine filename for output to directory: ' + target + ); + this.writeFile( + files[0], + isDirectory ? Path.resolve(target, files[0].filename) : target + ); + break; + + default: + // We got multiple files to output, so `target` should be a directory or + // not exist (so we can create it). + var self = this; + files.forEach(function(file) { + self.writeFile(file, Path.resolve(target, file.filename)); + }); + break; + } +}; + +Writer.prototype.writeFile = function(file, filename) { + var rendered = recast.print(file); + assert.ok(filename, 'missing filename for file: ' + rendered.code); + + mkdirp.sync(Path.dirname(filename)); + fs.writeFileSync(filename, rendered.code, { encoding: 'utf8' }); + if (rendered.map) { + fs.writeFileSync( + filename + '.map', + JSON.stringify(rendered.map), + { encoding: 'utf8' } + ); + } +}; + +module.exports = Writer; diff --git a/lib/yui_compiler.js b/lib/yui_compiler.js deleted file mode 100644 index 3d413b8..0000000 --- a/lib/yui_compiler.js +++ /dev/null @@ -1,81 +0,0 @@ -import AbstractCompiler from './abstract_compiler'; -import SourceModifier from './source_modifier'; - -class YUICompiler extends AbstractCompiler { - stringify() { - var string = this.string.toString(); // string is actually a node buffer - this.source = new SourceModifier(string); - - var out = this.buildPreamble(); - - // build* mutates this.source - this.buildImports(); - this.buildExports(); - - out += `${this.indentLines(" ")} - return __exports__; -}, "@VERSION@", ${this.buildMetas()});`; - - return out; - } - - buildPreamble() { - var name = this.moduleName || "@NAME@"; // support for anonymous modules - return `YUI.add("${name}", function(Y, NAME, __imports__, __exports__) { - "use strict"; -`; - } - - buildMetas() { - return JSON.stringify({ es: true, requires: this.dependencyNames }); - } - - doModuleImport(name, dependencyName, idx) { - return `var ${name} = __imports__["${dependencyName}"];\n`; - } - - doBareImport(name) { - return ""; - } - - doDefaultImport(name, dependencyName, idx) { - if (this.options.compatFix === true) { - return `var ${name} = __imports__["${dependencyName}"]["default"] || __imports__["${dependencyName}"];\n`; - } else { - return `var ${name} = __imports__["${dependencyName}"]["default"];\n`; - } - } - - doNamedImport(name, dependencyName, alias) { - var member = (name === 'default' ? '["default"]' : '.' + name); - return `var ${alias} = __imports__["${dependencyName}"]${member};\n`; - } - - doExportSpecifier(name, reexport) { - if (reexport) { - return `__exports__.${name} = __imports__["${reexport}"].${name};\n`; - } - return `__exports__.${name} = ${name};\n`; - } - - doExportDeclaration(name) { - return `\n__exports__.${name} = ${name};`; - } - - doDefaultExport() { - return `__exports__["default"] = `; - } - - doImportSpecifiers(import_, idx) { - var dependencyName = import_.source.value; - var replacement = ""; - for (var specifier of import_.specifiers) { - var alias = specifier.name ? specifier.name.name : specifier.id.name; - replacement += this.doNamedImport(specifier.id.name, dependencyName, alias); - } - return replacement; - } - -} - -export default YUICompiler; diff --git a/package.json b/package.json index dacbbc4..08c7c29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "es6-module-transpiler", "version": "0.4.0", - "description": "es6-module-transpiler is an experimental compiler that allows you to write your JavaScript using a subset of the current ES6 module syntax, and compile it into AMD, CommonJS, and globals styles.", + "description": "es6-module-transpiler is an experimental compiler that allows you to write your JavaScript using a subset of the current ES6 module syntax, and compile it into various formats.", "homepage": "http://square.github.com/es6-module-transpiler", "keywords": [ "es6", @@ -15,35 +15,31 @@ "compile-modules": "./bin/compile-modules" }, "directories": { - "lib": "./lib" + "lib": "./lib", + "test": "test" }, - "main": "./dist/es6-module-transpiler", + "main": "lib/index.js", "repository": { "type": "git", "url": "https://github.com/square/es6-module-transpiler.git" }, - "dependencies": { - "optimist": "~0.3.5", - "through": "~2.3.4" - }, "scripts": { - "test": "grunt build && grunt test", - "prepublish": "grunt build && grunt test" + "test": "node test/runner.js -f ${MODULE_FORMATTER:-module-variable}", + "test-module-variable": "node test/runner.js -f module-variable", + "test-export-variable": "node test/runner.js -f export-variable", + "test-commonjs": "node test/runner.js -f commonjs", + "test-all": "npm run test-module-variable && npm run test-export-variable && npm run test-commonjs" }, - "devDependencies": { - "es6ify": "~0.2.0", + "author": "Square, Inc.", + "license": "Apache 2", + "dependencies": { + "recast": "^0.5.17", "esprima": "git://github.com/thomasboyt/esprima#4be906f1abcbb", - "grunt": "~0.4.1", - "grunt-browserify": "~1.2.0", - "grunt-contrib-clean": "~0.5.0", - "grunt-contrib-concat": "~0.3.0", - "grunt-contrib-uglify": "~0.2.2", - "grunt-es6-module-transpiler": "~0.4.1", - "grunt-simple-mocha": "~0.4.0", - "matchdep": "~0.1.2", - "mocha": "~1.12.0", - "qunit-mocha-ui": "0.0.4", - "uglify-js": "~2.2.4", - "grunt-contrib-jshint": "~0.6.2" + "ast-util": "^0.0.6", + "mkdirp": "^0.5.0", + "posix-getopt": "^1.0.0" + }, + "devDependencies": { + "example-runner": "^0.1.0" } } diff --git a/tasks/es6ify.js b/tasks/es6ify.js deleted file mode 100644 index 95a90f2..0000000 --- a/tasks/es6ify.js +++ /dev/null @@ -1,21 +0,0 @@ -function transpile(grunt, file) { - var es6ify = require('es6ify'), - fs = require('fs'), - path = require('path'), - done = this.async(); - - grunt.file.mkdir(path.dirname(file.dest)); - var output = fs.createWriteStream(file.dest, 'utf8'); - - grunt.util.async.forEachSeries(file.src, function(src, next) { - var input = fs.createReadStream(src, 'utf8'); - output.once('finish', next); - input.pipe(es6ify(src)).pipe(output); - }, done); -} - -module.exports = function(grunt) { - grunt.registerMultiTask('es6ify', 'Transpiles scripts written using ES6 to ES5.', function() { - this.files.forEach(transpile.bind(this, grunt)); - }); -}; diff --git a/tasks/features.js b/tasks/features.js deleted file mode 100644 index c99fc9a..0000000 --- a/tasks/features.js +++ /dev/null @@ -1,121 +0,0 @@ -var es6Ext = '.es6.js', - typeMap = { amd: 'AMD', yui: 'YUI', cjs: 'CJS', globals: 'Globals' }, - path = require('path'); - -module.exports = function(grunt) { - function extractSourceOptions(source) { - var lines = source.split('\n'), - options = {}; - - source = grunt.util._.trim(lines.filter(function(line) { - var lineopts = optionsFromLine(line); - - if (lineopts) { - grunt.util._.extend(options, lineopts); - return false; - } - - return true; - }).join('\n')); - - return { source: source, options: options }; - } - - function optionsFromLine(line) { - var match = line.match(/^\/\*\s*transpile:\s*(.*?)\s*\*\/\s*$/); - if (!match) { return null; } - - var optsString = match[1]; - - if (optsString === 'INVALID') { - return { invalid: true }; - } - - var options = {}; - - optsString.split(/\s+/).forEach(function(pairString) { - var pair = pairString.split('='), - key = pair[0], - value = pair[1]; - - if (key === 'imports') { - var imports = {}; - value.split(',').forEach(function(importPairString) { - var importPair = importPairString.split(':'), - importPath = importPair[0], - importGlobal = importPair[1]; - imports[importPath] = importGlobal; - }); - options[key] = imports; - } else if (value === 'true') { - options[key] = true; - } else if (value === 'false') { - options[key] = false; - } else if (value === 'null') { - options[key] = null; - } else { - options[key] = value; - } - }); - - return options; - } - - grunt.registerMultiTask('features', 'Creates test files for ES6 examples.', function() { - var tmpl = grunt.file.read(this.data.template), - _ = grunt.util._, - testFiles = []; - - this.files.forEach(function(file) { - if (!_.endsWith(file.src, es6Ext)) { - grunt.log.warn("skipping non-ES6 example file: "+file.src); - return; - } - - var basename = path.basename(file.src, es6Ext), - mod = {}, - source = grunt.file.read(file.src), - lines = source.split('\n'), - options = null; - - mod.name = basename.replace(/[^a-z]/gi, ' '); - mod.basename = basename; - mod.tests = []; - - var extractedOptionsAndRemainingSource = extractSourceOptions(source); - options = extractedOptionsAndRemainingSource.options; - source = extractedOptionsAndRemainingSource.source; - - if (options.invalid) { - mod.tests.push({ - name : "does not parse", - source : source, - options : options, - invalid : true - }); - } - - ['amd', 'yui', 'cjs', 'globals'].forEach(function(type) { - var typedExt = '.'+type+'.js', - typeName = typeMap[type], - typedFile = file.src[0].replace(es6Ext, typedExt); - - if (!grunt.file.exists(typedFile)) { return; } - - var test = {}; - mod.tests.push(test); - - test.name = 'to' + typeName; - test.typeName = typeName; - test.source = source; - test.expected = _.trim(grunt.file.read(typedFile)); - test.options = options; - }); - - grunt.file.write( - file.dest.replace(es6Ext, '_test.js'), - grunt.template.process(tmpl, { data: { mod: mod } }) - ); - }); - }); -}; diff --git a/tasks/options/browserify.js b/tasks/options/browserify.js deleted file mode 100644 index dccd003..0000000 --- a/tasks/options/browserify.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - dist: { - src: 'tmp/transpiled/index.js', - dest: 'tmp/es6-module-transpiler.es5.js', - options: { - transform: [function(file) { - return require('es6ify')(file); - }], - standalone: 'ModuleTranspiler' - } - } -}; diff --git a/tasks/options/clean.js b/tasks/options/clean.js deleted file mode 100644 index 031fa6a..0000000 --- a/tasks/options/clean.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = ['dist', 'bin', 'tmp', 'test/.generated']; diff --git a/tasks/options/concat.js b/tasks/options/concat.js deleted file mode 100644 index 79a0818..0000000 --- a/tasks/options/concat.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - dist: { - src: [ - // FIXME: This one really ought to be require('es6ify').runtime. - // See https://github.com/thlorenz/es6ify/issues/3. - 'node_modules/es6ify/node_modules/node-traceur/src/runtime/runtime.js', - 'tmp/es6-module-transpiler.es5.js' - ], - dest: 'dist/es6-module-transpiler.js' - }, - - bin: { - options: { - banner: '#!/usr/bin/env node\n' - }, - files: { - 'bin/compile-modules': 'tmp/bin/compile-modules' - } - } -}; diff --git a/tasks/options/es6ify.js b/tasks/options/es6ify.js deleted file mode 100644 index 6872508..0000000 --- a/tasks/options/es6ify.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - cli: { - files: { - 'tmp/bin/compile-modules': 'tmp/transpiled/compile-modules.js' - } - } -}; diff --git a/tasks/options/features.js b/tasks/options/features.js deleted file mode 100644 index a119737..0000000 --- a/tasks/options/features.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - all: { - template: 'test/features/test_template.js.tmpl', - files: [{ - expand: true, - cwd: 'test/features/', - src: ['**/*.es6.js'], - dest: 'test/.generated' - }] - } -}; diff --git a/tasks/options/jshint.js b/tasks/options/jshint.js deleted file mode 100644 index 9b09727..0000000 --- a/tasks/options/jshint.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - all: { - src: [ - 'Gruntfile.js', - 'lib/**/*.js' - ] - }, - options: { - jshintrc: '.jshintrc', - force: true - } -}; diff --git a/tasks/options/simplemocha.js b/tasks/options/simplemocha.js deleted file mode 100644 index bf0e4a9..0000000 --- a/tasks/options/simplemocha.js +++ /dev/null @@ -1,13 +0,0 @@ -var Mocha = require('mocha'); -Mocha.interfaces['qunit-mocha-ui'] = require('qunit-mocha-ui'); - -module.exports = { - options: { - globals: ['should'], - timeout: 3000, - ignoreLeaks: false, - ui: 'qunit-mocha-ui' - }, - - all: { src: ['test/.generated/**/*.js'] } -}; diff --git a/tasks/options/transpile.js b/tasks/options/transpile.js deleted file mode 100644 index 6f0c9c1..0000000 --- a/tasks/options/transpile.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - app: { - type: 'cjs', - files: [{ - expand: true, - cwd: 'lib/', - src: ['**/*.js'], - dest: 'tmp/transpiled/' - }] - } -}; diff --git a/tasks/options/uglify.js b/tasks/options/uglify.js deleted file mode 100644 index 9aa38fb..0000000 --- a/tasks/options/uglify.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - dist: { - files: { - 'dist/es6-module-transpiler.min.js': ['dist/es6-module-transpiler.js'] - } - } -}; diff --git a/test/examples/bare-import/exporter.js b/test/examples/bare-import/exporter.js new file mode 100644 index 0000000..89cf73f --- /dev/null +++ b/test/examples/bare-import/exporter.js @@ -0,0 +1,3 @@ +/* jshint esnext:true */ + +global.sideEffectyValue = 99; diff --git a/test/examples/bare-import/importer.js b/test/examples/bare-import/importer.js new file mode 100644 index 0000000..d9c51a0 --- /dev/null +++ b/test/examples/bare-import/importer.js @@ -0,0 +1,5 @@ +/* jshint esnext:true */ + +import './exporter'; + +assert.equal(global.sideEffectyValue, 99); diff --git a/test/examples/bindings/exporter.js b/test/examples/bindings/exporter.js new file mode 100644 index 0000000..a635466 --- /dev/null +++ b/test/examples/bindings/exporter.js @@ -0,0 +1,7 @@ +/* jshint esnext:true */ + +export var count = 0; + +export function incr() { + count++; +} diff --git a/test/examples/bindings/importer.js b/test/examples/bindings/importer.js new file mode 100644 index 0000000..df69785 --- /dev/null +++ b/test/examples/bindings/importer.js @@ -0,0 +1,7 @@ +/* jshint esnext:true */ + +import { count, incr } from './exporter'; + +assert.equal(count, 0); +incr(); +assert.equal(count, 1); diff --git a/test/examples/cycles-defaults/a.js b/test/examples/cycles-defaults/a.js new file mode 100644 index 0000000..4e0289b --- /dev/null +++ b/test/examples/cycles-defaults/a.js @@ -0,0 +1,5 @@ +/* jshint esnext:true */ + +import b from './b'; + +export default { a: 1, get b() { return b.b; } }; diff --git a/test/examples/cycles-defaults/b.js b/test/examples/cycles-defaults/b.js new file mode 100644 index 0000000..3ce87b6 --- /dev/null +++ b/test/examples/cycles-defaults/b.js @@ -0,0 +1,5 @@ +/* jshint esnext:true */ + +import a from './a'; + +export default { b: 2, get a() { return a.a; } }; diff --git a/test/examples/cycles-defaults/importer.js b/test/examples/cycles-defaults/importer.js new file mode 100644 index 0000000..7b5708c --- /dev/null +++ b/test/examples/cycles-defaults/importer.js @@ -0,0 +1,9 @@ +/* jshint esnext:true */ + +import a from './a'; +import b from './b'; + +assert.equal(a.a, 1); +assert.equal(a.b, 2); +assert.equal(b.a, 1); +assert.equal(b.b, 2); diff --git a/test/examples/cycles/a.js b/test/examples/cycles/a.js new file mode 100644 index 0000000..511370c --- /dev/null +++ b/test/examples/cycles/a.js @@ -0,0 +1,9 @@ +/* jshint esnext:true */ + +import { b } from './b'; + +export function getb() { + return b; +} + +export var a = 1; diff --git a/test/examples/cycles/b.js b/test/examples/cycles/b.js new file mode 100644 index 0000000..5a66f9b --- /dev/null +++ b/test/examples/cycles/b.js @@ -0,0 +1,9 @@ +/* jshint esnext:true */ + +import { a } from './a'; + +export function geta() { + return a; +} + +export var b = 2; diff --git a/test/examples/cycles/c.js b/test/examples/cycles/c.js new file mode 100644 index 0000000..c9a5044 --- /dev/null +++ b/test/examples/cycles/c.js @@ -0,0 +1,9 @@ +/* jshint esnext:true */ + +import { a, getb } from './a'; +import { b, geta } from './b'; + +assert.equal(geta(), 1); +assert.equal(a, 1); +assert.equal(getb(), 2); +assert.equal(b, 2); diff --git a/test/examples/export-and-import-reference-share-var/first.js b/test/examples/export-and-import-reference-share-var/first.js new file mode 100644 index 0000000..56ee87e --- /dev/null +++ b/test/examples/export-and-import-reference-share-var/first.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +export var a = 1; +assert.equal(a, 1); \ No newline at end of file diff --git a/test/examples/export-and-import-reference-share-var/second.js b/test/examples/export-and-import-reference-share-var/second.js new file mode 100644 index 0000000..f1677a0 --- /dev/null +++ b/test/examples/export-and-import-reference-share-var/second.js @@ -0,0 +1,15 @@ +/* jshint esnext:true */ + +import { a } from './first'; + +// This variable declaration is going to be altered because `b` needs to be +// re-written. We need to make sure that the `a` re-writing and the unaffected +// `c` declarator are not being clobbered by that alteration. +var a_ = a, b = 9, c = 'c'; + +assert.equal(a, 1); +assert.equal(a_, 1); +assert.equal(b, 9); +assert.equal(c, 'c'); + +export { b }; diff --git a/test/examples/export-default/exporter.js b/test/examples/export-default/exporter.js new file mode 100644 index 0000000..39138f4 --- /dev/null +++ b/test/examples/export-default/exporter.js @@ -0,0 +1,16 @@ +/* jshint esnext:true */ + +var a = 42; + +export function change() { + a++; +} + +assert.equal(a, 42); +export default a; + +// Any replacement for the `export default` above needs to happen in the same +// location. It cannot be done, say, at the end of the file. Otherwise the new +// value of `a` will be used and will be incorrect. +a = 0; +assert.equal(a, 0); diff --git a/test/examples/export-default/importer.js b/test/examples/export-default/importer.js new file mode 100644 index 0000000..e368b11 --- /dev/null +++ b/test/examples/export-default/importer.js @@ -0,0 +1,12 @@ +/* jshint esnext:true */ + +import value from './exporter'; +import { change } from './exporter'; +assert.equal(value, 42); + +change(); +assert.equal( + value, + 42, + 'default export should not be bound' +); diff --git a/test/examples/export-from/first.js b/test/examples/export-from/first.js new file mode 100644 index 0000000..c17a6e9 --- /dev/null +++ b/test/examples/export-from/first.js @@ -0,0 +1,3 @@ +/* jshint esnext:true */ + +export var a = 1; diff --git a/test/examples/export-from/second.js b/test/examples/export-from/second.js new file mode 100644 index 0000000..3d0b980 --- /dev/null +++ b/test/examples/export-from/second.js @@ -0,0 +1,7 @@ +/* jshint esnext:true */ + +export { a } from './first'; + +// This `a` reference should not be re-written because this export is not +// creating a local binding. +assert.equal(typeof a, 'undefined'); diff --git a/test/examples/export-from/third.js b/test/examples/export-from/third.js new file mode 100644 index 0000000..95314fc --- /dev/null +++ b/test/examples/export-from/third.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import { a } from './second'; +assert.equal(a, 1); diff --git a/test/examples/export-function/exporter.js b/test/examples/export-function/exporter.js new file mode 100644 index 0000000..f74c2ee --- /dev/null +++ b/test/examples/export-function/exporter.js @@ -0,0 +1,6 @@ +/* jshint esnext:true */ + +export function foo() { + return 121; +} +assert.equal(foo(), 121); diff --git a/test/examples/export-function/importer.js b/test/examples/export-function/importer.js new file mode 100644 index 0000000..c2bd539 --- /dev/null +++ b/test/examples/export-function/importer.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import { foo } from './exporter'; +assert.equal(foo(), 121); diff --git a/test/examples/export-list/exporter.js b/test/examples/export-list/exporter.js new file mode 100644 index 0000000..66dbba8 --- /dev/null +++ b/test/examples/export-list/exporter.js @@ -0,0 +1,10 @@ +/* jshint esnext:true */ + +var a = 1; +var b = 2; + +function incr() { + a++; b++; +} + +export { a, b, incr }; diff --git a/test/examples/export-list/importer.js b/test/examples/export-list/importer.js new file mode 100644 index 0000000..569d434 --- /dev/null +++ b/test/examples/export-list/importer.js @@ -0,0 +1,9 @@ +/* jshint esnext:true */ + +import { a, b, incr } from './exporter'; + +assert.equal(a, 1); +assert.equal(b, 2); +incr(); +assert.equal(a, 2); +assert.equal(b, 3); diff --git a/test/examples/export-var/exporter.js b/test/examples/export-var/exporter.js new file mode 100644 index 0000000..e35920b --- /dev/null +++ b/test/examples/export-var/exporter.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +export var a = 1; +assert.equal(a, 1); diff --git a/test/examples/export-var/importer.js b/test/examples/export-var/importer.js new file mode 100644 index 0000000..e23ff4b --- /dev/null +++ b/test/examples/export-var/importer.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import { a } from './exporter'; +assert.equal(a, 1); diff --git a/test/examples/import-as/exporter.js b/test/examples/import-as/exporter.js new file mode 100644 index 0000000..6c6f1bd --- /dev/null +++ b/test/examples/import-as/exporter.js @@ -0,0 +1,5 @@ +/* jshint esnext:true */ + +export var a = 'a'; +export var b = 'b'; +export default 'DEF'; diff --git a/test/examples/import-as/importer.js b/test/examples/import-as/importer.js new file mode 100644 index 0000000..e9cc6b9 --- /dev/null +++ b/test/examples/import-as/importer.js @@ -0,0 +1,7 @@ +/* jshint esnext:true */ + +import { a as b, b as a, default as def } from './exporter'; + +assert.equal(b, 'a'); +assert.equal(a, 'b'); +assert.equal(def, 'DEF'); diff --git a/test/examples/import-chain/first.js b/test/examples/import-chain/first.js new file mode 100644 index 0000000..0cc3795 --- /dev/null +++ b/test/examples/import-chain/first.js @@ -0,0 +1,3 @@ +/* jshint esnext:true */ + +export var value = 42; diff --git a/test/examples/import-chain/second.js b/test/examples/import-chain/second.js new file mode 100644 index 0000000..e111ea3 --- /dev/null +++ b/test/examples/import-chain/second.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import { value } from './first'; +export { value }; diff --git a/test/examples/import-chain/third.js b/test/examples/import-chain/third.js new file mode 100644 index 0000000..c608ea4 --- /dev/null +++ b/test/examples/import-chain/third.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import { value } from './second'; +assert.equal(value, 42); diff --git a/test/examples/re-export-default-import/first.js b/test/examples/re-export-default-import/first.js new file mode 100644 index 0000000..81b78df --- /dev/null +++ b/test/examples/re-export-default-import/first.js @@ -0,0 +1,5 @@ +/* jshint esnext:true */ + +export default function hi() { + return 'hi'; +} diff --git a/test/examples/re-export-default-import/second.js b/test/examples/re-export-default-import/second.js new file mode 100644 index 0000000..d3c9eee --- /dev/null +++ b/test/examples/re-export-default-import/second.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import hi from './first'; +export { hi }; diff --git a/test/examples/re-export-default-import/third.js b/test/examples/re-export-default-import/third.js new file mode 100644 index 0000000..1e0a5a4 --- /dev/null +++ b/test/examples/re-export-default-import/third.js @@ -0,0 +1,4 @@ +/* jshint esnext:true */ + +import { hi } from './second'; +assert.equal(hi(), 'hi'); diff --git a/test/features/bare_import.amd.js b/test/features/bare_import.amd.js deleted file mode 100644 index d322410..0000000 --- a/test/features/bare_import.amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ["foo"], - function(__dependency1__) { - "use strict"; - - }); - diff --git a/test/features/bare_import.cjs.js b/test/features/bare_import.cjs.js deleted file mode 100644 index 9650456..0000000 --- a/test/features/bare_import.cjs.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -require("foo"); diff --git a/test/features/bare_import.es6.js b/test/features/bare_import.es6.js deleted file mode 100644 index c074830..0000000 --- a/test/features/bare_import.es6.js +++ /dev/null @@ -1 +0,0 @@ -import "foo"; diff --git a/test/features/bare_import.yui.js b/test/features/bare_import.yui.js deleted file mode 100644 index 9af13ba..0000000 --- a/test/features/bare_import.yui.js +++ /dev/null @@ -1,5 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - - return __exports__; -}, "@VERSION@", {"es":true,"requires":["foo"]}); \ No newline at end of file diff --git a/test/features/block_comments.amd.js b/test/features/block_comments.amd.js deleted file mode 100644 index ec8fe03..0000000 --- a/test/features/block_comments.amd.js +++ /dev/null @@ -1,10 +0,0 @@ -define( - ["rsvp"], - function(__dependency1__) { - "use strict"; - var async = __dependency1__.async; - /* import { foo } from "foo"; - import { bazz } from "bazz"; - import { bar } from "bar"; - import { buzz } from "buzz"; */ - }); diff --git a/test/features/block_comments.cjs.js b/test/features/block_comments.cjs.js deleted file mode 100644 index b1bd2dc..0000000 --- a/test/features/block_comments.cjs.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -var async = require("rsvp").async; -/* import { foo } from "foo"; -import { bazz } from "bazz"; -import { bar } from "bar"; -import { buzz } from "buzz"; */ diff --git a/test/features/block_comments.es6.js b/test/features/block_comments.es6.js deleted file mode 100644 index 7131041..0000000 --- a/test/features/block_comments.es6.js +++ /dev/null @@ -1,7 +0,0 @@ -/* transpile: imports=rsvp:RSVP */ - -import { async } from "rsvp"; -/* import { foo } from "foo"; -import { bazz } from "bazz"; -import { bar } from "bar"; -import { buzz } from "buzz"; */ diff --git a/test/features/block_comments.globals.js b/test/features/block_comments.globals.js deleted file mode 100644 index 96d31d3..0000000 --- a/test/features/block_comments.globals.js +++ /dev/null @@ -1,8 +0,0 @@ -(function(__dependency1__) { - "use strict"; - var async = __dependency1__.async; - /* import { foo } from "foo"; - import { bazz } from "bazz"; - import { bar } from "bar"; - import { buzz } from "buzz"; */ -})(window.RSVP); diff --git a/test/features/block_comments.yui.js b/test/features/block_comments.yui.js deleted file mode 100644 index dce33e1..0000000 --- a/test/features/block_comments.yui.js +++ /dev/null @@ -1,9 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var async = __imports__["rsvp"].async; - /* import { foo } from "foo"; - import { bazz } from "bazz"; - import { bar } from "bar"; - import { buzz } from "buzz"; */ - return __exports__; -}, "@VERSION@", {"es":true,"requires":["rsvp"]}); \ No newline at end of file diff --git a/test/features/compatfix_option.amd.js b/test/features/compatfix_option.amd.js deleted file mode 100644 index 31222d4..0000000 --- a/test/features/compatfix_option.amd.js +++ /dev/null @@ -1,6 +0,0 @@ -define( - ["jquery"], - function(__dependency1__) { - "use strict"; - var $ = __dependency1__["default"] || __dependency1__; - }); diff --git a/test/features/compatfix_option.cjs.js b/test/features/compatfix_option.cjs.js deleted file mode 100644 index 1dc0c36..0000000 --- a/test/features/compatfix_option.cjs.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -var $ = require("jquery")["default"] || require("jquery"); diff --git a/test/features/compatfix_option.es6.js b/test/features/compatfix_option.es6.js deleted file mode 100644 index 96b643a..0000000 --- a/test/features/compatfix_option.es6.js +++ /dev/null @@ -1,3 +0,0 @@ -/* transpile: compatFix=true */ - -import $ from 'jquery'; diff --git a/test/features/compatfix_option.yui.js b/test/features/compatfix_option.yui.js deleted file mode 100644 index bf6383b..0000000 --- a/test/features/compatfix_option.yui.js +++ /dev/null @@ -1,5 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var $ = __imports__["jquery"]["default"] || __imports__["jquery"]; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["jquery"]}); \ No newline at end of file diff --git a/test/features/export_default.amd.js b/test/features/export_default.amd.js deleted file mode 100644 index 414f3be..0000000 --- a/test/features/export_default.amd.js +++ /dev/null @@ -1,8 +0,0 @@ -define( - ["exports"], - function(__exports__) { - "use strict"; - var jQuery = function() { }; - - __exports__["default"] = jQuery; - }); diff --git a/test/features/export_default.cjs.js b/test/features/export_default.cjs.js deleted file mode 100644 index 47375a9..0000000 --- a/test/features/export_default.cjs.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -var jQuery = function() { }; - -exports["default"] = jQuery; diff --git a/test/features/export_default.es6.js b/test/features/export_default.es6.js deleted file mode 100644 index ed4711e..0000000 --- a/test/features/export_default.es6.js +++ /dev/null @@ -1,3 +0,0 @@ -var jQuery = function() { }; - -export default jQuery; diff --git a/test/features/export_default.globals.js b/test/features/export_default.globals.js deleted file mode 100644 index 86d5e71..0000000 --- a/test/features/export_default.globals.js +++ /dev/null @@ -1,6 +0,0 @@ -(function(__exports__) { - "use strict"; - var jQuery = function() { }; - - __exports__.jQuery = jQuery; -})(window); diff --git a/test/features/export_default.yui.js b/test/features/export_default.yui.js deleted file mode 100644 index 419bc24..0000000 --- a/test/features/export_default.yui.js +++ /dev/null @@ -1,7 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var jQuery = function() { }; - - __exports__["default"] = jQuery; - return __exports__; -}, "@VERSION@", {"es":true,"requires":[]}); \ No newline at end of file diff --git a/test/features/export_from_module.amd.js b/test/features/export_from_module.amd.js deleted file mode 100644 index 6fa7f70..0000000 --- a/test/features/export_from_module.amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ["path","exports"], - function(__dependency1__, __exports__) { - "use strict"; - __exports__.join = __dependency1__.join; - __exports__.extname = __dependency1__.extname; - }); diff --git a/test/features/export_from_module.cjs.js b/test/features/export_from_module.cjs.js deleted file mode 100644 index 2d75453..0000000 --- a/test/features/export_from_module.cjs.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -exports.join = require("path").join; -exports.extname = require("path").extname; diff --git a/test/features/export_from_module.es6.js b/test/features/export_from_module.es6.js deleted file mode 100644 index 3f35d32..0000000 --- a/test/features/export_from_module.es6.js +++ /dev/null @@ -1,3 +0,0 @@ -/* transpile: imports=path:path */ - -export { join, extname } from "path"; diff --git a/test/features/export_from_module.globals.js b/test/features/export_from_module.globals.js deleted file mode 100644 index b916c0f..0000000 --- a/test/features/export_from_module.globals.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(__exports__, __dependency1__) { - "use strict"; - __exports__.join = __dependency1__.join; - __exports__.extname = __dependency1__.extname; -})(window, window.path); diff --git a/test/features/export_from_module.yui.js b/test/features/export_from_module.yui.js deleted file mode 100644 index 8826bfb..0000000 --- a/test/features/export_from_module.yui.js +++ /dev/null @@ -1,6 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - __exports__.join = __imports__["path"].join; - __exports__.extname = __imports__["path"].extname; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["path"]}); \ No newline at end of file diff --git a/test/features/export_function.amd.js b/test/features/export_function.amd.js deleted file mode 100644 index 6f4ce8e..0000000 --- a/test/features/export_function.amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ["exports"], - function(__exports__) { - "use strict"; - function jQuery() { }; - __exports__.jQuery = jQuery; - }); diff --git a/test/features/export_function.cjs.js b/test/features/export_function.cjs.js deleted file mode 100644 index 260399e..0000000 --- a/test/features/export_function.cjs.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -function jQuery() { }; -exports.jQuery = jQuery; diff --git a/test/features/export_function.es6.js b/test/features/export_function.es6.js deleted file mode 100644 index 43e4a40..0000000 --- a/test/features/export_function.es6.js +++ /dev/null @@ -1 +0,0 @@ -export function jQuery() { }; diff --git a/test/features/export_function.globals.js b/test/features/export_function.globals.js deleted file mode 100644 index 0402fdf..0000000 --- a/test/features/export_function.globals.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(__exports__) { - "use strict"; - function jQuery() { }; - __exports__.jQuery = jQuery; -})(window); diff --git a/test/features/export_function.yui.js b/test/features/export_function.yui.js deleted file mode 100644 index 1fbf3c2..0000000 --- a/test/features/export_function.yui.js +++ /dev/null @@ -1,6 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - function jQuery() { }; - __exports__.jQuery = jQuery; - return __exports__; -}, "@VERSION@", {"es":true,"requires":[]}); \ No newline at end of file diff --git a/test/features/export_identifier.amd.js b/test/features/export_identifier.amd.js deleted file mode 100644 index eec06f9..0000000 --- a/test/features/export_identifier.amd.js +++ /dev/null @@ -1,8 +0,0 @@ -define( - ["exports"], - function(__exports__) { - "use strict"; - var jQuery = function() { }; - - __exports__.jQuery = jQuery; - }); diff --git a/test/features/export_identifier.cjs.js b/test/features/export_identifier.cjs.js deleted file mode 100644 index 009936f..0000000 --- a/test/features/export_identifier.cjs.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -var jQuery = function() { }; - -exports.jQuery = jQuery; diff --git a/test/features/export_identifier.es6.js b/test/features/export_identifier.es6.js deleted file mode 100644 index 4f883c6..0000000 --- a/test/features/export_identifier.es6.js +++ /dev/null @@ -1,3 +0,0 @@ -var jQuery = function() { }; - -export { jQuery }; diff --git a/test/features/export_identifier.globals.js b/test/features/export_identifier.globals.js deleted file mode 100644 index 86d5e71..0000000 --- a/test/features/export_identifier.globals.js +++ /dev/null @@ -1,6 +0,0 @@ -(function(__exports__) { - "use strict"; - var jQuery = function() { }; - - __exports__.jQuery = jQuery; -})(window); diff --git a/test/features/export_identifier.yui.js b/test/features/export_identifier.yui.js deleted file mode 100644 index 468ef17..0000000 --- a/test/features/export_identifier.yui.js +++ /dev/null @@ -1,7 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var jQuery = function() { }; - - __exports__.jQuery = jQuery; - return __exports__; -}, "@VERSION@", {"es":true,"requires":[]}); \ No newline at end of file diff --git a/test/features/export_specifier_set.amd.js b/test/features/export_specifier_set.amd.js deleted file mode 100644 index 9d4e6d5..0000000 --- a/test/features/export_specifier_set.amd.js +++ /dev/null @@ -1,10 +0,0 @@ -define( - ["exports"], - function(__exports__) { - "use strict"; - var get = function() { }; - var set = function() { }; - - __exports__.get = get; - __exports__.set = set; - }); diff --git a/test/features/export_specifier_set.cjs.js b/test/features/export_specifier_set.cjs.js deleted file mode 100644 index 827c922..0000000 --- a/test/features/export_specifier_set.cjs.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -var get = function() { }; -var set = function() { }; - -exports.get = get; -exports.set = set; diff --git a/test/features/export_specifier_set.es6.js b/test/features/export_specifier_set.es6.js deleted file mode 100644 index 27886a3..0000000 --- a/test/features/export_specifier_set.es6.js +++ /dev/null @@ -1,6 +0,0 @@ -/* transpile: into=Ember */ - -var get = function() { }; -var set = function() { }; - -export { get, set }; diff --git a/test/features/export_specifier_set.globals.js b/test/features/export_specifier_set.globals.js deleted file mode 100644 index 5f896f9..0000000 --- a/test/features/export_specifier_set.globals.js +++ /dev/null @@ -1,8 +0,0 @@ -(function(__exports__) { - "use strict"; - var get = function() { }; - var set = function() { }; - - __exports__.get = get; - __exports__.set = set; -})(window.Ember = {}); diff --git a/test/features/export_specifier_set.yui.js b/test/features/export_specifier_set.yui.js deleted file mode 100644 index 7a7431c..0000000 --- a/test/features/export_specifier_set.yui.js +++ /dev/null @@ -1,9 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var get = function() { }; - var set = function() { }; - - __exports__.get = get; - __exports__.set = set; - return __exports__; -}, "@VERSION@", {"es":true,"requires":[]}); \ No newline at end of file diff --git a/test/features/export_var.amd.js b/test/features/export_var.amd.js deleted file mode 100644 index 6a18386..0000000 --- a/test/features/export_var.amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ["exports"], - function(__exports__) { - "use strict"; - var jQuery = function() { }; - __exports__.jQuery = jQuery; - }); diff --git a/test/features/export_var.cjs.js b/test/features/export_var.cjs.js deleted file mode 100644 index d69bb54..0000000 --- a/test/features/export_var.cjs.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -var jQuery = function() { }; -exports.jQuery = jQuery; diff --git a/test/features/export_var.es6.js b/test/features/export_var.es6.js deleted file mode 100644 index 7084044..0000000 --- a/test/features/export_var.es6.js +++ /dev/null @@ -1 +0,0 @@ -export var jQuery = function() { }; diff --git a/test/features/export_var.globals.js b/test/features/export_var.globals.js deleted file mode 100644 index fb80c19..0000000 --- a/test/features/export_var.globals.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(__exports__) { - "use strict"; - var jQuery = function() { }; - __exports__.jQuery = jQuery; -})(window); diff --git a/test/features/export_var.yui.js b/test/features/export_var.yui.js deleted file mode 100644 index 0cf4af4..0000000 --- a/test/features/export_var.yui.js +++ /dev/null @@ -1,6 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var jQuery = function() { }; - __exports__.jQuery = jQuery; - return __exports__; -}, "@VERSION@", {"es":true,"requires":[]}); \ No newline at end of file diff --git a/test/features/import_default.amd.js b/test/features/import_default.amd.js deleted file mode 100644 index 0975d6e..0000000 --- a/test/features/import_default.amd.js +++ /dev/null @@ -1,6 +0,0 @@ -define( - ["rsvp"], - function(__dependency1__) { - "use strict"; - var RSVP = __dependency1__["default"]; - }); diff --git a/test/features/import_default.cjs.js b/test/features/import_default.cjs.js deleted file mode 100644 index e1dc093..0000000 --- a/test/features/import_default.cjs.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -var RSVP = require("rsvp")["default"]; diff --git a/test/features/import_default.es6.js b/test/features/import_default.es6.js deleted file mode 100644 index a070ae4..0000000 --- a/test/features/import_default.es6.js +++ /dev/null @@ -1,3 +0,0 @@ -/* transpile: imports=rsvp:RSVP */ - -import RSVP from 'rsvp'; diff --git a/test/features/import_default.globals.js b/test/features/import_default.globals.js deleted file mode 100644 index ca525b9..0000000 --- a/test/features/import_default.globals.js +++ /dev/null @@ -1,4 +0,0 @@ -(function(__dependency1__) { - "use strict"; - var RSVP = __dependency1__; -})(window.RSVP); diff --git a/test/features/import_default.yui.js b/test/features/import_default.yui.js deleted file mode 100644 index f1f3e96..0000000 --- a/test/features/import_default.yui.js +++ /dev/null @@ -1,5 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var RSVP = __imports__["rsvp"]["default"]; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["rsvp"]}); \ No newline at end of file diff --git a/test/features/import_default_as.amd.js b/test/features/import_default_as.amd.js deleted file mode 100644 index 46f2048..0000000 --- a/test/features/import_default_as.amd.js +++ /dev/null @@ -1,6 +0,0 @@ -define( - ["ember"], - function(__dependency1__) { - "use strict"; - var Ember = __dependency1__.default; - }); diff --git a/test/features/import_default_as.es6.js b/test/features/import_default_as.es6.js deleted file mode 100644 index 89fb1fe..0000000 --- a/test/features/import_default_as.es6.js +++ /dev/null @@ -1 +0,0 @@ -import { default as Ember } from "ember"; diff --git a/test/features/import_default_as.yui.js b/test/features/import_default_as.yui.js deleted file mode 100644 index 4f032f0..0000000 --- a/test/features/import_default_as.yui.js +++ /dev/null @@ -1,5 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var Ember = __imports__["ember"]["default"]; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["ember"]}); \ No newline at end of file diff --git a/test/features/import_module.amd.js b/test/features/import_module.amd.js deleted file mode 100644 index 74eaad8..0000000 --- a/test/features/import_module.amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ["foo","./foo/bar"], - function(__dependency1__, __dependency2__) { - "use strict"; - var foo = __dependency1__; - var bar = __dependency2__; - }); diff --git a/test/features/import_module.cjs.js b/test/features/import_module.cjs.js deleted file mode 100644 index a650542..0000000 --- a/test/features/import_module.cjs.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -function __es6_transpiler_warn__(warning) { - if (typeof console === 'undefined') { - } else if (typeof console.warn === "function") { - console.warn(warning); - } else if (typeof console.log === "function") { - console.log(warning); - } -} -function __es6_transpiler_build_module_object__(name, imported) { - var moduleInstanceObject = Object.create ? Object.create(null) : {}; - if (typeof imported === "function") { - __es6_transpiler_warn__("imported module '"+name+"' exported a function - this may not work as expected"); - } - for (var key in imported) { - if (Object.prototype.hasOwnProperty.call(imported, key)) { - moduleInstanceObject[key] = imported[key]; - } - } - if (Object.freeze) { - Object.freeze(moduleInstanceObject); - } - return moduleInstanceObject; -} -var foo = __es6_transpiler_build_module_object__("foo", require("foo")); -var bar = __es6_transpiler_build_module_object__("bar", require("./foo/bar")); diff --git a/test/features/import_module.es6.js b/test/features/import_module.es6.js deleted file mode 100644 index 34afc1f..0000000 --- a/test/features/import_module.es6.js +++ /dev/null @@ -1,2 +0,0 @@ -module foo from "foo"; -module bar from "./foo/bar"; diff --git a/test/features/import_module.yui.js b/test/features/import_module.yui.js deleted file mode 100644 index 9905603..0000000 --- a/test/features/import_module.yui.js +++ /dev/null @@ -1,6 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var foo = __imports__["foo"]; - var bar = __imports__["./foo/bar"]; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["foo","./foo/bar"]}); diff --git a/test/features/import_specifier.amd.js b/test/features/import_specifier.amd.js deleted file mode 100644 index 4b5ce97..0000000 --- a/test/features/import_specifier.amd.js +++ /dev/null @@ -1,6 +0,0 @@ -define( - ["foo"], - function(__dependency1__) { - "use strict"; - - }); diff --git a/test/features/import_specifier.cjs.js b/test/features/import_specifier.cjs.js deleted file mode 100644 index 9650456..0000000 --- a/test/features/import_specifier.cjs.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -require("foo"); diff --git a/test/features/import_specifier.es6.js b/test/features/import_specifier.es6.js deleted file mode 100644 index c074830..0000000 --- a/test/features/import_specifier.es6.js +++ /dev/null @@ -1 +0,0 @@ -import "foo"; diff --git a/test/features/import_specifier.yui.js b/test/features/import_specifier.yui.js deleted file mode 100644 index 9af13ba..0000000 --- a/test/features/import_specifier.yui.js +++ /dev/null @@ -1,5 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - - return __exports__; -}, "@VERSION@", {"es":true,"requires":["foo"]}); \ No newline at end of file diff --git a/test/features/import_specifier_set.amd.js b/test/features/import_specifier_set.amd.js deleted file mode 100644 index edf23b4..0000000 --- a/test/features/import_specifier_set.amd.js +++ /dev/null @@ -1,8 +0,0 @@ -define( - ["ember","rsvp"], - function(__dependency1__, __dependency2__) { - "use strict"; - var get = __dependency1__.get; - var set = __dependency1__.set; - var makeDeferred = __dependency2__.defer; - }); diff --git a/test/features/import_specifier_set.cjs.js b/test/features/import_specifier_set.cjs.js deleted file mode 100644 index 2efbce9..0000000 --- a/test/features/import_specifier_set.cjs.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -var get = require("ember").get; -var set = require("ember").set; -var makeDeferred = require("rsvp").defer; diff --git a/test/features/import_specifier_set.es6.js b/test/features/import_specifier_set.es6.js deleted file mode 100644 index 35e971d..0000000 --- a/test/features/import_specifier_set.es6.js +++ /dev/null @@ -1,4 +0,0 @@ -/* transpile: imports=ember:Ember,rsvp:RSVP */ - -import { get, set } from 'ember'; -import { defer as makeDeferred } from 'rsvp'; diff --git a/test/features/import_specifier_set.globals.js b/test/features/import_specifier_set.globals.js deleted file mode 100644 index 5fff9ae..0000000 --- a/test/features/import_specifier_set.globals.js +++ /dev/null @@ -1,6 +0,0 @@ -(function(__dependency1__, __dependency2__) { - "use strict"; - var get = __dependency1__.get; - var set = __dependency1__.set; - var makeDeferred = __dependency2__.defer; -})(window.Ember, window.RSVP); diff --git a/test/features/import_specifier_set.yui.js b/test/features/import_specifier_set.yui.js deleted file mode 100644 index 06f7518..0000000 --- a/test/features/import_specifier_set.yui.js +++ /dev/null @@ -1,7 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var get = __imports__["ember"].get; - var set = __imports__["ember"].set; - var makeDeferred = __imports__["rsvp"].defer; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["ember","rsvp"]}); \ No newline at end of file diff --git a/test/features/multi_line_declaration.amd.js b/test/features/multi_line_declaration.amd.js deleted file mode 100644 index 17fe566..0000000 --- a/test/features/multi_line_declaration.amd.js +++ /dev/null @@ -1,8 +0,0 @@ -define( - ["foo","exports"], - function(__dependency1__, __exports__) { - "use strict"; - __exports__["default"] = 1 + 2; - - var foo = __dependency1__.foo; - }); diff --git a/test/features/multi_line_declaration.cjs.js b/test/features/multi_line_declaration.cjs.js deleted file mode 100644 index 95540dd..0000000 --- a/test/features/multi_line_declaration.cjs.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -exports["default"] = 1 + 2; - -var foo = require("foo").foo; diff --git a/test/features/multi_line_declaration.es6.js b/test/features/multi_line_declaration.es6.js deleted file mode 100644 index 8cd5ae2..0000000 --- a/test/features/multi_line_declaration.es6.js +++ /dev/null @@ -1,6 +0,0 @@ -export -default -1 + 2; - -import -{ foo } from "foo"; diff --git a/test/features/multi_line_declaration.yui.js b/test/features/multi_line_declaration.yui.js deleted file mode 100644 index 2cab6cc..0000000 --- a/test/features/multi_line_declaration.yui.js +++ /dev/null @@ -1,7 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - __exports__["default"] = 1 + 2; - - var foo = __imports__["foo"].foo; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["foo"]}); \ No newline at end of file diff --git a/test/features/multiple_import_from_same_path.amd.js b/test/features/multiple_import_from_same_path.amd.js deleted file mode 100644 index 62140e9..0000000 --- a/test/features/multiple_import_from_same_path.amd.js +++ /dev/null @@ -1,7 +0,0 @@ -define( - ["utils"], - function(__dependency1__) { - "use strict"; - var uniq = __dependency1__.uniq; - var forEach = __dependency1__.forEach; - }); diff --git a/test/features/multiple_import_from_same_path.cjs.js b/test/features/multiple_import_from_same_path.cjs.js deleted file mode 100644 index 46717ef..0000000 --- a/test/features/multiple_import_from_same_path.cjs.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict"; -var uniq = require("utils").uniq; -var forEach = require("utils").forEach; diff --git a/test/features/multiple_import_from_same_path.es6.js b/test/features/multiple_import_from_same_path.es6.js deleted file mode 100644 index 4c44b79..0000000 --- a/test/features/multiple_import_from_same_path.es6.js +++ /dev/null @@ -1,4 +0,0 @@ -/* transpile: imports=utils:utils */ - -import { uniq } from 'utils'; -import { forEach } from 'utils'; diff --git a/test/features/multiple_import_from_same_path.globals.js b/test/features/multiple_import_from_same_path.globals.js deleted file mode 100644 index ecc2305..0000000 --- a/test/features/multiple_import_from_same_path.globals.js +++ /dev/null @@ -1,5 +0,0 @@ -(function(__dependency1__) { - "use strict"; - var uniq = __dependency1__.uniq; - var forEach = __dependency1__.forEach; -})(window.utils); diff --git a/test/features/multiple_import_from_same_path.yui.js b/test/features/multiple_import_from_same_path.yui.js deleted file mode 100644 index b0d92cd..0000000 --- a/test/features/multiple_import_from_same_path.yui.js +++ /dev/null @@ -1,6 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var uniq = __imports__["utils"].uniq; - var forEach = __imports__["utils"].forEach; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["utils"]}); \ No newline at end of file diff --git a/test/features/new_default.amd.js b/test/features/new_default.amd.js deleted file mode 100644 index f350154..0000000 --- a/test/features/new_default.amd.js +++ /dev/null @@ -1,13 +0,0 @@ -define( - ["foo","bar","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var foo = __dependency1__; - var bar = __dependency2__["default"]; - - var baz = "baz"; - var qux = "qux"; - - __exports__["default"] = baz; - __exports__.qux = qux; - }); diff --git a/test/features/new_default.cjs.js b/test/features/new_default.cjs.js deleted file mode 100644 index 4bb4c9d..0000000 --- a/test/features/new_default.cjs.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -function __es6_transpiler_warn__(warning) { - if (typeof console === 'undefined') { - } else if (typeof console.warn === "function") { - console.warn(warning); - } else if (typeof console.log === "function") { - console.log(warning); - } -} -function __es6_transpiler_build_module_object__(name, imported) { - var moduleInstanceObject = Object.create ? Object.create(null) : {}; - if (typeof imported === "function") { - __es6_transpiler_warn__("imported module '"+name+"' exported a function - this may not work as expected"); - } - for (var key in imported) { - if (Object.prototype.hasOwnProperty.call(imported, key)) { - moduleInstanceObject[key] = imported[key]; - } - } - if (Object.freeze) { - Object.freeze(moduleInstanceObject); - } - return moduleInstanceObject; -} -var foo = __es6_transpiler_build_module_object__("foo", require("foo")); -var bar = require("bar")["default"]; - -var baz = "baz"; -var qux = "qux"; - -exports["default"] = baz; -exports.qux = qux; diff --git a/test/features/new_default.es6.js b/test/features/new_default.es6.js deleted file mode 100644 index c07c683..0000000 --- a/test/features/new_default.es6.js +++ /dev/null @@ -1,8 +0,0 @@ -module foo from "foo"; -import bar from "bar"; - -var baz = "baz"; -var qux = "qux"; - -export default baz; -export { qux }; diff --git a/test/features/new_default.yui.js b/test/features/new_default.yui.js deleted file mode 100644 index 5a1cebc..0000000 --- a/test/features/new_default.yui.js +++ /dev/null @@ -1,12 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var foo = __imports__["foo"]; - var bar = __imports__["bar"]["default"]; - - var baz = "baz"; - var qux = "qux"; - - __exports__["default"] = baz; - __exports__.qux = qux; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["foo","bar"]}); \ No newline at end of file diff --git a/test/features/semicolons_optional.cjs.js b/test/features/semicolons_optional.cjs.js deleted file mode 100644 index 34ef3dc..0000000 --- a/test/features/semicolons_optional.cjs.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -var ajax = require("jquery").ajax;exports.defer = require("rsvp").defer; diff --git a/test/features/semicolons_optional.es6.js b/test/features/semicolons_optional.es6.js deleted file mode 100644 index 171eb42..0000000 --- a/test/features/semicolons_optional.es6.js +++ /dev/null @@ -1,2 +0,0 @@ -import { ajax } from 'jquery' -export { defer } from 'rsvp' diff --git a/test/features/semicolons_optional.yui.js b/test/features/semicolons_optional.yui.js deleted file mode 100644 index 4529380..0000000 --- a/test/features/semicolons_optional.yui.js +++ /dev/null @@ -1,5 +0,0 @@ -YUI.add("@NAME@", function(Y, NAME, __imports__, __exports__) { - "use strict"; - var ajax = __imports__["jquery"].ajax;__exports__.defer = __imports__["rsvp"].defer; - return __exports__; -}, "@VERSION@", {"es":true,"requires":["jquery","rsvp"]}); \ No newline at end of file diff --git a/test/features/test_template.js.tmpl b/test/features/test_template.js.tmpl deleted file mode 100644 index ebb257f..0000000 --- a/test/features/test_template.js.tmpl +++ /dev/null @@ -1,30 +0,0 @@ -<% - function compiler(test) { - return "var compiler = new Compiler(" + - JSON.stringify(test.source) + - ", null, " + - JSON.stringify(test.options) + - ");"; - } -%> - -var Compiler = require("../../dist/es6-module-transpiler").Compiler; - -suite("<%= mod.name %>"); - -<% _.each(mod.tests, function(test) { %> -test("<%= test.name %>", function() { - <% if (test.invalid) { %> -try { - <%= compiler(test) %> - compiler.toAMD(); - ok(false, "should have raised an error"); -} catch (e) { - ok(e, e ? e.message : "expects an error"); -} - <% } else { %> - <%= compiler(test) %> -equal(compiler.to<%= test.typeName %>(), <%= JSON.stringify(test.expected) %>); - <% } %> -}); -<% }); %> diff --git a/test/runner.js b/test/runner.js new file mode 100644 index 0000000..c66f5c5 --- /dev/null +++ b/test/runner.js @@ -0,0 +1,218 @@ +/* jshint node:true, undef:true, unused:true */ + +Error.stackTraceLimit = 50; + +var fs = require('fs'); +var Path = require('path'); +var vm = require('vm'); +var assert = require('assert'); + +var modules = require('../lib'); +var utils = require('../lib/utils'); +var endsWith = utils.endsWith; + +var examples = Path.join(__dirname, 'examples'); + +var paths = []; +var formatters = require('../lib/formatters'); +var formatterNames = Object.keys(formatters).filter(function(formatter) { + return formatter !== 'DEFAULT'; +}); +var formatter = formatters.DEFAULT; + +var getopt = require('posix-getopt'); +var parser = new getopt.BasicParser('h(help)f:(format)', process.argv); +var option; + +while ((option = parser.getopt()) !== undefined) { + if (option.error) { + usage(); + process.exit(1); + } + + switch (option.option) { + case 'f': + formatter = option.optarg; + if (formatterNames.indexOf(formatter) < 0) { + usage(); + process.exit(1); + } + break; + + case 'h': + usage(); + process.exit(0); + break; + } +} + +paths.push.apply(paths, process.argv.slice(parser.optind())); + +if (paths.length === 0) { + paths = fs.readdirSync(examples).map(function(example) { + return Path.join(examples, example); + }); +} else { + var cwd = process.cwd(); + paths = paths.map(function(example) { + return Path.resolve(cwd, example); + }); +} + +var results = Path.join(__dirname, 'results'); +if (fs.existsSync(results)) { + rmrf(results); +} +fs.mkdirSync(results); +runTests(paths); + +function runTests(paths) { + var passed = 0, failed = 0; + paths.forEach(function(path) { + if (runTestDir(path)) { + passed++; + } else { + failed++; + } + }); + + console.log(); + console.log('%d passed, %s failed.', passed, failed); + process.exit( + (passed + failed === 0) ? 1 : // no tests, fail + failed === 0 ? 0 : // no failed, pass + 1); // some failed, fail +} + +function runTestDir(testDir) { + var passed = false; + var testName = Path.basename(testDir); + + var options = { + resolvers: [new modules.FileResolver([testDir])], + formatter: formatters[formatter] + }; + var container = modules.makeContainer(options); + + try { + fs.readdirSync(testDir).forEach(function(child) { + container.getModule(child); + }); + + var resultPath = Path.join(results, testName + '.js'); + container.write(resultPath); + + var testAssert = wrappedAssert(); + if (fs.statSync(resultPath).isDirectory()) { + fs.readdirSync(resultPath).forEach(function(child) { + requireTestFile('./' + child, resultPath, testAssert); + }); + } else { + requireTestFile(resultPath, process.cwd(), testAssert); + } + + assert.ok( + testAssert.count > 0, + 'expected at least one assertion' + ); + + passed = true; + printSuccess(testName); + } catch (ex) { + printFailure(testName, ex); + console.log(); + } + + return passed; +} + +// TODO: Just use the real node require system with proxyquire? +var testFileCache; +var testFileGlobal; +function requireTestFile(path, relativeTo, assert) { + if (path[0] === '.') { + path = Path.resolve(relativeTo, path); + } + + if (!testFileCache) { testFileCache = {}; } + + if (path in testFileCache) { + return testFileCache[path]; + } else if (!fs.existsSync(path) && !endsWith(path, '.js')) { + return requireTestFile(path + '.js'); + } + + var code = fs.readFileSync(path); + var mod = {exports: {}}; + testFileCache[path] = mod.exports; + + if (!testFileGlobal) { testFileGlobal = {}; } + + vm.runInNewContext(code, { + assert: assert, + global: testFileGlobal, + + module: mod, + exports: mod.exports, + require: function(requiredPath) { + return requireTestFile(requiredPath, Path.dirname(path), assert); + } + }, path); + + testFileCache[path] = mod.exports; + return mod.exports; +} + +function wrappedAssert() { + var result = {count: 0}; + + Object.getOwnPropertyNames(assert).forEach(function(property) { + result[property] = function() { + result.count++; + return assert[property].apply(assert, arguments); + }; + }); + + return result; +} + +function rmrf(path) { + var stat = fs.statSync(path); + if (stat.isDirectory()) { + fs.readdirSync(path).forEach(function(child) { + rmrf(Path.join(path, child)); + }); + fs.rmdirSync(path); + } else if (stat.isFile()) { + fs.unlinkSync(path); + } +} + +/** + * Prints a line to stdout for the given test indicating that it passed. + * + * @param {string} testName + */ +function printSuccess(testName) { + console.log('\x1b[32m✓ \x1b[0m' + testName); +} + +/** + * Prints a line to stdout for the given test indicating that it failed. In + * addition, prints any additional information indented one level. + * + * @param {string} testName + * @param {Error} error + */ +function printFailure(testName, error) { + console.log('\x1b[31m✘ ' + testName + '\x1b[0m'); + console.log(); + console.log(error.stack); +} + +function usage() { + console.log('node test/runner.js [OPTIONS] [EXAMPLE1 [EXAMPLE2 ...]]'); + console.log(); + console.log(' -f, --format Choose from: %s.', formatterNames.join(', ')); + console.log(' -h, --help Show this help message.'); +}