├── .gitignore ├── README.md ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── test ├── app.js ├── components │ └── bear-component.ts ├── router.js ├── routes │ └── bear.js ├── templates │ ├── application.hbs │ ├── bear.hbs │ └── components │ │ └── bear-component.hbs └── tests │ └── resolve_test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-webpack-resolver 2 | 3 | > An Ember.js resolver heavily inspired by 4 | https://github.com/stefanpenner/ember-jj-abrams-resolver but mainly for use with webpack. 5 | 6 | ## Install 7 | 8 | ``` shell 9 | npm install ember-webpack-resolver --save-dev 10 | ``` 11 | 12 | ## Usage 13 | 14 | This resolver is intended to resolve modules with a folder structure like such: 15 | 16 | ``` 17 | | - app/ 18 | | --- components/ 19 | | --- controllers/ 20 | | --- models/ 21 | | --- routes/ 22 | | --- templates/ 23 | | --- app.js 24 | | --- router.js 25 | | - node_modules/ 26 | | --- some-widget-ember-component 27 | | ----- index.js 28 | | ----- index.hbs 29 | ``` 30 | 31 | A very simple config will resolve just your local modules: 32 | 33 | ``` javascript 34 | const App = Ember.Application.create({ 35 | Resolver: require('ember-webpack-resolver?' + __dirname)() 36 | }); 37 | ``` 38 | 39 | If you're using a file extension other than `.js`, supply the lookup extensions such use with typescript: 40 | 41 | ``` javascript 42 | const App = Ember.Application.create({ 43 | Resolver: require('ember-webpack-resolver?' + __dirname)({ 44 | extensions: ['.ts', '.hbs'] 45 | }) 46 | }); 47 | ``` 48 | 49 | ### Custom Lookup Patterns 50 | If you have a custom module type that you need to resolve, use the `lookupPatterns` option. It takes an array of functions with each function receiving a `parsedName` argument. The function optionally returns a `moduleName` value based on some criteria. 51 | 52 | ``` javascript 53 | const reactModuleFilter = function(parsedName) { 54 | if (parsedName.type === 'react') { 55 | return './react/' + parsedName.fullNameWithoutType 56 | } 57 | } 58 | 59 | const App = Ember.Application.create({ 60 | Resolver: require('ember-webpack-resolver?' + __dirname)({ 61 | extensions: ['.ts', '.hbs'], 62 | lookupPatterns: [reactModuleFilter] 63 | }) 64 | }); 65 | 66 | ``` 67 | 68 | ### Resolving Components 69 | If you want to also resolve modules within vendor folders, a bit more configuration is required: 70 | 71 | ``` javascript 72 | const App = Ember.Application.create({ 73 | Resolver: require('ember-webpack-resolver?' + __dirname)({ 74 | components: { 75 | 'some-widget': require('some-widget-ember-component'), 76 | 'other-widget': require('some-other-ember-component') 77 | } 78 | }) 79 | }); 80 | ``` 81 | 82 | Then it will resolve to the specified module when inserted into your template: 83 | 84 | ``` html 85 |

{{some-widget value="Hooray!"}}

86 |

