Skip to content

Commit fe3083b

Browse files
committed
bug #35 Fixing sourceMap bug and adding "lazy" & "export" loader options (weaverryan)
This PR was merged into the main branch. Discussion ---------- Fixing sourceMap bug and adding "lazy" & "export" loader options Fixes #33 (except for lazy controllers... that would be more complex). Hi! This PR contains 2 parts: 1) The sourcemap problem described in #33 is fixed. Thanks to @Kocal for that - he had all the right details... they just needed to be in a slightly different place. We don't need this sourcemap trick in the actual `loader.js` because that is loading a `controllers.json` file - were not trying to map eventual errors to that file. 2) This adds the ability to add a `?lazy=true` when using the `lazy-controller-loader`, allowing you to make 3rd party controllers lazy (though the syntax isn't super attractive). See the updated README. I also tested this on a real project. Cheers! Commits ------- c3c0111 Fixing sourceMap bug and adding ability to specify laziness and export names as loader query
2 parents bfa1910 + c3c0111 commit fe3083b

File tree

8 files changed

+186
-25
lines changed

8 files changed

+186
-25
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,43 @@ export default class extends Controller {
230230
}
231231
```
232232

233+
### Advanced Lazy Controllers
234+
235+
Sometimes you may want to use a third-party controller and make it lazy.
236+
Unfortunately, you can't edit that controller's source code to add
237+
the `/* stimulusFetch: 'lazy' */`.
238+
239+
To handle this, you can use the `lazy-controller-loader` with some
240+
custom query options.
241+
242+
```js
243+
// assets/bootstrap.js
244+
245+
import { startStimulusApp } from '@symfony/stimulus-bridge';
246+
247+
// example from https://stimulus-components.netlify.app/docs/components/stimulus-clipboard/
248+
// normal, non-lazy import
249+
//import Clipboard from 'stimulus-clipboard';
250+
// lazy import
251+
import Clipboard from '@symfony/stimulus-bridge/lazy-controller-loader?lazy=true!stimulus-clipboard';
252+
253+
// example from https://github.com/afcapel/stimulus-autocomplete
254+
// normal, non-lazy import
255+
//import { Autocomplete } from 'stimulus-autocomplete';
256+
// lazy import - it includes export=Autocomplete to handle the named export
257+
import { Autocomplete } from '@symfony/stimulus-bridge/lazy-controller-loader?lazy=true&export=Autocomplete!stimulus-autocomplete';
258+
259+
const app = startStimulusApp(require.context(
260+
// your existing code to load from controllers/
261+
));
262+
263+
// the normal way to manually register controllers
264+
application.register('clipboard', Clipboard)
265+
application.register('autocomplete', Autocomplete)
266+
267+
export { app };
268+
```
269+
233270
## Run tests
234271

235272
```sh

dist/webpack/generate-lazy-controller.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
*
1212
* @param {string} controllerPath The importable path to the controller
1313
* @param {Number} indentationSpaces Amount each line should be indented
14+
* @param {string} exportName The name of the module that's exported from the controller
1415
*/
1516

1617
module.exports = function generateLazyController(controllerPath, indentationSpaces) {
18+
var exportName = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'default';
1719
var spaces = ' '.repeat(indentationSpaces);
18-
return "".concat(spaces, "(function() {\n").concat(spaces, " function LazyController(context) {\n").concat(spaces, " this.__stimulusLazyController = true;\n").concat(spaces, " Controller.call(this, context);\n").concat(spaces, " }\n").concat(spaces, " LazyController.prototype = Object.create(Controller && Controller.prototype, {\n").concat(spaces, " constructor: { value: LazyController, writable: true, configurable: true }\n").concat(spaces, " });\n").concat(spaces, " Object.setPrototypeOf(LazyController, Controller);\n").concat(spaces, " LazyController.prototype.initialize = function() {\n").concat(spaces, " var _this = this;\n").concat(spaces, " if (this.application.controllers.find(function(controller) {\n").concat(spaces, " return controller.identifier === _this.identifier && controller.__stimulusLazyController;\n").concat(spaces, " })) {\n").concat(spaces, " return;\n").concat(spaces, " }\n").concat(spaces, " import('").concat(controllerPath.replace(/\\/g, '\\\\'), "').then(function(controller) {\n").concat(spaces, " _this.application.register(_this.identifier, controller.default);\n").concat(spaces, " });\n").concat(spaces, " }\n").concat(spaces, " return LazyController;\n").concat(spaces, "})()");
20+
return "".concat(spaces, "(function() {\n").concat(spaces, " function LazyController(context) {\n").concat(spaces, " this.__stimulusLazyController = true;\n").concat(spaces, " Controller.call(this, context);\n").concat(spaces, " }\n").concat(spaces, " LazyController.prototype = Object.create(Controller && Controller.prototype, {\n").concat(spaces, " constructor: { value: LazyController, writable: true, configurable: true }\n").concat(spaces, " });\n").concat(spaces, " Object.setPrototypeOf(LazyController, Controller);\n").concat(spaces, " LazyController.prototype.initialize = function() {\n").concat(spaces, " var _this = this;\n").concat(spaces, " if (this.application.controllers.find(function(controller) {\n").concat(spaces, " return controller.identifier === _this.identifier && controller.__stimulusLazyController;\n").concat(spaces, " })) {\n").concat(spaces, " return;\n").concat(spaces, " }\n").concat(spaces, " import('").concat(controllerPath.replace(/\\/g, '\\\\'), "').then(function(controller) {\n").concat(spaces, " _this.application.register(_this.identifier, controller.").concat(exportName, ");\n").concat(spaces, " });\n").concat(spaces, " }\n").concat(spaces, " return LazyController;\n").concat(spaces, "})()");
1921
};

