├── .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 |
--------------------------------------------------------------------------------