diff --git a/.gitignore b/.gitignore index 86fceae..bcf4db6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,17 @@ /libpeerconnection.log npm-debug.log testem.log + +# linking ember-browserify here, so addon-discovery bug is worked-around. +# linking the dir, so as: +# * not to create a cycyle +# * use the correct version of ember-browserify +# * use symlinks when possible, but correctly fallback to junctions on windows +# (if no symlinks are used) +# * symlinked files turn out to be realpathSync'd before being loaded into +# node, which means this should nicely trick node. Atleast in posix, although +# in windows this may also be true with junctions... Ultimately this is just +# for testing, and its working around a potential addon-discovery bug which +# will hopefully soon be addressed. +tests/dummy/lib/modern/node_modules/ember-browserify/lib +tests/dummy/lib/outdated/node_modules/ember-browserify/lib diff --git a/.travis.yml b/.travis.yml index eae0e25..221a4b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,39 @@ --- language: node_js sudo: false -before_install: - - npm install -g npm@2 - - npm install -g npm +node_js: + - "0.12" + - node + +cache: + directories: + - node_modules + +env: + # we recommend testing LTS's and latest stable release (bonus points to beta/canary) + - EMBER_TRY_SCENARIO=ember-1.13 + - EMBER_TRY_SCENARIO=ember-lts-2.4 + - EMBER_TRY_SCENARIO=ember-release + - EMBER_TRY_SCENARIO=ember-beta + - EMBER_TRY_SCENARIO=ember-canary + matrix: - include: - - node_js: '0.12' - - node_js: node + fast_finish: true + allow_failures: + - env: EMBER_TRY_SCENARIO=ember-canary + +before_install: + - npm config set spin false + - npm install -g bower + - bower --version + - npm install phantomjs-prebuilt + - node_modules/phantomjs-prebuilt/bin/phantomjs --version + +install: + - npm install + - bower install + +script: + # Usually, it's ok to finish the test scenario without reverting + # to the addon's original dependency state, skipping "cleanup". + - ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4469ddd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Mocha", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "args": ["node-tests"], + "cwd": "${workspaceRoot}", + "preLaunchTask": null, + "runtimeExecutable": null, + "runtimeArgs": [ + "--nolazy" + ], + "env": { + "NODE_ENV": "development" + }, + "externalConsole": false, + "sourceMaps": false, + "outDir": null + } + ] +} \ No newline at end of file diff --git a/addon/.gitkeep b/addon/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/.gitkeep b/app/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/appveyor.yml b/appveyor.yml index 3e2f25d..4b37c71 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ install: test_script: # Output useful info for debugging. - npm version - - cmd: npm test + - cmd: npm run node-test # Don't actually build. build: off diff --git a/bin/install-ember-addons.js b/bin/install-ember-addons.js new file mode 100755 index 0000000..737691d --- /dev/null +++ b/bin/install-ember-addons.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +var symlinkOrCopy = require('symlink-or-copy'); +var path = require('path'); +var rimraf = require('rimraf'); +var fs = require('fs'); + +['modern', 'outdated'].forEach(function(inRepoAddon) { + var source = path.resolve(__dirname, '..', 'lib/'); + var target = path.resolve(__dirname, '..', 'tests/dummy/lib', inRepoAddon, 'node_modules/ember-browserify/lib/'); + + console.log('rimraf', target); + rimraf.sync(target); + + console.log('symlink', source, target) + symlinkOrCopy.sync(source, target); +}); diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..6fbb20d --- /dev/null +++ b/bower.json @@ -0,0 +1,7 @@ +{ + "name": "ember-browserify", + "dependencies": { + "ember": "~2.8.0", + "ember-cli-shims": "0.1.1" + } +} diff --git a/config/ember-try.js b/config/ember-try.js new file mode 100644 index 0000000..0355b5f --- /dev/null +++ b/config/ember-try.js @@ -0,0 +1,60 @@ +/*jshint node:true*/ +module.exports = { + scenarios: [ + { + name: 'ember-1.13', + bower: { + dependencies: { + 'ember': '~1.13.0' + }, + resolutions: { + 'ember': '~1.13.0' + } + } + }, + { + name: 'ember-lts-2.4', + bower: { + dependencies: { + 'ember': 'components/ember#lts-2-4' + }, + resolutions: { + 'ember': 'lts-2-4' + } + } + }, + { + name: 'ember-release', + bower: { + dependencies: { + 'ember': 'components/ember#release' + }, + resolutions: { + 'ember': 'release' + } + } + }, + { + name: 'ember-beta', + bower: { + dependencies: { + 'ember': 'components/ember#beta' + }, + resolutions: { + 'ember': 'beta' + } + } + }, + { + name: 'ember-canary', + bower: { + dependencies: { + 'ember': 'components/ember#canary' + }, + resolutions: { + 'ember': 'canary' + } + } + } + ] +}; diff --git a/config/environment.js b/config/environment.js new file mode 100644 index 0000000..28a787b --- /dev/null +++ b/config/environment.js @@ -0,0 +1,6 @@ +/*jshint node:true*/ +'use strict'; + +module.exports = function(/* environment, appConfig */) { + return { }; +}; diff --git a/ember-cli-build.js b/ember-cli-build.js new file mode 100644 index 0000000..4ac3913 --- /dev/null +++ b/ember-cli-build.js @@ -0,0 +1,18 @@ +/*jshint node:true*/ +/* global require, module */ +var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); + +module.exports = function(defaults) { + var app = new EmberAddon(defaults, { + // Add options here + }); + + /* + This build file specifies the options for the dummy test app of this + addon, located in `/tests/dummy` + This build file does *not* influence how the addon or the app using it + behave. You most likely want to be modifying `./index.js` or app's build file + */ + + return app.toTree(); +}; diff --git a/index.js b/index.js deleted file mode 100644 index b58e228..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./lib/index'); diff --git a/lib/.jshintrc b/lib/.jshintrc new file mode 100644 index 0000000..839c191 --- /dev/null +++ b/lib/.jshintrc @@ -0,0 +1,4 @@ +{ + "node": true, + "browser": false +} diff --git a/lib/caching-browserify.js b/lib/caching-browserify.js index d9b35ad..80b25c3 100644 --- a/lib/caching-browserify.js +++ b/lib/caching-browserify.js @@ -1,11 +1,12 @@ +'use strict'; + var fs = require('fs'); var path = require('path'); -var browserify = require('browserify'); var helpers = require('broccoli-kitchen-sink-helpers'); var RSVP = require('rsvp'); var CoreObject = require('core-object'); var mapSeries = require('promise-map-series'); -var merge = require('lodash').merge; +var merge = require('lodash.merge'); var rimraf = require('rimraf'); var symlinkOrCopy = require('symlink-or-copy'); var quickTemp = require('quick-temp'); @@ -16,7 +17,7 @@ var debug = require('debug')('ember-browserify:caching-browserify'); var through = require('through2'); module.exports = CoreObject.extend({ - init: function(inputTree, options){ + init: function(inputTree, options) { if (!options) { options = {}; } @@ -28,11 +29,12 @@ module.exports = CoreObject.extend({ this.fullPaths = typeof options.fullPaths !== 'undefined' ? options.fullPaths : true; this.outputFile = options.outputFile || 'browserify/browserify.js'; this.cache = {}; + quickTemp.makeOrRemake(this, '_inputStaging'); quickTemp.makeOrRemake(this, '_destDir'); }, - description: 'ember-browserify', + description: 'CachingBrowserify', cleanup: function() { if (this._destDir) { @@ -46,18 +48,24 @@ module.exports = CoreObject.extend({ } }, + toString: function() { + return '[' + this.description + ']'; + }, + read: function (readTree) { var self = this; - return readTree(this.inputTree).then(function(inDir){ - return self.checkCache(inDir).then(function(cacheValid){ + + return readTree(this.inputTree).then(function(inDir) { + return self.checkCache(inDir).then(function(cacheValid) { if (!self._outputCache || !cacheValid) { return self._rebuild(inDir); } }); - }).then(function(){ + }).then(function() { rimraf.sync(self._destDir); symlinkOrCopy.sync(self._outputCache, self._destDir); - return self.watchNodeModules(readTree).then(function(){ + + return self.watchNodeModules(readTree).then(function() { return self._destDir; }); }); @@ -65,6 +73,7 @@ module.exports = CoreObject.extend({ _rebuild: function(inDir){ var self = this; + this._watchModules = Object.create(null); // _inputStaging needs to stay at the same path, because the @@ -74,7 +83,8 @@ module.exports = CoreObject.extend({ symlinkOrCopy.sync(inDir + '/' + this.inFile, this._inputStaging + '/' + this.inFile); quickTemp.makeOrRemake(this, '_outputCache'); - return this.updateCache(this._outputCache).catch(function(err){ + + return this.updateCache(this._outputCache).catch(function(err) { rimraf.sync(self._outputCache); delete self._outputCache; throw err; @@ -98,10 +108,10 @@ module.exports = CoreObject.extend({ debug: this.enableSourcemap }, this.browserifyOptions); - var b = browserify(opts); + var b = require('browserify')(opts); ['transforms', 'externals', 'ignores', 'includes'].forEach(function(thing) { if (!opts[thing]) { return; } - opts[thing].forEach(function(args){ + opts[thing].forEach(function(args) { if (!Array.isArray(args)) { args = [args]; } @@ -114,9 +124,10 @@ module.exports = CoreObject.extend({ b = b[thing.replace(/s$/, '')].apply(b, args); }); }); + b.add('./' + self.inFile); - b.on('package', function(pkg){ + b.on('package', function(pkg) { // browserify *used to* reliably put the package's directory in // pkg.__dirname. But as of browser-resolve 1.7.0 that isn't // true, and we sometimes get a value here like @@ -137,7 +148,7 @@ module.exports = CoreObject.extend({ self._watchModules[pkgDir] = true; }); - b.on('dep', function (dep) { + b.on('dep', function(dep) { dep.source = derequire(dep.source); if (typeof dep.id === 'string') { self.cache[dep.id] = dep; @@ -158,7 +169,7 @@ module.exports = CoreObject.extend({ // to prevent unwanted interactions with other code when concatenated // See https://github.com/ef4/ember-browserify/issues/63 // and https://github.com/substack/node-browserify/issues/806 - b.pipeline.get('wrap').push(through.obj(function (row, enc, next) { + b.pipeline.get('wrap').push(through.obj(function(row, enc, next) { var contents = row.toString(); if (contents[contents.length - 1] === ')') { contents += ';'; @@ -178,14 +189,18 @@ module.exports = CoreObject.extend({ var self = this; fs.mkdirSync(path.dirname(outPath)); var start = Date.now(); - return new RSVP.Promise(function (resolve, reject) { - self.bundler().bundle(function (err, data) { - debug('bundle in: %dms', Date.now() - start); - if (err) { - reject(err); - } else { - fs.writeFileSync(outPath, data); - resolve(destDir); + return new RSVP.Promise(function(resolve, reject) { + self.bundler().bundle(function(err, data) { + try { + debug('bundle in: %dms', Date.now() - start); + if (err) { + reject(err); + } else { + fs.writeFileSync(outPath, data); + resolve(destDir); + } + } catch(e) { + reject(e); } }); }); @@ -195,7 +210,7 @@ module.exports = CoreObject.extend({ var self = this; var root = self.normalizePath(self.root); - return mapSeries(Object.keys(self._watchModules), function(dir){ + return mapSeries(Object.keys(self._watchModules), function(dir) { if (!root || root.indexOf(self.normalizePath(dir)) !== 0){ return readTree(dir); } diff --git a/lib/imports-for.js b/lib/imports-for.js new file mode 100644 index 0000000..5f1699b --- /dev/null +++ b/lib/imports-for.js @@ -0,0 +1,93 @@ +'use strict'; + +var acorn = require('acorn'); + +module.exports = importsFor; + +function importsFor(src, fullPath) { + // In host applications the source is already ES5 code. + // In addons the source is ES6 code. + + // First, try to parse as es5 code. Es6 code will return an error. + var result = tryCatch(parseEs5, src); + + // If a syntax error is thrown, we assume this is because src is es6 code. + if (result instanceof Error) { + result = tryCatch(parseEs6, src); + } + + // If result is still an error, there must have been a parse error. + if (result instanceof Error) { + throw new Error('Error parsing code while looking for "npm:" imports: ' + result.stack || result + ' in file: ' + fullPath); + } + + return result; +} + +function forEachNode(node, visit) { + if (node && typeof node === 'object' && !(node instanceof acorn.SourceLocation) && !node._eb_visited) { + node._eb_visited = true; + visit(node); + var keys = Object.keys(node); + for (var i=0; i < keys.length; i++) { + forEachNode(node[keys[i]], visit); + } + } +} + +function head(array) { + return array[0]; +} + +function parseEs5(src) { + var imports = {}; + + var ast = acorn.parse(src, { locations: true }); + + forEachNode(ast, function(entry) { + if (entry.type === 'CallExpression' && entry.callee.name === 'define') { + head(entry.arguments.filter(function(item) { + return item.type === 'ArrayExpression'; + })).elements.filter(function(element) { + return element.value.slice(0, 4) === 'npm:'; + }).forEach(function(element) { + imports[element.value.slice(4)] = element.loc; + }); + } + }); + + return imports; +} + +function parseEs6(src) { + var imports = {}; + + var ast = acorn.parse(src, { + ecmaVersion: 6, + sourceType: 'module', + locations: true + }); + + forEachNode(ast, function(entry) { + if (entry.type === 'ImportDeclaration') { + var source = entry.source.value; + if (source.slice(0,4) === 'npm:') { + if (entry.kind === 'named') { + throw new Error("ember-browserify doesn't support named imports (you tried to import " + entry.specifiers[0].id.name + " from " + source); + } + imports[source.slice(4)] = entry.source.loc; + } + } + }); + + return imports; +} + +function tryCatch(func, arg) { + try { + return func.call(null, arg); + } + catch(e) { + return e; + } +} diff --git a/lib/index.js b/lib/index.js index 3b2e06a..0aea83a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,8 @@ +'use strict'; + // Support old versions of Ember CLI. -function findHost() { - var current = this; +// Nearest thing which provides `.import`` +function findHost(current) { var app; // Keep iterating upward until we don't have a grandparent. @@ -14,73 +16,123 @@ function findHost() { return app; } +// The root host. +function findRoot(current) { + var app; + + // Keep iterating upward until we don't have a grandparent. + // Has to do this grandparent check because at some point we hit the project. + // Stop at lazy engine boundaries. + do { + app = current.app || app; + } while (current.parent && current.parent.parent && (current = current.parent)); + + return app; +} + +// The thing which browserify should run on. +function findTarget(current) { + // If we are the project, or the project's child. + if (!current.parent || !current.parent.parent) { + return current.app; + } else { + return current.parent; + } +} + +var Funnel = require('broccoli-funnel'); + +function getPreprocessor(instance) { + return { + name: 'ember-browserify', + ext: 'js', + toTree: function(tree) { + var type = 'js'; + var target = findTarget(instance); + + var outputFile, options; + + // TODO: Sort out tests. + if (type === 'js'){ + outputFile = 'browserify/browserify.js'; + } else if (type === 'test'){ + outputFile = 'browserify-tests/browserify.js'; + } + + if (outputFile) { + var StubGenerator = require('./stub-generator'); + var CachingBrowserify = require('./caching-browserify'); + var MergeTrees = require('broccoli-merge-trees'); + + options = Object.create(instance.options); + options.outputFile = outputFile; + options.basedir = target.root; + + // produces: + // - browserify_stubs.js (for CachingBrowserify, to build a bundle); + // - any inputFiles that had npm imports + var stubs = new StubGenerator(tree, options); + + tree = new MergeTrees([ + // original files + tree, + + // copies rewritten inputFiles over (overwriting original files) + new Funnel(stubs, { exclude: ['browserify_stubs.js'] }), + + // produces browserify bundle, named options.outputFile (defaulting to browserify/browserify.js) + new CachingBrowserify(stubs, options) + ], { overwrite: true }); + } + + return tree; + } + }; +} + module.exports = { name: 'ember-browserify', - included: function(app){ + included: function(target) { + var host = findHost(this); + var root = findRoot(this); + var project = root.project; + var VersionChecker = require('ember-cli-version-checker'); var checker = new VersionChecker(this); var emberCliVersion = checker.for('ember-cli', 'npm'); - var newImportApi; - if (emberCliVersion.isAbove('1.13.8')) { - newImportApi = true; + if (emberCliVersion.satisfies('< 2.0.0')) { + throw new TypeError('ember-browserify@^2.0.0 no longer supports ember-cli versions less then 2.0.0.'); } - app = findHost.call(this); - - var enableSourcemaps = app.options.sourcemaps && app.options.sourcemaps.enabled && app.options.sourcemaps.extensions.indexOf('js') > -1; - - this.app = app; + var enableSourcemaps = root.options.sourcemaps && root.options.sourcemaps.enabled && root.options.sourcemaps.extensions.indexOf('js') > -1; this.options = { - root: this.app.project.root, - browserifyOptions: app.project.config(app.env).browserify || {}, + root: project.root, + browserifyOptions: project.config(root.env).browserify || {}, enableSourcemap: enableSourcemaps, - fullPaths: app.env !== 'production' + fullPaths: root.env !== 'production' }; - app.import('browserify/browserify.js'); - if (app.tests && (process.env.BROWSERIFY_TESTS || this.options.browserifyOptions.tests)) { - if (newImportApi) { - app.import('browserify-tests/browserify.js', { - type: 'test' - }); - } else { - app.import({ - test: 'browserify-tests/browserify.js' - }); - } + host.import('browserify/browserify.js'); + if (host.tests && (process.env.BROWSERIFY_TESTS || this.options.browserifyOptions.tests)) { + host.import('browserify-tests/browserify.js', { + type: 'test' + }); } - if (app.importWhitelistFilters) { - app.importWhitelistFilters.push(function(moduleName){ + if (host.importWhitelistFilters) { + host.importWhitelistFilters.push(function(moduleName) { return moduleName.slice(0,4) === 'npm:'; }); } }, - postprocessTree: function(type, tree){ - var outputFile, options; - if (type === 'js'){ - outputFile = 'browserify/browserify.js'; - } else if (type === 'test'){ - outputFile = 'browserify-tests/browserify.js'; - } + setupPreprocessorRegistry: function(type, registry) { + if (type === 'self') { return; } - if (outputFile) { - var StubGenerator = require('./stub-generator'); - var CachingBrowserify = require('./caching-browserify'); - var MergeTrees = require('broccoli-merge-trees'); - - options = Object.create(this.options); - options.outputFile = outputFile; - tree = new MergeTrees([ - tree, - new CachingBrowserify(new StubGenerator(tree), options) - ]); - } - return tree; + registry.add('js', getPreprocessor(this), ['js']); } }; diff --git a/lib/rewrite-imports.js b/lib/rewrite-imports.js new file mode 100644 index 0000000..cfcc0e8 --- /dev/null +++ b/lib/rewrite-imports.js @@ -0,0 +1,29 @@ +module.exports = function rewriteImports(content, data) { + var lines = content.split('\n'); + var offsets = []; + + Object.keys(data).forEach(function(name) { + var line = data[name].start.line - 1; + + if (offsets[line] === undefined) { + offsets[line] = 0; + } + + var start = data[name].start.column + offsets[line]; + var end = data[name].end.column + offsets[line]; + var version = data[name].version; + + var pre = lines[line].substring(0, start); + var moduleString = '"npm:' + name + '@' + version + '"'; + var post = lines[line].substring(end); + + lines[line] = [pre, moduleString, post].join(''); + + // Every time we make a replacement we need to adjust the offsets. + // We know that, by rule, these offsets will come in order. + // This means we only need to store the total cumulative offset. + offsets[line] += moduleString.length - (end - start); + }); + + return lines.join('\n'); +}; \ No newline at end of file diff --git a/lib/stub-generator.js b/lib/stub-generator.js index 6574d4a..6f8fba2 100644 --- a/lib/stub-generator.js +++ b/lib/stub-generator.js @@ -1,3 +1,5 @@ +'use strict'; + var Plugin = require('broccoli-plugin'); var FSTree = require('fs-tree-diff'); var walkSync = require('walk-sync'); @@ -5,9 +7,13 @@ var Stubs = require('./stubs'); var fs = require('fs'); var md5Hex = require('md5-hex'); var debug = require('debug')('ember-browserify:stub-generator:'); +var importsFor = require('./imports-for'); +var rewriteImports = require('./rewrite-imports'); +var versionForModuleName = require('./version-for-module-name'); module.exports = StubGenerator; -function StubGenerator(inputTree, options) { +function StubGenerator(inputTree, _options) { + var options = _options || {}; if (!(this instanceof StubGenerator)) { return new StubGenerator([inputTree], options); } @@ -17,7 +23,9 @@ function StubGenerator(inputTree, options) { } Plugin.call(this, [inputTree], options); + this._persistentOutput = true; + this.basedir = options.basedir; // setup persistent state this._previousTree = new FSTree(); @@ -31,10 +39,11 @@ StubGenerator.prototype.constructor = StubGenerator; StubGenerator.prototype.build = function() { var start = Date.now(); var inputPath = this.inputPaths[0]; - var previous = this._previousTree; + var outputPath = this.outputPath; + var previous = this._previousTree; // get patchset - var input = walkSync.entries(inputPath, [ '**/*.js' ]); + var input = walkSync.entries(inputPath); debug('input: %d', input.length); @@ -49,13 +58,36 @@ StubGenerator.prototype.build = function() { patchset.forEach(function(patch) { var operation = patch[0]; var path = patch[1]; - var fullPath = inputPath + '/' + path; + var fullInputPath = inputPath + '/' + path; + var fullOutputPath = outputPath + '/' + path; switch (operation) { - case 'unlink': this.stubs.delete(fullPath); break; + case 'unlink': + if (!/\.js$/.test(path)) { break; } + // TODO: only add files that required rewriting + fs.unlinkSync(fullOutputPath); + this.stubs.delete(fullInputPath); + break; + case 'mkdir': fs.mkdirSync(fullOutputPath); break; + case 'rmdir': fs.rmdir(fullOutputPath); break; case 'create': - case 'change': this.stubs.set(fullPath, fs.readFileSync(fullPath)); break; + case 'change': + if (!/\.js$/.test(path)) { break; } + + var content = fs.readFileSync(fullInputPath, 'UTF8'); + var imports = importsFor(content, fullInputPath); + var needsImportsReWritten = false; + + Object.keys(imports).forEach(function(name) { + imports[name].version = versionForModuleName(name, this.basedir); + }, this); + + fs.writeFileSync(fullOutputPath, rewriteImports(content, imports)); + this.stubs.set(fullInputPath, imports); + + break; } + }, this); debug('patched applied in: %dms', Date.now() - applyPatch); diff --git a/lib/stubs.js b/lib/stubs.js index 6fa0803..42140a5 100644 --- a/lib/stubs.js +++ b/lib/stubs.js @@ -1,5 +1,4 @@ module.exports = Stub; -var acorn = require('acorn'); var debug = require('debug')('ember-browserify:stubs'); function Stub() { @@ -25,10 +24,10 @@ Stub.prototype.delete = function(fullPath) { } }; -Stub.prototype.set = function(fullPath, content) { +Stub.prototype.set = function(fullPath, imports) { this._isDirty = true; var start = Date.now(); - this._map[fullPath] = importsFor(content, fullPath); + this._map[fullPath] = imports; this._stats.importTime += (Date.now() - start); }; @@ -48,98 +47,15 @@ Stub.prototype.toAMD = function() { // find unique modules Object.keys(this._map).forEach(function(filePath) { (Object.keys(this._map[filePath] || {})).forEach(function(moduleName) { - imports[moduleName] = true; - }); + imports[moduleName] = this._map[filePath][moduleName]; + }, this); }, this); // generate stub this._amd = Object.keys(imports).sort().map(function(moduleName) { - return "define('npm:" + moduleName + "', function(){ return { 'default': require('" + moduleName + "')};})"; + var version = imports[moduleName].version; + return "define('npm:" + moduleName + '@' + version + "', function(){ return { 'default': require('" + moduleName + "')};})"; }).join("\n"); return this._amd; }; - -function importsFor(src, fullPath) { - // In ember cli 2.x, src is es5 code, whereas in ember cli 1.x, src is still es6 code. - - // First, try to parse as es5 code. Es6 code will return an error. - var result = tryCatch(parseEs5, src); - // If a syntax error is thrown, we assume this is because src is es6 code. - if (result instanceof Error) { - result = tryCatch(parseEs6, src); - } - - // If result is still an error, there must have been a parse error - if (result instanceof Error) { - throw new Error('Error parsing code while looking for "npm:" imports: ' + result.stack || result + ' in file: ' + fullPath); - } - - return result; -} - -function forEachNode(node, visit) { - if (node && typeof node === 'object' && !node._eb_visited) { - node._eb_visited = true; - visit(node); - var keys = Object.keys(node); - for (var i=0; i < keys.length; i++) { - forEachNode(node[keys[i]], visit); - } - } -} - -function head(array) { - return array[0]; -} - -function parseEs5(src) { - var imports = {}; - - var ast = acorn.parse(src); - - forEachNode(ast, function(entry) { - if (entry.type === 'CallExpression' && entry.callee.name === 'define') { - head(entry.arguments.filter(function(item) { - return item.type === 'ArrayExpression'; - })).elements.filter(function(element) { - return element.value.slice(0, 4) === 'npm:'; - }).forEach(function(element) { - imports[element.value.slice(4)] = true; - }); - } - }); - return imports; -} - -function parseEs6(src) { - var imports = {}; - - var ast = acorn.parse(src, { - ecmaVersion: 6, - sourceType: 'module' - }); - - forEachNode(ast, function(entry) { - if (entry.type === 'ImportDeclaration') { - var source = entry.source.value; - if (source.slice(0,4) === 'npm:') { - if (entry.kind === 'named') { - throw new Error("ember-browserify doesn't support named imports (you tried to import " + entry.specifiers[0].id.name + " from " + source); - } - imports[source.slice(4)] = true; - } - } - }); - - return imports; -} - -function tryCatch(func, arg) { - try { - return func.call(null, arg); - } - catch(e) { - return e; - } -} diff --git a/lib/version-for-module-name.js b/lib/version-for-module-name.js new file mode 100644 index 0000000..dfac37d --- /dev/null +++ b/lib/version-for-module-name.js @@ -0,0 +1,18 @@ +'use strict'; + +var resolve = require('resolve'); +var findUp = require('find-up'); +var path = require('path'); +var fs = require('fs'); + +module.exports = function versionForModuleName(name, basedir) { + var cwd = path.dirname(resolve.sync(name, { + basedir: basedir + })); + + var content = fs.readFileSync(findUp.sync('package.json', { + cwd: cwd + }), 'UTF8'); + + return JSON.parse(content).version; +}; diff --git a/test/.gitignore b/node-tests/.gitignore similarity index 100% rename from test/.gitignore rename to node-tests/.gitignore diff --git a/node-tests/caching-browserify-test.js b/node-tests/caching-browserify-test.js new file mode 100644 index 0000000..6ab20e9 --- /dev/null +++ b/node-tests/caching-browserify-test.js @@ -0,0 +1,199 @@ +/* global describe, afterEach, it, beforeEach */ +/* jshint expr: true */ + +var sinon = require('sinon'); +var chai = require('chai'); +var sinonChai = require('sinon-chai'); +var chaiFiles = require('chai-files'); + +chai.use(sinonChai); +chai.use(chaiFiles); + +var expect = chai.expect; + +var Loader = require('./helpers/loader'); + + +var file = chaiFiles.file; + +var CachingBrowserify = require('../lib/caching-browserify'); +var fs = require('fs'); +var path = require('path'); +var broccoli = require('broccoli'); +var quickTemp = require('quick-temp'); +var copy = require('copy-dereference').sync; + +describe('CachingBrowserify', function() { + var src, builder, readTrees, loader; + + beforeEach(function() { + src = {}; + loader = new Loader(); + + quickTemp.makeOrRemake(src, 'tmp'); + src.inputTree = src.tmp + '/inputTree'; + copy(__dirname + '/fixtures/modules', src.inputTree); + src.entryTree = src.inputTree + '/src'; + readTrees = {}; + + fs.readdirSync(src.inputTree + '/node_modules').forEach(function(module){ + var parentLink = path.resolve(__dirname + '/../node_modules/' + module); + var childLink = src.inputTree + '/node_modules/' + module; + + try { + fs.lstatSync(parentLink); + fs.unlinkSync(parentLink); + fs.symlinkSync(childLink, parentLink); + } catch(err) {} + }); + }); + + afterEach(function() { + loader.teardown(); + + quickTemp.remove(src, 'tmp'); + return builder.cleanup(); + }); + + function recordReadTrees(tree) { + readTrees[tree] = true; + } + + it('builds successfully', function() { + var tree = new CachingBrowserify(src.entryTree); + var spy = sinon.spy(tree, 'updateCache'); + + builder = new broccoli.Builder(tree); + + return builder.build().then(function(result){ + loader.load(result.directory + '/browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + expect(spy).to.have.callCount(1); + return builder.build(); + }).then(function(){ + expect(spy).to.have.callCount(1); + }); + }); + + it('builds successfully with non-default output path', function() { + var tree = new CachingBrowserify(src.entryTree, { outputFile: './special-browserify/browserify.js'}); + + builder = new broccoli.Builder(tree); + + return builder.build().then(function(result){ + loader.load(result.directory + '/special-browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + return builder.build(); + }); + }); + + it('builds successfully with sourcemaps on', function() { + var tree = new CachingBrowserify(src.entryTree, { enableSourcemap: true }); + var spy = sinon.spy(tree, 'updateCache'); + + builder = new broccoli.Builder(tree); + + return builder.build().then(function(result) { + loader.load(result.directory + '/browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + + expect(file(result.directory + '/browserify/browserify.js')).to.match(/sourceMappingURL=data:application\/json;.*base64,/); + expect(spy).to.have.callCount(1); + return builder.build(); + }).then(function() { + expect(spy).to.have.callCount(1); + }); + }); + + it('rebuilds when an npm module changes', function() { + var module = src.inputTree + '/node_modules/my-module'; + var target = module + '/index.js'; + + var tree = new CachingBrowserify(src.entryTree); + var spy = sinon.spy(tree, 'updateCache'); + + builder = new broccoli.Builder(tree); + + return builder.build(recordReadTrees).then(function(result) { + loader.load(result.directory + '/browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + + expect(spy).to.have.callCount(1); + + expect(loader.require('npm:my-module').default.toString()).to.contain('other.something();'); + + expect(Object.keys(readTrees).filter(function(readTree) { + return /my-module/.test(readTree); + }), 'expected readTrees to contain a path that matched `/node_modules\/my-module/`').to.not.be.empty; + + var code = fs.readFileSync(target, 'utf-8'); + + code = code.replace('other.something()', 'other.something()+1'); + fs.unlinkSync(target); + fs.writeFileSync(target, code); + + return builder.build(); + }).then(function(result) { + expect(spy).to.have.callCount(2); + + loader.reload(result.directory + '/browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + + expect(loader.require('npm:my-module').default.toString()).to.contain('other.something()+1;'); + }); + }); + + it('rebuilds when the entry file changes', function() { + var tree = new CachingBrowserify(src.entryTree); + var spy = sinon.spy(tree, 'updateCache'); + + builder = new broccoli.Builder(tree); + + return builder.build(recordReadTrees).then(function(result) { + loader.load(result.directory + '/browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + + expect(spy).to.have.callCount(1); + expect(readTrees[src.entryTree]).to.equal(true, 'should be watching stubs file'); + + fs.unlinkSync(src.entryTree + '/browserify_stubs.js'); + copy(src.entryTree + '/second_stubs.js', src.entryTree + '/browserify_stubs.js'); + + return builder.build(); + }).then(function(result) { + expect(spy).to.have.callCount(2); + + loader.load(result.directory + '/browserify/browserify.js'); + + expect(loader.entries).to.have.keys([ + 'npm:my-module' + ]); + }); + }); + + it('recovers from failed build', function() { + var broken = src.entryTree + '/broken_stubs.js'; + var normal = src.entryTree + '/browserify_stubs.js'; + var temporary = src.entryTree + '/temporary.js'; + + copy(normal, temporary); + fs.unlinkSync(normal); + copy(broken, normal); + + var tree = new CachingBrowserify(src.entryTree); + + builder = new broccoli.Builder(tree); + + return builder.build().then(function() { + throw new Error('expected not to get here'); + }, function(err) { + expect(err.message).to.match(/Cannot find module 'this-is-nonexistent'/); + fs.unlinkSync(normal); + copy(temporary, normal); + return builder.build(); + }).then(function(result) { + loader.load(result.directory + '/browserify/browserify.js'); + expect(loader.entries).to.have.keys(['npm:my-module']); + }); + }); +}); diff --git a/node-tests/fixtures/modules/node_modules/additional-thing/index.js b/node-tests/fixtures/modules/node_modules/additional-thing/index.js new file mode 100644 index 0000000..e69de29 diff --git a/node-tests/fixtures/modules/node_modules/additional-thing/package.json b/node-tests/fixtures/modules/node_modules/additional-thing/package.json new file mode 100644 index 0000000..62df650 --- /dev/null +++ b/node-tests/fixtures/modules/node_modules/additional-thing/package.json @@ -0,0 +1,4 @@ +{ + "name": "additional-thing", + "version": "2.2.0" +} diff --git a/test/fixtures/modules/node_modules/another/index.js b/node-tests/fixtures/modules/node_modules/another/index.js similarity index 100% rename from test/fixtures/modules/node_modules/another/index.js rename to node-tests/fixtures/modules/node_modules/another/index.js diff --git a/test/fixtures/modules/node_modules/another/package.json b/node-tests/fixtures/modules/node_modules/another/package.json similarity index 100% rename from test/fixtures/modules/node_modules/another/package.json rename to node-tests/fixtures/modules/node_modules/another/package.json diff --git a/node-tests/fixtures/modules/node_modules/broccoli/index.js b/node-tests/fixtures/modules/node_modules/broccoli/index.js new file mode 100644 index 0000000..e69de29 diff --git a/node-tests/fixtures/modules/node_modules/broccoli/package.json b/node-tests/fixtures/modules/node_modules/broccoli/package.json new file mode 100644 index 0000000..38d493e --- /dev/null +++ b/node-tests/fixtures/modules/node_modules/broccoli/package.json @@ -0,0 +1,4 @@ +{ + "name": "broccoli", + "version": "3.0.0" +} diff --git a/test/fixtures/modules/node_modules/my-module/index.js b/node-tests/fixtures/modules/node_modules/my-module/index.js similarity index 100% rename from test/fixtures/modules/node_modules/my-module/index.js rename to node-tests/fixtures/modules/node_modules/my-module/index.js diff --git a/test/fixtures/modules/node_modules/my-module/node_modules/other-dependency/index.js b/node-tests/fixtures/modules/node_modules/my-module/node_modules/other-dependency/index.js similarity index 100% rename from test/fixtures/modules/node_modules/my-module/node_modules/other-dependency/index.js rename to node-tests/fixtures/modules/node_modules/my-module/node_modules/other-dependency/index.js diff --git a/test/fixtures/modules/node_modules/my-module/node_modules/other-dependency/package.json b/node-tests/fixtures/modules/node_modules/my-module/node_modules/other-dependency/package.json similarity index 100% rename from test/fixtures/modules/node_modules/my-module/node_modules/other-dependency/package.json rename to node-tests/fixtures/modules/node_modules/my-module/node_modules/other-dependency/package.json diff --git a/test/fixtures/modules/node_modules/my-module/package.json b/node-tests/fixtures/modules/node_modules/my-module/package.json similarity index 100% rename from test/fixtures/modules/node_modules/my-module/package.json rename to node-tests/fixtures/modules/node_modules/my-module/package.json diff --git a/node-tests/fixtures/modules/node_modules/package.json b/node-tests/fixtures/modules/node_modules/package.json new file mode 100644 index 0000000..e69de29 diff --git a/node-tests/fixtures/modules/node_modules/something-new/index.js b/node-tests/fixtures/modules/node_modules/something-new/index.js new file mode 100644 index 0000000..e69de29 diff --git a/node-tests/fixtures/modules/node_modules/something-new/package.json b/node-tests/fixtures/modules/node_modules/something-new/package.json new file mode 100644 index 0000000..e7767df --- /dev/null +++ b/node-tests/fixtures/modules/node_modules/something-new/package.json @@ -0,0 +1,4 @@ +{ + "name": "something-new", + "version": "2.2.2" +} diff --git a/node-tests/fixtures/modules/node_modules/x/index.js b/node-tests/fixtures/modules/node_modules/x/index.js new file mode 100644 index 0000000..e69de29 diff --git a/node-tests/fixtures/modules/node_modules/x/package.json b/node-tests/fixtures/modules/node_modules/x/package.json new file mode 100644 index 0000000..c573c1d --- /dev/null +++ b/node-tests/fixtures/modules/node_modules/x/package.json @@ -0,0 +1,4 @@ +{ + "name": "x", + "version": "3.0.0" +} diff --git a/node-tests/fixtures/modules/node_modules/y/index.js b/node-tests/fixtures/modules/node_modules/y/index.js new file mode 100644 index 0000000..e69de29 diff --git a/node-tests/fixtures/modules/node_modules/y/package.json b/node-tests/fixtures/modules/node_modules/y/package.json new file mode 100644 index 0000000..677f431 --- /dev/null +++ b/node-tests/fixtures/modules/node_modules/y/package.json @@ -0,0 +1,4 @@ +{ + "name": "y", + "version": "3.0.0" +} diff --git a/test/fixtures/modules/src/broken_stubs.js b/node-tests/fixtures/modules/src/broken_stubs.js similarity index 100% rename from test/fixtures/modules/src/broken_stubs.js rename to node-tests/fixtures/modules/src/broken_stubs.js diff --git a/test/fixtures/modules/src/browserify_stubs.js b/node-tests/fixtures/modules/src/browserify_stubs.js similarity index 100% rename from test/fixtures/modules/src/browserify_stubs.js rename to node-tests/fixtures/modules/src/browserify_stubs.js diff --git a/test/fixtures/modules/src/second_stubs.js b/node-tests/fixtures/modules/src/second_stubs.js similarity index 100% rename from test/fixtures/modules/src/second_stubs.js rename to node-tests/fixtures/modules/src/second_stubs.js diff --git a/node-tests/fixtures/stubs/es5/inner/other.js b/node-tests/fixtures/stubs/es5/inner/other.js new file mode 100644 index 0000000..3cd9b3d --- /dev/null +++ b/node-tests/fixtures/stubs/es5/inner/other.js @@ -0,0 +1,3 @@ +define('inner/other', ['exports', 'npm:x', 'npm:y'], function(exports, _npmX, _npmY) { + +}); diff --git a/node-tests/fixtures/stubs/es5/sample.js b/node-tests/fixtures/stubs/es5/sample.js new file mode 100644 index 0000000..c1f0874 --- /dev/null +++ b/node-tests/fixtures/stubs/es5/sample.js @@ -0,0 +1,3 @@ +define('sample', ['exports', 'npm:broccoli'], function(exports, Broccoli) { + exports['default'] = Broccoli; +}); diff --git a/node-tests/helpers/keys.js b/node-tests/helpers/keys.js new file mode 100644 index 0000000..f23cae4 --- /dev/null +++ b/node-tests/helpers/keys.js @@ -0,0 +1,44 @@ +'use strict'; + +module.exports.FIRST = { + keys: [ + 'sample', + 'inner/other', + 'npm:broccoli@3.0.0', + 'npm:x@3.0.0', + 'npm:y@3.0.0', + ] +}; + +module.exports.SECOND = { + keys: [ + 'npm:broccoli@3.0.0', + 'npm:x@3.0.0', + 'npm:y@3.0.0', + 'npm:something-new@2.2.2', + ] +}; + +module.exports.THIRD = { + keys: [ + 'npm:x@3.0.0', + 'npm:y@3.0.0', + ] +}; + +module.exports.FOURTH = { + keys: [ + 'npm:additional-thing@2.2.0', + 'npm:broccoli@3.0.0', + 'npm:x@3.0.0', + 'npm:y@3.0.0', + ] +}; + +module.exports. FIFTH = { + keys: [ + 'npm:broccoli@3.0.0', + 'npm:y@3.0.0', + ] +}; + diff --git a/node-tests/helpers/loader.js b/node-tests/helpers/loader.js new file mode 100644 index 0000000..70af5c9 --- /dev/null +++ b/node-tests/helpers/loader.js @@ -0,0 +1,46 @@ +'use strict'; +var fs = require('fs'); + +module.exports = Loader; +function Loader() { + this.entries = {}; + this.setGlobalDefine(); +} + +Loader.prototype.load = function(path) { + require(path); +}; + +Loader.prototype.unload = function(_path) { + var path = fs.realpathSync(_path); + delete require('module')._cache[path]; + delete require.cache[path]; + delete require('module')._cache[_path]; + delete require.cache[_path]; + this.entries = {}; +}; + +Loader.prototype.reload = function(path) { + this.unload(path); + this.load(path); +}; + +Loader.prototype.require = function(name) { + if (!(name in this.entries)) { + throw new TypeError('no such module: `' + name + '`'); + } + + return this.entries[name](); +}; + +Loader.prototype.setGlobalDefine = function() { + global.define = function(name, callback) { + this.entries[name] = callback; + }.bind(this); +}; + +Loader.prototype.teardown = function() { + this.entries = undefined; + delete global.define; +}; + diff --git a/node-tests/imports-for-test.js b/node-tests/imports-for-test.js new file mode 100644 index 0000000..c02fed5 --- /dev/null +++ b/node-tests/imports-for-test.js @@ -0,0 +1,14 @@ +/* global describe, it */ + +var expect = require('chai').expect; +var importsFor = require('../lib/imports-for'); + +function toPojo(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +describe('importsFor', function() { + it('parses AMD (ES5)', function() { + expect(toPojo(importsFor('define("apple", ["npm:foo"], function() { });'))).to.eql({ foo: { "end": { "column": 26, "line": 1 }, "start": { "column": 17, "line": 1 } } }); + }); +}); diff --git a/node-tests/jshint.js b/node-tests/jshint.js new file mode 100644 index 0000000..7af4cde --- /dev/null +++ b/node-tests/jshint.js @@ -0,0 +1,9 @@ +var walkSync = require('walk-sync'); +var path = require('path'); + +require('mocha-jshint')({ + paths: ['lib'].concat(walkSync(__dirname, { ignore: ['fixtures', '.gitignore'] }). + map(function(relativePath) { + return path.join(__dirname, relativePath); + })) +}); diff --git a/node-tests/stub-generator-tests.js b/node-tests/stub-generator-tests.js new file mode 100644 index 0000000..b04f289 --- /dev/null +++ b/node-tests/stub-generator-tests.js @@ -0,0 +1,235 @@ +/* global describe, afterEach, it, beforeEach */ + +var chai = require('chai'); +var expect = chai.expect; // jshint ignore:line +var Loader = require('./helpers/loader'); + +var StubGenerator = require('../lib/stub-generator'); + +var fs = require('fs'); +var broccoli = require('broccoli'); +var quickTemp = require('quick-temp'); +var copy = require('copy-dereference').sync; +var walkSync = require('walk-sync'); + +var keys = require('./helpers/keys'); + +var FIRST = keys.FIRST; +var SECOND = keys.SECOND; +var THIRD = keys.THIRD; +var FOURTH = keys.FOURTH; +var FIFTH = keys.FIFTH; + +describe('Stub Generator', function() { + var src, loader; + + beforeEach(function() { + loader = new Loader(); + src = {}; + + quickTemp.makeOrRemake(src, 'tmp'); + src.inputTree = src.tmp + '/inputTree'; + copy(__dirname + '/fixtures/stubs/es5', src.inputTree); + }); + + afterEach(function() { + loader.teardown(); + + quickTemp.remove(src, 'tmp'); + }); + + describe('input', function() { + it('supports 1 inputTree', function() { + expect(function() { + new StubGenerator(); + }).to.throw(/Expects one inputTree/); + expect(function() { + new StubGenerator([]); + }).to.throw(/Expects one inputTree/); + expect(function() { + new StubGenerator(['a','b']); + }).to.throw(/Expects one inputTree/); + }); + }); + + describe('building', function() { + var builder; + beforeEach(function() { + var tree = new StubGenerator(src.inputTree, { + basedir: __dirname + '/fixtures/modules' + }); + + builder = new broccoli.Builder(tree); + }); + + afterEach(function() { + return builder.cleanup(); + }); + + it('generates stub file', function() { + return builder.build().then(function(result) { + loader.load(result.directory + '/browserify_stubs.js'); + loader.load(result.directory + '/sample.js'); + loader.load(result.directory + '/inner/other.js'); + + expect(loader.entries).to.have.keys(FIRST.keys); + + var broc = loader.require('npm:broccoli@3.0.0'); + + expect(broc).to.have.keys(['default']); + expect(broc.default).to.have.keys([ + 'loadBrocfile', + 'server', + 'getMiddleware', + 'Watcher', + 'cli', + 'makeTree', + 'bowerTrees', + 'MergedTree', + 'Builder' + ]); + }); + }); + + it('generates same stubFile if inputs do not change', function() { + var firstRun; + return builder.build().then(function(result) { + firstRun = fs.statSync(result.directory + '/browserify_stubs.js'); + return builder.build(); + }).then(function(result) { + var nextRun = fs.statSync(result.directory + '/browserify_stubs.js'); + + expect(firstRun, 'stat information should remain the same').to.deep.equal(nextRun); + }); + }); + + it('adds deps from new file', function() { + return builder.build().then(function(result) { + loader.load(result.directory + '/browserify_stubs.js'); + loader.load(result.directory + '/sample.js'); + loader.load(result.directory + '/inner/other.js'); + + expect(walkSync(result.directory)).to.eql([ + 'browserify_stubs.js', + 'inner/', + 'inner/other.js', + 'sample.js', + ]); + + expect(loader.entries).to.have.keys(FIRST.keys); + + var broc = loader.require('npm:broccoli@3.0.0'); + + expect(broc).to.have.keys(['default']); + expect(broc.default).to.have.keys([ + 'loadBrocfile', + 'server', + 'getMiddleware', + 'Watcher', + 'cli', + 'makeTree', + 'bowerTrees', + 'MergedTree', + 'Builder' + ]); + + fs.writeFileSync(src.inputTree + '/new.js', "define(\"fizz\", [\"exports\", \"npm:something-new\"], function(exports, SomethingNew) {});"); + return builder.build(); + }).then(function(result) { + expect(walkSync(result.directory)).to.eql([ + 'browserify_stubs.js', + 'inner/', + 'inner/other.js', + 'new.js', + 'sample.js', + ]); + + loader.reload(result.directory + '/browserify_stubs.js'); + + expect(loader.entries).to.have.keys(SECOND.keys); + }); + }); + + it('removes deps from deleted file', function() { + return builder.build().then(function(result) { + loader.load(result.directory + '/browserify_stubs.js'); + loader.load(result.directory + '/sample.js'); + loader.load(result.directory + '/inner/other.js'); + + expect(loader.entries).to.have.keys(FIRST.keys); + fs.unlinkSync(src.inputTree + '/sample.js'); + return builder.build(); + }).then(function(result) { + loader.reload(result.directory + '/browserify_stubs.js'); + + expect(loader.entries).to.have.keys(THIRD.keys); + }); + }); + + it('adds deps in modified file', function() { + return builder.build().then(function(result) { + loader.load(result.directory + '/browserify_stubs.js'); + loader.load(result.directory + '/sample.js'); + loader.load(result.directory + '/inner/other.js'); + + expect(walkSync(result.directory)).to.eql([ + 'browserify_stubs.js', + 'inner/', + 'inner/other.js', + 'sample.js', + ]); + + expect(loader.entries).to.have.keys(FIRST.keys); + + var was = "define('foo', ['exports', 'npm:broccoli', 'npm:additional-thing'], function(exports, Broccoli, Additional) { exports['default'] = Broccoli;});"; + + fs.writeFileSync(src.inputTree + '/sample.js', was); + return builder.build(); + }).then(function(result) { + expect(walkSync(result.directory)).to.eql([ + 'browserify_stubs.js', + 'inner/', + 'inner/other.js', + 'sample.js', + ]); + + loader.reload(result.directory + '/browserify_stubs.js'); + + expect(loader.entries).to.have.keys(FOURTH.keys); + }); + }); + + it('removes deps in modified file', function() { + return builder.build().then(function(result) { + loader.load(result.directory + '/browserify_stubs.js'); + loader.load(result.directory + '/sample.js'); + loader.load(result.directory + '/inner/other.js'); + + expect(loader.entries).to.have.keys(FIRST.keys); + + expect(walkSync(result.directory)).to.eql([ + 'browserify_stubs.js', + 'inner/', + 'inner/other.js', + 'sample.js', + ]); + + var was = "define('foo', ['exports', 'npm:y'], function(exports, _npmY) {});"; + + fs.writeFileSync(src.inputTree + '/inner/other.js', was); + return builder.build(); + }).then(function(result) { + loader.reload(result.directory + '/browserify_stubs.js'); + + expect(walkSync(result.directory)).to.eql([ + 'browserify_stubs.js', + 'inner/', + 'inner/other.js', + 'sample.js', + ]); + + expect(loader.entries).to.have.keys(FIFTH.keys); + }); + }); + }); +}); diff --git a/test/stubs-test.js b/node-tests/stubs-test.js similarity index 52% rename from test/stubs-test.js rename to node-tests/stubs-test.js index 4d69474..6750437 100644 --- a/test/stubs-test.js +++ b/node-tests/stubs-test.js @@ -1,3 +1,5 @@ +/* global describe, it, beforeEach */ + var Stubs = require('../lib/stubs'); var chai = require('chai'); var expect = chai.expect; // jshint ignore:line @@ -12,17 +14,17 @@ describe('Stubs', function() { describe('no-data', function() { it('toAMD', function() { expect(stubs.toAMD()).to.eql(''); - }) + }); }); describe('basic', function() { describe('es6', function() { beforeEach(function() { - stubs.set('foo/bar', 'import asdf from "npm:asdf"'); + stubs.set('foo/bar', { asdf: { version: '1.0.0' }}); }); it('toAMD', function() { - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})"); + expect(stubs.toAMD()).to.eql("define('npm:asdf@1.0.0', function(){ return { 'default': require('asdf')};})"); }); it('delete', function() { @@ -31,8 +33,8 @@ describe('Stubs', function() { }); it('set', function() { - stubs.set('foo', 'import asdf from "npm:asdf"'); - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})"); + stubs.set('foo', { asdf: { version: '42' }}); + expect(stubs.toAMD()).to.eql("define('npm:asdf@42', function(){ return { 'default': require('asdf')};})"); }); describe('cache busting', function() { @@ -48,24 +50,24 @@ describe('Stubs', function() { it('delete then add back', function() { stubs.delete('foo/bar'); expect(stubs.toAMD()).to.eql(""); - stubs.set('foo', 'import asdf from "npm:asdf"'); - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})"); + stubs.set('foo', { asdf: { version: 1 }}); + expect(stubs.toAMD()).to.eql("define('npm:asdf@1', function(){ return { 'default': require('asdf')};})"); }); it('set', function() { - stubs.set('apple', 'import apple from "npm:foo"'); - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})\ndefine('npm:foo', function(){ return { 'default': require('foo')};})"); + stubs.set('apple', { asdf: { version: 'OMG' }, foo: { version: '22' }}); + expect(stubs.toAMD()).to.eql("define('npm:asdf@OMG', function(){ return { 'default': require('asdf')};})\ndefine('npm:foo@22', function(){ return { 'default': require('foo')};})"); }); }); }); describe('es5', function() { beforeEach(function() { - stubs.set('foo/bar', 'define("asdf", ["npm:asdf"], function() { });'); + stubs.set('foo/bar', { asdf: { version: '3.0.0' }}); }); it('toAMD', function() { - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})"); + expect(stubs.toAMD()).to.eql("define('npm:asdf@3.0.0', function(){ return { 'default': require('asdf')};})"); }); it('delete', function() { @@ -74,8 +76,8 @@ describe('Stubs', function() { }); it('set', function() { - stubs.set('foo', 'define("asdf", ["npm:asdf"], function() { });'); - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})"); + stubs.set('foo', { asdf: { version: '3.0.0' }}); + expect(stubs.toAMD()).to.eql("define('npm:asdf@3.0.0', function(){ return { 'default': require('asdf')};})"); }); describe('cache busting', function() { @@ -91,13 +93,13 @@ describe('Stubs', function() { it('delete then add back', function() { stubs.delete('foo/bar'); expect(stubs.toAMD()).to.eql(""); - stubs.set('foo', 'define("asdf", ["npm:asdf"], function() { });'); - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})"); + stubs.set('foo', { asdf: { version: '1'}}); + expect(stubs.toAMD()).to.eql("define('npm:asdf@1', function(){ return { 'default': require('asdf')};})"); }); it('set', function() { - stubs.set('apple', 'define("apple", ["npm:foo"], function() { });'); - expect(stubs.toAMD()).to.eql("define('npm:asdf', function(){ return { 'default': require('asdf')};})\ndefine('npm:foo', function(){ return { 'default': require('foo')};})"); + stubs.set('apple', { foo: { version: 16 }}); + expect(stubs.toAMD()).to.eql("define('npm:asdf@3.0.0', function(){ return { 'default': require('asdf')};})\ndefine('npm:foo@16', function(){ return { 'default': require('foo')};})"); }); }); }); diff --git a/package.json b/package.json index 052ca74..6407338 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,18 @@ "name": "ember-browserify", "description": "ember-cli addon for easily loading CommonJS modules from npm via browserify.", "version": "1.1.13", - "main": "index.js", + "main": "lib/index.js", "directories": { "doc": "doc", "test": "tests" }, "scripts": { - "test": "mocha --inline-diffs", - "test:debug": "mocha debug --inline-diffs" + "build": "ember build", + "start": "ember server", + "test": "npm run node-test && ember try:each", + "node-test": "mocha node-tests --inline-diffs", + "node-test:debug": "mocha debug node-tests --inline-diffs", + "prepublish": "./bin/install-ember-addons.js" }, "repository": "https://github.com/ef4/ember-browserify", "engines": { @@ -22,16 +26,39 @@ "license": "MIT", "devDependencies": { "broccoli": "^0.16.8", - "chai": "^1.10.0", + "broccoli-asset-rev": "^2.4.2", + "chai": "^3.5.0", + "chai-files": "^1.4.0", "copy-dereference": "^1.0.0", + "ember-ajax": "^2.0.1", + "ember-cli": "2.8.0", + "ember-cli-app-version": "^1.0.0", + "ember-cli-dependency-checker": "^1.2.0", + "ember-cli-htmlbars": "^1.0.3", + "ember-cli-htmlbars-inline-precompile": "^0.3.1", + "ember-cli-inject-live-reload": "^1.4.0", + "ember-cli-jshint": "^1.0.0", + "ember-cli-qunit": "^2.1.0", + "ember-cli-release": "^0.2.9", + "ember-cli-sri": "^2.1.0", + "ember-cli-test-loader": "^1.1.0", + "ember-cli-uglify": "^1.2.0", + "ember-data": "^2.8.0", + "ember-disable-prototype-extensions": "^1.1.0", + "ember-export-application-global": "^1.0.5", + "ember-load-initializers": "^0.5.1", + "ember-resolver": "^2.0.3", + "loader.js": "^4.0.1", "mocha": "^2.0.1", - "mocha-jshint": "0.0.9", + "mocha-jshint": "2.3.1", "sinon": "^1.12.2", - "sinon-chai": "^2.6.0" + "sinon-chai": "^2.6.0", + "symlink-or-copy": "^1.1.6" }, "dependencies": { - "acorn": "^2.6.4", + "acorn": "^4.0.0", "broccoli-caching-writer": "^3.0.3", + "broccoli-funnel": "^1.0.6", "broccoli-kitchen-sink-helpers": "^0.3.1", "broccoli-merge-trees": "^1.1.2", "broccoli-plugin": "^1.2.1", @@ -39,18 +66,28 @@ "core-object": "^1.1.0", "debug": "^2.2.0", "derequire": "^2.0.3", + "ember-cli-babel": "^5.1.6", "ember-cli-version-checker": "^1.1.4", + "find-up": "^1.1.2", "fs-tree": "^1.0.0", "fs-tree-diff": "^0.5.0", - "lodash": "^4.5.1", + "lodash.merge": "^4.6.0", "md5-hex": "^1.3.0", - "mkdirp": "^0.5.0", + "mkdirp": "^0.5.1", "promise-map-series": "^0.2.0", "quick-temp": "^0.1.2", + "resolve": "^1.1.7", "rimraf": "^2.2.8", "rsvp": "^3.0.14", "symlink-or-copy": "^1.0.0", "through2": "^2.0.0", - "walk-sync": "^0.2.7" + "walk-sync": "^0.3.1" + }, + "ember-addon": { + "configPath": "tests/dummy/config", + "paths": [ + "node_modules/modern", + "node_modules/outdated" + ] } } diff --git a/test/fixtures/stubs/es5/inner/other.js b/test/fixtures/stubs/es5/inner/other.js deleted file mode 100644 index 6bd002e..0000000 --- a/test/fixtures/stubs/es5/inner/other.js +++ /dev/null @@ -1,3 +0,0 @@ -define('foo', ['exports', 'npm:x', 'npm:y'], function(exports, _npmX, _npmY) { - -}); diff --git a/test/fixtures/stubs/es5/sample.js b/test/fixtures/stubs/es5/sample.js deleted file mode 100644 index 2b94680..0000000 --- a/test/fixtures/stubs/es5/sample.js +++ /dev/null @@ -1,3 +0,0 @@ -define('foo', ['exports', 'npm:broccoli'], function(exports, Broccoli) { - exports['default'] = Broccoli; -}); diff --git a/test/fixtures/stubs/es6/inner/other.js b/test/fixtures/stubs/es6/inner/other.js deleted file mode 100644 index 8cb4e3b..0000000 --- a/test/fixtures/stubs/es6/inner/other.js +++ /dev/null @@ -1,2 +0,0 @@ -import X from "npm:x"; -import "npm:y"; \ No newline at end of file diff --git a/test/fixtures/stubs/es6/sample.js b/test/fixtures/stubs/es6/sample.js deleted file mode 100644 index 3ab7d72..0000000 --- a/test/fixtures/stubs/es6/sample.js +++ /dev/null @@ -1,2 +0,0 @@ -import Broccoli from "npm:broccoli"; -export default Broccoli; \ No newline at end of file diff --git a/test/jshint.js b/test/jshint.js deleted file mode 100644 index 26f4b22..0000000 --- a/test/jshint.js +++ /dev/null @@ -1,3 +0,0 @@ -require('mocha-jshint')([ - 'lib' -]); diff --git a/test/test.js b/test/test.js deleted file mode 100644 index bd40f25..0000000 --- a/test/test.js +++ /dev/null @@ -1,542 +0,0 @@ -/* global describe, afterEach, it, expect, beforeEach */ - -var chai = require('chai'); -var expect = chai.expect; // jshint ignore:line -var sinon = require('sinon'); -var sinonChai = require("sinon-chai"); -chai.use(sinonChai); - -var StubGenerator = require('../lib/stub-generator'); -var CachingBrowserify = require('../lib/caching-browserify'); -var RSVP = require('rsvp'); -RSVP.on('error', function(err){throw err;}); -var fs = require('fs'); -var path = require('path'); -var broccoli = require('broccoli'); -var quickTemp = require('quick-temp'); -var copy = require('copy-dereference').sync; - -var FIRST = { - keys: [ - 'npm:broccoli', - 'npm:x', - 'npm:y', - ] -}; - -var SECOND = { - keys: [ - 'npm:broccoli', - 'npm:x', - 'npm:y', - 'npm:something-new', - ] -}; - -var THIRD = { - keys: [ - 'npm:x', - 'npm:y', - ] -}; - -var FOURTH = { - keys: [ - 'npm:additional-thing', - 'npm:broccoli', - 'npm:x', - 'npm:y', - ] -}; - -var FIFTH = { - keys: [ - 'npm:broccoli', - 'npm:y', - ] -}; - -function Loader() { - this.entries = {}; - this.setGlobalDefine(); -} - -Loader.prototype.load = function(path) { - require(path); -}; - -Loader.prototype.unload = function(_path) { - var path = fs.realpathSync(_path); - delete require('module')._cache[path]; - delete require.cache[path]; - delete require('module')._cache[_path]; - delete require.cache[_path]; - this.entries = {}; -}; - -Loader.prototype.reload = function(path) { - this.unload(path); - this.load(path); -}; - -Loader.prototype.require = function(name) { - if (!(name in this.entries)) { - throw new TypeError('no such module: `' + name + '`'); - } - - return this.entries[name](); -}; - -Loader.prototype.setGlobalDefine = function() { - global.define = function(name, callback) { - this.entries[name] = callback; - }.bind(this); -}; - -Loader.prototype.teardown = function() { - this.entries = undefined; - delete global.define; -}; - -describe('Ember CLI 2.x Stub Generator', function() { - var src = {}; - var builder; - var loader; - - beforeEach(function() { - loader = new Loader(); - - quickTemp.makeOrRemake(src, 'tmp'); - src.inputTree = src.tmp + '/inputTree'; - copy(__dirname + '/fixtures/stubs/es5', src.inputTree); - }); - - afterEach(function() { - loader.teardown(); - - if (src.tmp) { - quickTemp.remove(src, 'tmp'); - } - - if (builder) { - return builder.cleanup(); - } - }); - - describe('input', function() { - it('only supports 1 inputTree', function() { - expect(function() { - new StubGenerator(); - }).to.throw(/Expects one inputTree/); - expect(function() { - new StubGenerator([]); - }).to.throw(/Expects one inputTree/); - expect(function() { - new StubGenerator(['a','b']); - }).to.throw(/Expects one inputTree/); - }); - }); - - it('generates stub file', function() { - var tree = new StubGenerator(src.inputTree); - - builder = new broccoli.Builder(tree); - - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIRST.keys); - - var broc = loader.require('npm:broccoli'); - - expect(broc).to.have.keys(['default']); - expect(broc.default).to.have.keys([ - 'loadBrocfile', - 'server', - 'getMiddleware', - 'Watcher', - 'cli', - 'makeTree', - 'bowerTrees', - 'MergedTree', - 'Builder' - ]); - }); - }); - - it('generates same stubFile if inputs do not change', function() { - var tree = new StubGenerator(src.inputTree); - - builder = new broccoli.Builder(tree); - - var firstRun; - return builder.build().then(function(result) { - firstRun = fs.statSync(result.directory + '/browserify_stubs.js'); - return builder.build(); - }).then(function(result) { - nextRun = fs.statSync(result.directory + '/browserify_stubs.js'); - - expect(firstRun, 'stat information should remain the same').to.deep.equal(nextRun); - }); - }); - - it('adds deps from new file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIRST.keys); - - var broc = loader.require('npm:broccoli'); - - expect(broc).to.have.keys(['default']); - expect(broc.default).to.have.keys([ - 'loadBrocfile', - 'server', - 'getMiddleware', - 'Watcher', - 'cli', - 'makeTree', - 'bowerTrees', - 'MergedTree', - 'Builder' - ]); - - fs.writeFileSync(src.inputTree + '/new.js', "define(\"fizz\", [\"exports\", \"npm:something-new\"], function(exports, SomethingNew) {});"); - return builder.build(); - }).then(function(result){ - - loader.reload(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(SECOND.keys); - }); - }); - - it('removes deps from deleted file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIRST.keys); - fs.unlinkSync(src.inputTree + '/sample.js'); - return builder.build(); - }).then(function(result){ - - loader.reload(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(THIRD.keys); - }); - }); - - it('adds deps in modified file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIRST.keys); - - var was = "define('foo', ['exports', 'npm:broccoli', 'npm:additional-thing'], function(exports, Broccoli, Additional) { exports['default'] = Broccoli;});"; - - fs.writeFileSync(src.inputTree + '/sample.js', was); - return builder.build(); - }).then(function(result){ - - loader.reload(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FOURTH.keys); - }); - }); - - it('removes deps in modified file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIRST.keys); - - var was = "define('foo', ['exports', 'npm:y'], function(exports, _npmY) {});"; - - fs.writeFileSync(src.inputTree + '/inner/other.js', was); - return builder.build(); - }).then(function(result){ - loader.reload(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIFTH.keys); - }); - }); -}); - -describe('Ember CLI 1.x Stub Generator', function() { - var src = {}; - var builder; - - var loader; - - beforeEach(function() { - loader = new Loader(); - quickTemp.makeOrRemake(src, 'tmp'); - src.inputTree = src.tmp + '/inputTree'; - copy(__dirname + '/fixtures/stubs/es6', src.inputTree); - }); - - afterEach(function() { - loader.teardown(); - - if (src.tmp) { - quickTemp.remove(src, 'tmp'); - } - if (builder) { - return builder.cleanup(); - } - }); - - it('generates stub file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - - expect(loader.entries).to.have.keys(FIRST.keys); - }); - }); - - it('adds deps from new file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys(FIRST.keys); - fs.writeFileSync(src.inputTree + '/new.js', "import SomethingNew from \"npm:something-new\"\n"); - return builder.build(); - }).then(function(result){ - loader.reload(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys([ - 'npm:broccoli', - 'npm:x', - 'npm:y', - 'npm:something-new' - ]); - }); - }); - - it('removes deps from deleted file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys(FIRST.keys); - fs.unlinkSync(src.inputTree + '/sample.js'); - return builder.build(); - }).then(function(result){ - loader.reload(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys([ - 'npm:x', - 'npm:y' - ]); - }); - }); - - it('adds deps in modified file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys(FIRST.keys); - var was = fs.readFileSync(src.inputTree + '/sample.js'); - was += "\nimport Additional from 'npm:additional-thing';"; - fs.writeFileSync(src.inputTree + '/sample.js', was); - return builder.build(); - }).then(function(result){ - loader.reload(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys(FOURTH.keys); - }); - }); - - it('removes deps in modified file', function() { - var tree = new StubGenerator(src.inputTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result) { - loader.load(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys(FIRST.keys); - var was = fs.readFileSync(src.inputTree + '/inner/other.js', 'utf-8'); - was = was.split("\n").slice(1).join("\n"); - - fs.writeFileSync(src.inputTree + '/inner/other.js', was); - return builder.build(); - }).then(function(result){ - loader.reload(result.directory + '/browserify_stubs.js'); - expect(loader.entries).to.have.keys(FIFTH.keys); - }); - }); -}); - - -describe('CachingBrowserify', function() { - var src = {}; - var builder; - var readTrees; - var loader; - - beforeEach(function() { - loader = new Loader(); - - quickTemp.makeOrRemake(src, 'tmp'); - src.inputTree = src.tmp + '/inputTree'; - copy(__dirname + '/fixtures/modules', src.inputTree); - src.entryTree = src.inputTree + '/src'; - readTrees = {}; - fs.readdirSync(src.inputTree + '/node_modules').forEach(function(module){ - var parentLink = path.resolve(__dirname + '/../node_modules/' + module); - var childLink = src.inputTree + '/node_modules/' + module; - try { - fs.lstatSync(parentLink); - fs.unlinkSync(parentLink); - } catch(err) {} - fs.symlinkSync(childLink, parentLink); - }); - - }); - - afterEach(function() { - loader.teardown(); - - if (src.tmp) { - quickTemp.remove(src, 'tmp'); - } - if (builder) { - return builder.cleanup(); - } - }); - - function recordReadTrees(tree) { - readTrees[tree] = true; - } - - it('builds successfully', function() { - var tree = new CachingBrowserify(src.entryTree); - var spy = sinon.spy(tree, 'updateCache'); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result){ - loader.load(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - expect(spy).to.have.callCount(1); - return builder.build(); - }).then(function(){ - expect(spy).to.have.callCount(1); - }); - }); - - it('builds successfully with non-default output path', function() { - var tree = new CachingBrowserify(src.entryTree, { outputFile: './special-browserify/browserify.js'}); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result){ - loader.load(result.directory + '/special-browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - return builder.build(); - }); - }); - - it('builds successfully with sourcemaps on', function() { - var tree = new CachingBrowserify(src.entryTree, {enableSourcemap: true}); - var spy = sinon.spy(tree, 'updateCache'); - builder = new broccoli.Builder(tree); - return builder.build().then(function(result){ - loader.load(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - - var file = fs.readFileSync(result.directory + '/browserify/browserify.js', 'UTF8'); - expect(file).to.match(/sourceMappingURL=data:application\/json;.*base64,/); - expect(spy).to.have.callCount(1); - return builder.build(); - }).then(function(){ - expect(spy).to.have.callCount(1); - }); - }); - - it('rebuilds when an npm module changes', function(){ - var tree = new CachingBrowserify(src.entryTree); - var spy = sinon.spy(tree, 'updateCache'); - - builder = new broccoli.Builder(tree); - return builder.build(recordReadTrees).then(function(result){ - loader.load(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - - expect(spy).to.have.callCount(1); - - expect(loader.require('npm:my-module').default.toString()).to.contain('other.something();'); - - var module = path.resolve(__dirname + '/../node_modules/my-module'); - var target = module + '/index.js'; - - expect(Object.keys(readTrees).filter(function(readTree) { - return /my-module/.test(readTree); - }), 'expected readTrees to contain a path that matched `/node_modules\/my-module/`').to.not.be.empty; - - var code = fs.readFileSync(target, 'utf-8'); - code = code.replace('other.something()', 'other.something()+1'); - fs.writeFileSync(target, code); - - return builder.build(); - }).then(function(result){ - expect(spy).to.have.callCount(2); - - loader.reload(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - - expect(loader.require('npm:my-module').default.toString()).to.contain('other.something()+1;'); - }); - }); - - it('rebuilds when the entry file changes', function(){ - var tree = new CachingBrowserify(src.entryTree); - var spy = sinon.spy(tree, 'updateCache'); - - builder = new broccoli.Builder(tree); - return builder.build(recordReadTrees).then(function(result){ - loader.load(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - - expect(spy).to.have.callCount(1); - expect(readTrees[src.entryTree]).to.equal(true, 'should be watching stubs file'); - fs.unlinkSync(src.entryTree + '/browserify_stubs.js'); - copy(src.entryTree + '/second_stubs.js', src.entryTree + '/browserify_stubs.js'); - return builder.build(); - }).then(function(result){ - expect(spy).to.have.callCount(2); - - loader.load(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys([ - 'npm:my-module' - ]); - }); - }); - - it('recovers from failed build', function(){ - var broken = src.entryTree + '/broken_stubs.js'; - var normal = src.entryTree + '/browserify_stubs.js'; - var temporary = src.entryTree + '/temporary.js'; - - copy(normal, temporary); - fs.unlinkSync(normal); - copy(broken, normal); - - var tree = new CachingBrowserify(src.entryTree); - builder = new broccoli.Builder(tree); - return builder.build().then(function(){ - throw new Error('expected not to get here'); - }, function(err){ - expect(err.message).to.match(/Cannot find module 'this-is-nonexistent'/); - fs.unlinkSync(normal); - copy(temporary, normal); - return builder.build(); - }).then(function(result){ - loader.load(result.directory + '/browserify/browserify.js'); - expect(loader.entries).to.have.keys(['npm:my-module']); - }); - }); -}); diff --git a/testem.js b/testem.js new file mode 100644 index 0000000..26044b2 --- /dev/null +++ b/testem.js @@ -0,0 +1,13 @@ +/*jshint node:true*/ +module.exports = { + "framework": "qunit", + "test_page": "tests/index.html?hidepassed", + "disable_watching": true, + "launch_in_ci": [ + "PhantomJS" + ], + "launch_in_dev": [ + "PhantomJS", + "Chrome" + ] +}; diff --git a/tests/.jshintrc b/tests/.jshintrc new file mode 100644 index 0000000..d2bd113 --- /dev/null +++ b/tests/.jshintrc @@ -0,0 +1,52 @@ +{ + "predef": [ + "document", + "window", + "location", + "setTimeout", + "$", + "-Promise", + "define", + "console", + "visit", + "exists", + "fillIn", + "click", + "keyEvent", + "triggerEvent", + "find", + "findWithAssert", + "wait", + "DS", + "andThen", + "currentURL", + "currentPath", + "currentRouteName" + ], + "node": false, + "browser": false, + "boss": true, + "curly": true, + "debug": false, + "devel": false, + "eqeqeq": true, + "evil": true, + "forin": false, + "immed": false, + "laxbreak": false, + "newcap": true, + "noarg": true, + "noempty": false, + "nonew": false, + "nomen": false, + "onevar": false, + "plusplus": false, + "regexp": false, + "undef": true, + "sub": true, + "strict": false, + "white": false, + "eqnull": true, + "esversion": 6, + "unused": true +} diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js new file mode 100644 index 0000000..831ad61 --- /dev/null +++ b/tests/dummy/app/app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +let App; + +Ember.MODEL_FACTORY_INJECTIONS = true; + +App = Ember.Application.extend({ + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/tests/dummy/app/components/.gitkeep b/tests/dummy/app/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/app/controllers/.gitkeep b/tests/dummy/app/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/app/helpers/.gitkeep b/tests/dummy/app/helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/app/index.html b/tests/dummy/app/index.html new file mode 100644 index 0000000..5120bd7 --- /dev/null +++ b/tests/dummy/app/index.html @@ -0,0 +1,25 @@ + + + + + + Dummy + + + + {{content-for "head"}} + + + + + {{content-for "head-footer"}} + + + {{content-for "body"}} + + + + + {{content-for "body-footer"}} + + diff --git a/tests/dummy/app/models/.gitkeep b/tests/dummy/app/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/app/resolver.js b/tests/dummy/app/resolver.js new file mode 100644 index 0000000..2fb563d --- /dev/null +++ b/tests/dummy/app/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js new file mode 100644 index 0000000..cdc2578 --- /dev/null +++ b/tests/dummy/app/router.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; +import config from './config/environment'; + +const Router = Ember.Router.extend({ + location: config.locationType, + rootURL: config.rootURL +}); + +Router.map(function() { +}); + +export default Router; diff --git a/tests/dummy/app/routes/.gitkeep b/tests/dummy/app/routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/app/routes/application.js b/tests/dummy/app/routes/application.js new file mode 100644 index 0000000..8a54b75 --- /dev/null +++ b/tests/dummy/app/routes/application.js @@ -0,0 +1,29 @@ +import Ember from 'ember'; + +// Should be version 2030.0.0 +import flooring from 'npm:flooring'; + +// Should be 2030.0.0, which the host app includes +import modernApp from '../utils/floor-modern'; // source: tests/dummy/lib/modern/app/utils/floor-modern +import outdatedApp from '../utils/floor-outdated'; // source: tests/dummy/lib/outdated/app/utils/floor-outdated + +// Should be version 1970.0.0 +import outdated from 'outdated/utils/floor-type'; +import outdatedReexports from '../utils/reexports-outdated'; // source: tests/dummy/lib/outdated/app/utils/reexports-outdated + +// Should be version 2010.0.0 +import modern from 'modern/utils/floor-type'; +import modernReexports from '../utils/reexports-modern'; // source: tests/dummy/lib/modern/app/utils/reexports-modern + +export default Ember.Route.extend({ + flooring, + + modernApp, + outdatedApp, + + outdated, + outdatedReexports, + + modern, + modernReexports +}); diff --git a/tests/dummy/app/styles/app.css b/tests/dummy/app/styles/app.css new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs new file mode 100644 index 0000000..c24cd68 --- /dev/null +++ b/tests/dummy/app/templates/application.hbs @@ -0,0 +1 @@ +{{outlet}} diff --git a/tests/dummy/app/templates/components/.gitkeep b/tests/dummy/app/templates/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js new file mode 100644 index 0000000..2529939 --- /dev/null +++ b/tests/dummy/config/environment.js @@ -0,0 +1,46 @@ +/* jshint node: true */ + +module.exports = function(environment) { + var ENV = { + modulePrefix: 'dummy', + environment: environment, + rootURL: '/', + locationType: 'auto', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. 'with-controller': true + } + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + } + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + } + + if (environment === 'production') { + + } + + return ENV; +}; diff --git a/tests/dummy/lib/.jshintrc b/tests/dummy/lib/.jshintrc new file mode 100644 index 0000000..839c191 --- /dev/null +++ b/tests/dummy/lib/.jshintrc @@ -0,0 +1,4 @@ +{ + "node": true, + "browser": false +} diff --git a/tests/dummy/lib/flooring/index.js b/tests/dummy/lib/flooring/index.js new file mode 100644 index 0000000..e5de798 --- /dev/null +++ b/tests/dummy/lib/flooring/index.js @@ -0,0 +1 @@ +module.exports = function() { return 'space cloud'; }; diff --git a/tests/dummy/lib/flooring/package.json b/tests/dummy/lib/flooring/package.json new file mode 100644 index 0000000..6b9fd63 --- /dev/null +++ b/tests/dummy/lib/flooring/package.json @@ -0,0 +1,5 @@ +{ + "name": "flooring", + "version": "2030.0.0", + "main": "index.js" +} diff --git a/tests/dummy/lib/modern/addon/utils/floor-type.js b/tests/dummy/lib/modern/addon/utils/floor-type.js new file mode 100644 index 0000000..5327a34 --- /dev/null +++ b/tests/dummy/lib/modern/addon/utils/floor-type.js @@ -0,0 +1,5 @@ +import flooring from 'npm:flooring'; + +export default function() { + return flooring(); +} diff --git a/tests/dummy/lib/modern/app/utils/floor-modern.js b/tests/dummy/lib/modern/app/utils/floor-modern.js new file mode 100644 index 0000000..5327a34 --- /dev/null +++ b/tests/dummy/lib/modern/app/utils/floor-modern.js @@ -0,0 +1,5 @@ +import flooring from 'npm:flooring'; + +export default function() { + return flooring(); +} diff --git a/tests/dummy/lib/modern/app/utils/reexports-modern.js b/tests/dummy/lib/modern/app/utils/reexports-modern.js new file mode 100644 index 0000000..29e3011 --- /dev/null +++ b/tests/dummy/lib/modern/app/utils/reexports-modern.js @@ -0,0 +1 @@ +export { default } from 'modern/utils/floor-type'; diff --git a/tests/dummy/lib/modern/index.js b/tests/dummy/lib/modern/index.js new file mode 100644 index 0000000..f36adc5 --- /dev/null +++ b/tests/dummy/lib/modern/index.js @@ -0,0 +1,8 @@ +/*jshint node:true*/ +module.exports = { + name: 'modern', + + isDevelopingAddon: function() { + return true; + } +}; diff --git a/tests/dummy/lib/modern/node_modules/ember-browserify/package.json b/tests/dummy/lib/modern/node_modules/ember-browserify/package.json new file mode 100644 index 0000000..bdd7fd3 --- /dev/null +++ b/tests/dummy/lib/modern/node_modules/ember-browserify/package.json @@ -0,0 +1,8 @@ +{ + "name": "ember-browserify", + "version": "*", + "main": "lib/index.js", + "keywords": [ + "ember-addon" + ] +} diff --git a/tests/dummy/lib/modern/node_modules/flooring/index.js b/tests/dummy/lib/modern/node_modules/flooring/index.js new file mode 100644 index 0000000..af9b26e --- /dev/null +++ b/tests/dummy/lib/modern/node_modules/flooring/index.js @@ -0,0 +1 @@ +module.exports = function() { return 'hardwood'; }; diff --git a/tests/dummy/lib/modern/node_modules/flooring/package.json b/tests/dummy/lib/modern/node_modules/flooring/package.json new file mode 100644 index 0000000..f791fcf --- /dev/null +++ b/tests/dummy/lib/modern/node_modules/flooring/package.json @@ -0,0 +1,5 @@ +{ + "name": "flooring", + "version": "2010.0.0", + "main": "index.js" +} diff --git a/tests/dummy/lib/modern/package.json b/tests/dummy/lib/modern/package.json new file mode 100644 index 0000000..0348c1d --- /dev/null +++ b/tests/dummy/lib/modern/package.json @@ -0,0 +1,11 @@ +{ + "name": "modern", + "version": "1.0.0", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "flooring": "2010.0.0", + "ember-browserify": "*" + } +} diff --git a/tests/dummy/lib/outdated/addon/utils/floor-type.js b/tests/dummy/lib/outdated/addon/utils/floor-type.js new file mode 100644 index 0000000..5327a34 --- /dev/null +++ b/tests/dummy/lib/outdated/addon/utils/floor-type.js @@ -0,0 +1,5 @@ +import flooring from 'npm:flooring'; + +export default function() { + return flooring(); +} diff --git a/tests/dummy/lib/outdated/app/utils/floor-outdated.js b/tests/dummy/lib/outdated/app/utils/floor-outdated.js new file mode 100644 index 0000000..5327a34 --- /dev/null +++ b/tests/dummy/lib/outdated/app/utils/floor-outdated.js @@ -0,0 +1,5 @@ +import flooring from 'npm:flooring'; + +export default function() { + return flooring(); +} diff --git a/tests/dummy/lib/outdated/app/utils/reexports-outdated.js b/tests/dummy/lib/outdated/app/utils/reexports-outdated.js new file mode 100644 index 0000000..aa73913 --- /dev/null +++ b/tests/dummy/lib/outdated/app/utils/reexports-outdated.js @@ -0,0 +1 @@ +export { default } from 'outdated/utils/floor-type'; diff --git a/tests/dummy/lib/outdated/index.js b/tests/dummy/lib/outdated/index.js new file mode 100644 index 0000000..18bd621 --- /dev/null +++ b/tests/dummy/lib/outdated/index.js @@ -0,0 +1,8 @@ +/*jshint node:true*/ +module.exports = { + name: 'outdated', + + isDevelopingAddon: function() { + return true; + } +}; diff --git a/tests/dummy/lib/outdated/node_modules/ember-browserify/package.json b/tests/dummy/lib/outdated/node_modules/ember-browserify/package.json new file mode 100644 index 0000000..bdd7fd3 --- /dev/null +++ b/tests/dummy/lib/outdated/node_modules/ember-browserify/package.json @@ -0,0 +1,8 @@ +{ + "name": "ember-browserify", + "version": "*", + "main": "lib/index.js", + "keywords": [ + "ember-addon" + ] +} diff --git a/tests/dummy/lib/outdated/node_modules/flooring/index.js b/tests/dummy/lib/outdated/node_modules/flooring/index.js new file mode 100644 index 0000000..fd59a48 --- /dev/null +++ b/tests/dummy/lib/outdated/node_modules/flooring/index.js @@ -0,0 +1 @@ +module.exports = function() { return 'shag carpet'; }; diff --git a/tests/dummy/lib/outdated/node_modules/flooring/package.json b/tests/dummy/lib/outdated/node_modules/flooring/package.json new file mode 100644 index 0000000..5450566 --- /dev/null +++ b/tests/dummy/lib/outdated/node_modules/flooring/package.json @@ -0,0 +1,5 @@ +{ + "name": "flooring", + "version": "1970.0.0", + "main": "index.js" +} diff --git a/tests/dummy/lib/outdated/package.json b/tests/dummy/lib/outdated/package.json new file mode 100644 index 0000000..a2839d1 --- /dev/null +++ b/tests/dummy/lib/outdated/package.json @@ -0,0 +1,11 @@ +{ + "name": "outdated", + "version": "1.0.0", + "keywords": [ + "ember-addon" + ], + "dependencies": { + "flooring": "1970.0.0", + "ember-browserify": "*" + } +} diff --git a/tests/dummy/public/crossdomain.xml b/tests/dummy/public/crossdomain.xml new file mode 100644 index 0000000..0c16a7a --- /dev/null +++ b/tests/dummy/public/crossdomain.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/tests/dummy/public/robots.txt b/tests/dummy/public/robots.txt new file mode 100644 index 0000000..f591645 --- /dev/null +++ b/tests/dummy/public/robots.txt @@ -0,0 +1,3 @@ +# http://www.robotstxt.org +User-agent: * +Disallow: diff --git a/tests/helpers/destroy-app.js b/tests/helpers/destroy-app.js new file mode 100644 index 0000000..c3d4d1a --- /dev/null +++ b/tests/helpers/destroy-app.js @@ -0,0 +1,5 @@ +import Ember from 'ember'; + +export default function destroyApp(application) { + Ember.run(application, 'destroy'); +} diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js new file mode 100644 index 0000000..76996fd --- /dev/null +++ b/tests/helpers/module-for-acceptance.js @@ -0,0 +1,23 @@ +import { module } from 'qunit'; +import Ember from 'ember'; +import startApp from '../helpers/start-app'; +import destroyApp from '../helpers/destroy-app'; + +const { RSVP: { Promise } } = Ember; + +export default function(name, options = {}) { + module(name, { + beforeEach() { + this.application = startApp(); + + if (options.beforeEach) { + return options.beforeEach.apply(this, arguments); + } + }, + + afterEach() { + let afterEach = options.afterEach && options.afterEach.apply(this, arguments); + return Promise.resolve(afterEach).then(() => destroyApp(this.application)); + } + }); +} diff --git a/tests/helpers/resolver.js b/tests/helpers/resolver.js new file mode 100644 index 0000000..b208d38 --- /dev/null +++ b/tests/helpers/resolver.js @@ -0,0 +1,11 @@ +import Resolver from '../../resolver'; +import config from '../../config/environment'; + +const resolver = Resolver.create(); + +resolver.namespace = { + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix +}; + +export default resolver; diff --git a/tests/helpers/start-app.js b/tests/helpers/start-app.js new file mode 100644 index 0000000..e098f1d --- /dev/null +++ b/tests/helpers/start-app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Application from '../../app'; +import config from '../../config/environment'; + +export default function startApp(attrs) { + let application; + + let attributes = Ember.merge({}, config.APP); + attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; + + Ember.run(() => { + application = Application.create(attributes); + application.setupForTesting(); + application.injectTestHelpers(); + }); + + return application; +} diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..f7ff652 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,33 @@ + + + + + + Dummy Tests + + + + {{content-for "head"}} + {{content-for "test-head"}} + + + + + + {{content-for "head-footer"}} + {{content-for "test-head-footer"}} + + + {{content-for "body"}} + {{content-for "test-body"}} + + + + + + + + {{content-for "body-footer"}} + {{content-for "test-body-footer"}} + + diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/test-helper.js b/tests/test-helper.js new file mode 100644 index 0000000..e6cfb70 --- /dev/null +++ b/tests/test-helper.js @@ -0,0 +1,6 @@ +import resolver from './helpers/resolver'; +import { + setResolver +} from 'ember-qunit'; + +setResolver(resolver); diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/routes/application-test.js b/tests/unit/routes/application-test.js new file mode 100644 index 0000000..863b98d --- /dev/null +++ b/tests/unit/routes/application-test.js @@ -0,0 +1,26 @@ +import { moduleFor, test } from 'ember-qunit'; + +moduleFor('route:application', 'Unit | Route | application', { + // Specify the other units that are required for this test. + // needs: ['controller:foo'] +}); + +test('it properly resolves npm modules', function(assert) { + let route = this.subject(); + + // Host application which depends on flooring@2030.0.0 + // Set by virtue of folder structure. + assert.equal(route.flooring(), 'space cloud'); + + // Moved into the app folder via the addons. + assert.equal(route.modernApp(), 'space cloud'); + assert.equal(route.outdatedApp(), 'space cloud'); + + // Addon which depends on flooring@1970.0.0 + assert.equal(route.outdated(), 'shag carpet'); + assert.equal(route.outdatedReexports(), 'shag carpet'); + + // Addon which depends on flooring@2010.0.0 + assert.equal(route.modern(), 'hardwood'); + assert.equal(route.modernReexports(), 'hardwood'); +}); diff --git a/vendor/.gitkeep b/vendor/.gitkeep new file mode 100644 index 0000000..e69de29