dist/webpack/lazy-controller-loader.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
1717
var generateLazyController = require('./generate-lazy-controller');
1818

1919
var getStimulusCommentOptions = require('../util/get-stimulus-comment-options');
20+
21+
var _require = require('loader-utils'),
22+
getOptions = _require.getOptions;
23+
24+
var _require2 = require('schema-utils'),
25+
validate = _require2.validate;
26+
27+
var schema = {
28+
type: 'object',
29+
properties: {
30+
lazy: {
31+
type: 'boolean'
32+
},
33+
"export": {
34+
type: 'string'
35+
}
36+
}
37+
};
2038
/**
2139
* Loader that can make a Stimulus controller lazy.
2240
*
@@ -27,11 +45,12 @@ var getStimulusCommentOptions = require('../util/get-stimulus-comment-options');
2745
* element appears.
2846
*
2947
* @param {string} source of a module that exports a Stimulus controller
48+
* @param {string} sourceMap the current source map string
49+
*
3050
* @return {string}
3151
*/
3252

33-
34-
module.exports = function (source) {
53+
module.exports = function (source, sourceMap) {
3554
var _getStimulusCommentOp = getStimulusCommentOptions(source),
3655
options = _getStimulusCommentOp.options,
3756
errors = _getStimulusCommentOp.errors;
@@ -56,11 +75,22 @@ module.exports = function (source) {
5675
this.emitError(new Error("Invalid value \"".concat(stimulusFetch, "\" found for \"stimulusFetch\". Allowed values are \"lazy\" or \"eager\"")));
5776
}
5877

59-
var isLazy = stimulusFetch === 'lazy';
78+
var loaderOptions = getOptions(this);
79+
validate(schema, loaderOptions, {
80+
name: '@symfony/stimulus-bridge/lazy-controller-loader',
81+
baseDataPath: 'options'
82+
}); // the ?lazy= loader option takes priority over the comment
83+
84+
var isLazy = typeof loaderOptions.lazy !== 'undefined' ? loaderOptions.lazy : stimulusFetch === 'lazy';
6085

6186
if (!isLazy) {
62-
return source;
87+
return this.callback(null, source, sourceMap);
6388
}
6489

65-
return "import { Controller } from 'stimulus';\nexport default ".concat(generateLazyController(this.resource, 0));
90+
var exportName = typeof loaderOptions["export"] !== 'undefined' ? loaderOptions["export"] : 'default';
91+
var finalSource = "import { Controller } from 'stimulus';\nconst controller = ".concat(generateLazyController(this.resource, 0, exportName), ";\nexport { controller as ").concat(exportName, " };"); // The source Map cannot be passed when lazy, as the sourceMap won't
92+
// map up to the new source. In theory, this is fixable, but I'm
93+
// not entirely sure how.
94+
95+
this.callback(null, finalSource);
6696
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"stimulus": "^2.0"
1717
},
1818
"dependencies": {
19-
"acorn": "^8.0.5"
19+
"acorn": "^8.0.5",
20+
"loader-utils": "^2.0.0",
21+
"schema-utils": "^3.0.0"
2022
},
2123
"devDependencies": {
2224
"@babel/cli": "^7.12.1",

src/webpack/generate-lazy-controller.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
*
1414
* @param {string} controllerPath The importable path to the controller
1515
* @param {Number} indentationSpaces Amount each line should be indented
16+
* @param {string} exportName The name of the module that's exported from the controller
1617
*/
17-
module.exports = function generateLazyController(controllerPath, indentationSpaces) {
18+
module.exports = function generateLazyController(controllerPath, indentationSpaces, exportName = 'default') {
1819
const spaces = ' '.repeat(indentationSpaces);
1920

2021
return `${spaces}(function() {
@@ -34,7 +35,7 @@ ${spaces} })) {
3435
${spaces} return;
3536
${spaces} }
3637
${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then(function(controller) {
37-
${spaces} _this.application.register(_this.identifier, controller.default);
38+
${spaces} _this.application.register(_this.identifier, controller.${exportName});
3839
${spaces} });
3940
${spaces} }
4041
${spaces} return LazyController;

src/webpack/lazy-controller-loader.js

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@
1111

1212
const generateLazyController = require('./generate-lazy-controller');
1313
const getStimulusCommentOptions = require('../util/get-stimulus-comment-options');
14+
const { getOptions } = require('loader-utils');
15+
const { validate } = require('schema-utils');
16+
17+
const schema = {
18+
type: 'object',
19+
properties: {
20+
lazy: {
21+
type: 'boolean',
22+
},
23+
export: {
24+
type: 'string',
25+
},
26+
},
27+
};
1428

1529
/**
1630
* Loader that can make a Stimulus controller lazy.
@@ -22,9 +36,11 @@ const getStimulusCommentOptions = require('../util/get-stimulus-comment-options'
2236
* element appears.
2337
*
2438
* @param {string} source of a module that exports a Stimulus controller
39+
* @param {string} sourceMap the current source map string
40+
*
2541
* @return {string}
2642
*/
27-
module.exports = function (source) {
43+
module.exports = function (source, sourceMap) {
2844
const { options, errors } = getStimulusCommentOptions(source);
2945

3046
for (const error of errors) {
@@ -41,12 +57,29 @@ module.exports = function (source) {
4157
)
4258
);
4359
}
44-
const isLazy = stimulusFetch === 'lazy';
60+
61+
const loaderOptions = getOptions(this);
62+
63+
validate(schema, loaderOptions, {
64+
name: '@symfony/stimulus-bridge/lazy-controller-loader',
65+
baseDataPath: 'options',
66+
});
67+
68+
// the ?lazy= loader option takes priority over the comment
69+
const isLazy = typeof loaderOptions.lazy !== 'undefined' ? loaderOptions.lazy : stimulusFetch === 'lazy';
4570

4671
if (!isLazy) {
47-
return source;
72+
return this.callback(null, source, sourceMap);
4873
}
4974

50-
return `import { Controller } from 'stimulus';
51-
export default ${generateLazyController(this.resource, 0)}`;
75+
const exportName = typeof loaderOptions.export !== 'undefined' ? loaderOptions.export : 'default';
76+
77+
const finalSource = `import { Controller } from 'stimulus';
78+
const controller = ${generateLazyController(this.resource, 0, exportName)};
79+
export { controller as ${exportName} };`;
80+
81+
// The source Map cannot be passed when lazy, as the sourceMap won't
82+
// map up to the new source. In theory, this is fixable, but I'm
83+
// not entirely sure how.
84+
this.callback(null, finalSource);
5285
};

test/webpack/generate-lazy-controller.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('generateLazyControllerModule', () => {
3030
const lazyControllerClass = eval(`${controllerCode}`);
3131
// if all goes correctly, the prototype should have a Controller key
3232
expect(Object.getPrototypeOf(lazyControllerClass)).toHaveProperty('Controller');
33+
expect(controllerCode).toContain('_this.application.register(_this.identifier, controller.default)');
3334
});
3435

3536
it('must return a functional ES5 class on Windows', () => {
@@ -52,5 +53,21 @@ describe('generateLazyControllerModule', () => {
5253
*/
5354
expect(controllerCode).toContain(`import('C:\\\\\\\\path\\\\to\\\\file.js')`);
5455
});
56+
57+
it('must use the correct, named export', () => {
58+
const controllerCode =
59+
"const Controller = require('stimulus');\n" +
60+
// this, for some reason, is undefined in a test but populated in a real situation
61+
// this avoid an explosion since it is undefined here
62+
'Controller.prototype = {};\n' +
63+
generateLazyController('@symfony/some-module/dist/controller.js', 0, 'CustomController');
64+
const result = babelParser.parse(controllerCode, {
65+
sourceType: 'module',
66+
});
67+
expect(babelTypes.isNode(result)).toBeTruthy();
68+
expect(controllerCode).toContain(
69+
'_this.application.register(_this.identifier, controller.CustomController)'
70+
);
71+
});
5572
});
5673
});

test/webpack/lazy-controller-loader.test.js

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,79 @@
1111

1212
const lazyControllerLoader = require('../../dist/webpack/lazy-controller-loader');
1313

14-
function callLoader(src, errors = []) {
14+
function callLoader(src, startingSourceMap = '', query = '') {
1515
const loaderThis = {
16+
emittedErrors: [],
17+
executedCallback: null,
18+
1619
resource: './some-resource',
20+
query,
1721
emitError(error) {
18-
errors.push(error);
22+
this.emittedErrors.push(error);
23+
},
24+
callback(error, content, sourceMap) {
25+
this.executedCallback = { error, content, sourceMap };
1926
},
2027
};
2128

22-
return lazyControllerLoader.call(loaderThis, src);
29+
lazyControllerLoader.call(loaderThis, src, startingSourceMap);
30+
31+
return {
32+
content: loaderThis.executedCallback.content,
33+
errors: loaderThis.emittedErrors,
34+
sourceMap: loaderThis.executedCallback.sourceMap,
35+
callbackErrors: loaderThis.executedCallback.errors,
36+
};
2337
}
2438

2539
describe('lazyControllerLoader', () => {
2640
it('does nothing with a non-lazy controller', () => {
2741
const src = 'export default class extends Controller {}';
28-
expect(callLoader(src)).toEqual(src);
42+
expect(callLoader(src).content).toEqual(src);
43+
expect(callLoader(src, 'source_map_contents').sourceMap).toEqual('source_map_contents');
44+
expect(callLoader(src).errors).toHaveLength(0);
2945
});
3046

3147
it('it exports a lazy controller', () => {
3248
const src = "/* stimulusFetch: 'lazy' */ export default class extends Controller {}";
3349
// look for a little bit of the lazy controller code
34-
expect(callLoader(src)).toContain('application.register(');
50+
expect(callLoader(src).content).toContain('function LazyController');
51+
// unfortunately, we cannot pass along sourceMap info since we changed the source
52+
expect(callLoader(src, 'source_map_contents').sourceMap).toBeUndefined();
53+
expect(callLoader(src).errors).toHaveLength(0);
3554
});
3655

3756
it('it emits an error on a syntax problem', () => {
3857
const src = '/* stimulusFetch: "lazy */ export default class extends Controller {}';
39-
const errors = [];
40-
callLoader(src, errors);
41-
expect(errors).toHaveLength(1);
58+
expect(callLoader(src).errors).toHaveLength(1);
4259
});
4360

4461
it('it emits an error on an invalid value', () => {
4562
const src = '/* stimulusFetch: "lazy-once" */ export default class extends Controller {}';
46-
const errors = [];
47-
callLoader(src, errors);
48-
expect(errors).toHaveLength(1);
63+
expect(callLoader(src).errors).toHaveLength(1);
64+
});
65+
66+
it('it reads ?lazy option', () => {
67+
const src = 'export default class extends Controller {}';
68+
const results = callLoader(src, '', '?lazy=true');
69+
expect(results.content).toContain('function LazyController');
70+
expect(results.errors).toHaveLength(0);
71+
});
72+
73+
it('it reads ?lazy and it wins over comments', () => {
74+
const src = "/* stimulusFetch: 'eager' */ export default class extends Controller {}";
75+
const results = callLoader(src, '', '?lazy=true');
76+
expect(results.content).toContain('function LazyController');
77+
expect(results.errors).toHaveLength(0);
78+
});
79+
80+
it('it reads ?export for non-default exports', () => {
81+
const src = 'const MyController = class extends Controller {}; export { MyController };';
82+
const results = callLoader(src, '', '?lazy=true&export=MyController');
83+
// check that the results are lazy
84+
expect(results.content).toContain('function LazyController');
85+
// check named export
86+
expect(results.content).toContain('export { controller as MyController };');
87+
expect(results.errors).toHaveLength(0);
4988
});
5089
});

0 commit comments

Comments
 (0)