{{#other-widget}}Stuff{{/other-widget}}

87 | ``` 88 | 89 | --- 90 | 91 | *To resolve modules within the `bower_components` folder, be sure to add the folder to your webpack config:* 92 | 93 | ``` javascript 94 | module.exports = { 95 | // ... 96 | resolve: { 97 | moduleDirectories: ["node_modules", "bower_components"] 98 | } 99 | }; 100 | ``` 101 | 102 | --- 103 | 104 | ## Release History 105 | * Look at [commits](https://github.com/shama/ember-webpack-resolver/commits/master) for release history 106 | * 1.0.0 - Support for returning Ember classes with `lookupPatterns`. 107 | * 0.3.0 - simplify resolving components 108 | * 0.2.0 - handle nested components, update API 109 | * 0.1.0 - initial release 110 | 111 | ## License 112 | Copyright (c) 2020 Kyle Robinson Young 113 | Licensed under the MIT license. 114 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 9 |
10 |
11 | 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(options) { 2 | options = options || {}; 3 | 4 | // Automatically set context if require('ember-webpack-resolver?' + __dirname) 5 | if (__resourceQuery && !options.context) { 6 | options.context = require.context(__resourceQuery.substr(1), true); 7 | } 8 | 9 | options.modulePrefix = options.modulePrefix || './'; 10 | options.podModulePrefix = options.podModulePrefix || "pods"; 11 | options.extensions = options.extensions || ['.js', '.hbs']; 12 | options.fixToString = options.fixToString !== false; 13 | options.has = options.has || options.context.keys(); 14 | options.components = options.components || Object.create(null); 15 | 16 | // Convert has array to index for faster lookups 17 | var hasIndex = Object.create(null); 18 | var useHasIndex = false; 19 | if (Array.isArray(options.has)) { 20 | useHasIndex = true; 21 | for (var i = 0; i < options.has.length; i++) { 22 | hasIndex[options.has[i]] = 1; 23 | } 24 | } 25 | 26 | function parseName(fullName) { 27 | var nameParts = fullName.split(':'); 28 | return { 29 | fullName: fullName, 30 | type: nameParts[0], 31 | fullNameWithoutType: nameParts[1], 32 | name: nameParts[1], 33 | root: Ember.get(this, 'namespace'), 34 | resolveMethodName: 'resolve' + Ember.String.classify(nameParts[0]) 35 | }; 36 | } 37 | 38 | function getToStringFunction(type, fullNameWithoutType) { 39 | return function () { 40 | type = Ember.String.classify(type); 41 | if (type === 'Model') type = ''; 42 | return 'App.' + Ember.String.classify(fullNameWithoutType + type); 43 | } 44 | } 45 | 46 | function resolveOther(parsedName) { 47 | if (!parsedName.name || !parsedName.type) { 48 | return this._super.apply(this, arguments); 49 | } 50 | 51 | var factory; 52 | var moduleName = false; 53 | // Add variations for Ember pod-like structure 54 | var podVariation = options.modulePrefix + parsedName.fullNameWithoutType + '/' + parsedName.type; 55 | var withinComponentPodVariation = options.modulePrefix + parsedName.type + 's/' + parsedName.fullNameWithoutType + '/' + parsedName.type; 56 | if (parsedName.type === 'template' && parsedName.fullNameWithoutType.slice(0, 11) === 'components/') { 57 | podVariation = options.modulePrefix + parsedName.fullNameWithoutType.slice(11) + '/' + parsedName.type; 58 | withinComponentPodVariation = options.modulePrefix + 'components/' + parsedName.fullNameWithoutType.slice(11) + '/' + parsedName.type; 59 | } 60 | 61 | var withinPodModulePrefixVariation = "./" + options.podModulePrefix + podVariation.slice(1); 62 | 63 | var variations = [ 64 | podVariation, 65 | options.modulePrefix + parsedName.type + 's/' + parsedName.fullNameWithoutType, 66 | withinComponentPodVariation, 67 | withinPodModulePrefixVariation 68 | ]; 69 | var contextrequire = options.context; 70 | 71 | if (useHasIndex) { 72 | dance: 73 | for (var i = 0; i < variations.length; i++) { 74 | // Try without an extension first 75 | if (hasIndex[variations[i]]) { 76 | moduleName = variations[i]; 77 | break; 78 | } 79 | // Now try extension variations 80 | for (var j = 0; j < options.extensions.length; j++) { 81 | if (hasIndex[variations[i] + options.extensions[j]]) { 82 | moduleName = variations[i] + options.extensions[j]; 83 | break dance; 84 | } 85 | } 86 | } 87 | } else { 88 | moduleName = options.modulePrefix + parsedName.type + 's/' + parsedName.fullNameWithoutType; 89 | } 90 | 91 | /** 92 | A possible array of functions to test for moduleName's based on the provided 93 | `parsedName`. This allows easy customization of additional module based 94 | lookup patterns. 95 | */ 96 | if (Array.isArray(options.lookupPatterns)) { 97 | for (var i = 0; i < options.lookupPatterns.length; i++) { 98 | var lookupFn = options.lookupPatterns[i]; 99 | if (typeof lookupFn === 'function') { 100 | var result = lookupFn(parsedName); 101 | if (!(result === void 0)) { 102 | if (typeof result === 'string') { 103 | moduleName = result; 104 | } else { 105 | factory = result; 106 | moduleName = true; 107 | } 108 | } 109 | } else { 110 | throw new Error('Lookup patterns should be functions. Got type "' + typeof lookupFn + '" instead.'); 111 | } 112 | } 113 | } 114 | 115 | // If module not found, look if matches a specified component 116 | if (moduleName === false && parsedName.type === 'component' && options.components[parsedName.fullNameWithoutType]) { 117 | moduleName = parsedName.fullNameWithoutType; 118 | factory = options.components[parsedName.fullNameWithoutType]; 119 | } 120 | 121 | // Module not found, return the parent 122 | if (moduleName === false) { 123 | if (Ember.ENV.LOG_MODULE_RESOLVER) { 124 | Ember.Logger.info('miss', parsedName.fullName); 125 | } 126 | return this._super.apply(this, arguments); 127 | } 128 | 129 | if (!factory) { 130 | try { 131 | factory = contextrequire(moduleName); 132 | } catch (err) { 133 | if (Ember.ENV.LOG_MODULE_RESOLVER) { 134 | Ember.Logger.info('miss', moduleName); 135 | } 136 | return this._super.apply(this, arguments); 137 | } 138 | } 139 | 140 | if (factory === undefined) { 141 | throw new Error(' Expected to find: "' + parsedName.fullName + '" within "' + moduleName + '" but got "undefined". Did you forget to `module.exports` within "' + moduleName + '"?'); 142 | } 143 | 144 | // If using `export default` 145 | if (factory && factory['default']) { 146 | factory = factory['default']; 147 | } 148 | 149 | // To fix class introspection 150 | if (options.fixToString) { 151 | var className = factory.toString(); 152 | if (className.indexOf('(') !== -1 || className === "@ember/component") { 153 | factory.toString = getToStringFunction(parsedName.type, parsedName.fullNameWithoutType); 154 | } 155 | } 156 | 157 | if (Ember.ENV.LOG_MODULE_RESOLVER) { 158 | Ember.Logger.info('hit', moduleName); 159 | } 160 | return factory; 161 | } 162 | 163 | function resolveRouter(parsedName) { 164 | if (parsedName.fullName === 'router:main') { 165 | var name = options.modulePrefix + 'router'; 166 | if (options.context.keys().indexOf(name) !== -1) { 167 | var context = options.context(name); 168 | var isModule = typeof context === "object" && typeof context.default === "function"; 169 | return isModule ? context.default : context; 170 | } 171 | } 172 | } 173 | 174 | return Ember.DefaultResolver.extend({ 175 | resolveOther: resolveOther, 176 | resolveTemplate: resolveOther, // TODO: Check Ember.TEMPLATES as backup 177 | resolveRouter: resolveRouter, 178 | parseName: parseName, 179 | makeToString: function(factory, fullName) { 180 | return '' + options.modulePrefix + '@' + fullName + ':'; 181 | }, 182 | normalize: function(fullName) { 183 | // replace `.` with `/` in order to make nested controllers work in the following cases 184 | // 1. `needs: ['posts/post']` 185 | // 2. `{{render 'posts/post'}}` 186 | // 3. `this.render('posts/post')` from Route 187 | return Ember.String.dasherize(fullName.replace(/\./g, '/')); 188 | } 189 | }); 190 | }; 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-webpack-resolver", 3 | "version": "1.4.1", 4 | "description": "An Ember resolver for use with webpack", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "test": "webpack-dev-server" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/shama/ember-webpack-resolver.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/shama/ember-webpack-resolver/issues" 18 | }, 19 | "license": "MIT", 20 | "keywords": [ 21 | "ember", 22 | "resolver", 23 | "webpack" 24 | ], 25 | "devDependencies": { 26 | "css-loader": "^4.3.0", 27 | "ember-source": "3.12.2", 28 | "ember-templates-loader": "~2.14.1-1", 29 | "expose-loader": "^0.7.5", 30 | "html-webpack-plugin": "^4.5.2", 31 | "jquery": "^3.6.0", 32 | "qunitjs": "1.23.0", 33 | "script-loader": "^0.7.2", 34 | "style-loader": "^1.3.0", 35 | "webpack": "5.94.0", 36 | "webpack-cli": "^5.1.4", 37 | "webpack-dev-server": "^5.2.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | // Load Ember + Deps 2 | require("expose-loader?$!expose-loader?jQuery!jquery/dist/jquery.min.js") 3 | 4 | window.Ember = {} 5 | // This will include Ember in a more readable way, rather than using eval on it 6 | require("expose-loader?unused_var!ember-source/dist/ember.debug.js") 7 | 8 | // Use RSVP from Ember for Promises 9 | window.RSVP = Ember.RSVP 10 | window.Promise = Ember.RSVP.Promise 11 | 12 | require("script-loader!ember-source/dist/ember-testing.js") 13 | 14 | // Load QUnit 15 | require("script-loader!qunitjs/qunit/qunit.js") 16 | require("style-loader!css-loader!qunitjs/qunit/qunit.css") 17 | 18 | QUnit.config.autostart = false; 19 | 20 | // Create Fixture Ember App 21 | const App = Ember.Application.extend({ 22 | Resolver: require('../?' + __dirname)() 23 | }); 24 | 25 | const app = App.create({ 26 | rootElement: "#qunit-fixture", 27 | LOG_ACTIVE_GENERATION: true, 28 | LOG_VIEW_LOOKUPS: false 29 | }) 30 | 31 | app.setupForTesting(); 32 | app.injectTestHelpers(); 33 | //App.deferReadiness(); 34 | 35 | $(document).ready(function() { 36 | QUnit.start(); 37 | }); 38 | 39 | QUnit.testStart(function() { 40 | app.reset(); 41 | }); 42 | 43 | // Automatically load all tests (files that end with _test.js) 44 | var requireTest = require.context('./', true, /_test\.js$/) 45 | requireTest.keys().forEach(requireTest); 46 | -------------------------------------------------------------------------------- /test/components/bear-component.ts: -------------------------------------------------------------------------------- 1 | module.exports = Ember.Component.extend({ 2 | tagName: "button", 3 | classNames: ["bear"], 4 | wasClicked: false, 5 | click() { 6 | this.toggleProperty("wasClicked") 7 | } 8 | }) -------------------------------------------------------------------------------- /test/router.js: -------------------------------------------------------------------------------- 1 | const Router = Ember.Router.extend({ 2 | location: "history" 3 | }) 4 | 5 | export default Router 6 | 7 | Router.map(function() { 8 | this.route('bear') 9 | }) 10 | -------------------------------------------------------------------------------- /test/routes/bear.js: -------------------------------------------------------------------------------- 1 | module.exports = Ember.Route.extend({}) -------------------------------------------------------------------------------- /test/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

APPLICATION

2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /test/templates/bear.hbs: -------------------------------------------------------------------------------- 1 |

BEAR

2 | {{component "bear-component"}} -------------------------------------------------------------------------------- /test/templates/components/bear-component.hbs: -------------------------------------------------------------------------------- 1 | {{#if wasClicked}} 2 | ON 3 | {{else}} 4 | OFF 5 | {{/if}} 6 | -------------------------------------------------------------------------------- /test/tests/resolve_test.js: -------------------------------------------------------------------------------- 1 | QUnit.module('resolve') 2 | 3 | test('resolve a route and template', function() { 4 | visit('/bear') 5 | click('.bear') 6 | andThen(function() { 7 | equal($.trim(find('.bear').text()), 'ON') 8 | }) 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = env => { 6 | return { 7 | entry: { 8 | index: [path.resolve(__dirname, "test/app.js")], 9 | }, 10 | module: { 11 | rules: [ 12 | { test: /\.hbs$/, loader: "ember-templates-loader" } 13 | ] 14 | }, 15 | resolve: { 16 | extensions: [".ts", ".js"], 17 | alias: { 18 | "ember-testing": path.resolve(__dirname, "node_modules/ember-source/dist/ember-testing.js") 19 | } 20 | }, 21 | mode: 'development', 22 | devServer: { 23 | historyApiFallback: { 24 | index: '/index.html' 25 | }, 26 | index: 'index.html' 27 | }, 28 | node: { 29 | __filename: true, 30 | __dirname: true 31 | }, 32 | plugins: [ 33 | new webpack.ProgressPlugin({ profile: false }), 34 | new webpack.LoaderOptionsPlugin({ 35 | options: { 36 | emberTemplatesLoader: { 37 | compiler: require.resolve("ember-source/dist/ember-template-compiler.js") 38 | } 39 | } 40 | }), 41 | // Ember 2.10.2 adds a package vertx that doesnt exist nor needs to, so we ignore it 42 | new webpack.IgnorePlugin(/vertx/), 43 | new HtmlWebpackPlugin({ 44 | template: "index.html" 45 | }) 46 | ] 47 | }; 48 | }; 49 | --------------------------------------------------------------------------------