Skip to content

Commit 5a7d462

Browse files
committed
feature #15 "fetch: lazy": don't download until the controller is present (weaverryan)
This PR was squashed before being merged into the main branch. Discussion ---------- "fetch: lazy": don't download until the controller is present Hi! This deprecates the `webpackMode` option in `controllers.json` and replaces it with a new `lazy` option that can be set to `eager` or `lazy`. The `lazy` mode is quite a bit smarter than the old `webpackMode: 'lazy'`. When you use this, the controller (and its dependencies) isn't downloaded until the corresponding `data-controller` appears on the page. This is great for things like chartjs, which is quite huge, but probably you don't have it on every page and a tiny delay is no problem. PR to update the UX packages: symfony/ux#53 Cheers! Fixes #3 Commits ------- a9028b2 "fetch: lazy": don't download until the controller is present
2 parents e112b75 + a9028b2 commit 5a7d462

19 files changed

+358
-45
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
Before: `@symfony/ux-dropzone/dropzone`
1212
After: `symfony--ux-dropzone--dropzone`
1313

14+
* Support for "lazy controllers" was added. By setting the `fetch`
15+
in `controllers.json` to `lazy`, your controller will not
16+
be downloaded until the controller element first appears on the page.
17+
18+
* The `webpackMode` option in `controllers.json` was deprecated. Use
19+
the new `fetch` option instead.
20+
1421
## 1.1.0
1522

1623
* Support for Stimulus 1 dropped and support for Stimulus 2 added - #4.

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,44 @@ If you get the error:
152152
153153
Be sure to upgrade to `@symfony/webpack-encore` version 1.0.0 or higher.
154154

155+
## The controllers.json File
156+
157+
The bridge works by reading a `controllers.json` file that is automatically
158+
updated by Symfony Flex whenever you download a UX-powered package. For
159+
example, after running `composer require symfony/ux-dropzone`, it will
160+
look like this:
161+
162+
```json
163+
{
164+
"controllers": {
165+
"@symfony/ux-dropzone": {
166+
"dropzone": {
167+
"enabled": true,
168+
"fetch": "eager",
169+
"autoimport": {
170+
"@symfony/ux-dropzone/src/style.css": true
171+
}
172+
}
173+
}
174+
},
175+
"entrypoints": []
176+
}
177+
```
178+
179+
Each item under `controllers` will cause a Stimulus controller to be
180+
registered with a specific name - in this case the controller would
181+
be called `symfony--ux-dropzone--dropzone` (the `/` becomes `--`).
182+
183+
By default, the new controller will always be included in your
184+
JavaScript package. You can control that with the `fetch` option,
185+
ordered from least to most lazy:
186+
187+
* `fetch: 'eager'`: controller & dependencies are included in the JavaScript
188+
that's downloaded when the page is loaded.
189+
* `fetch: 'lazy'`: controller & dependencies are isolated into a
190+
separate file and only downloaded asynchronously if (and when) the `data-controller`
191+
HTML appears on the page.
192+
155193
## Run tests
156194

157195
```sh

dist/index.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,14 @@ function startStimulusApp(context) {
3030
application.load((0, _webpackHelpers.definitionsFromContext)(context));
3131
}
3232

33-
var _loop = function _loop(_controllerName) {
34-
if (!_controllers["default"].hasOwnProperty(_controllerName)) {
35-
controllerName = _controllerName;
33+
var _loop = function _loop(controllerName) {
34+
if (!_controllers["default"].hasOwnProperty(controllerName)) {
3635
return "continue";
3736
}
3837

39-
_controllers["default"][_controllerName].then(function (module) {
40-
// Normalize the controller name: remove the initial @ and use Stimulus format
41-
_controllerName = _controllerName.substr(1).replace(/_/g, '-').replace(/\//g, '--');
42-
application.register(_controllerName, module["default"]);
38+
_controllers["default"][controllerName].then(function (module) {
39+
application.register(controllerName, module["default"]);
4340
});
44-
45-
controllerName = _controllerName;
4641
};
4742

4843
for (var controllerName in _controllers["default"]) {

dist/webpack/create-controllers-module.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88
*/
99
'use strict';
1010

11+
var generateLazyController = require('./generate-lazy-controller');
12+
1113
module.exports = function createControllersModule(config) {
1214
var controllerContents = 'export default {';
1315
var autoImportContents = '';
16+
var hasLazyControllers = false;
17+
var deprecations = [];
1418

1519
if ('undefined' !== typeof config['placeholder']) {
1620
throw new Error('Your controllers.json file was not found. Be sure to add a Webpack alias from "@symfony/stimulus-bridge/controllers.json" to *your* controllers.json file.');
@@ -24,7 +28,9 @@ module.exports = function createControllersModule(config) {
2428
var packageConfig = require(packageName + '/package.json');
2529

2630
for (var controllerName in config.controllers[packageName]) {
27-
var controllerReference = packageName + '/' + controllerName; // Find package config for the controller
31+
var controllerReference = packageName + '/' + controllerName; // Normalize the controller name: remove the initial @ and use Stimulus format
32+
33+
var controllerNormalizedName = controllerReference.substr(1).replace(/_/g, '-').replace(/\//g, '--'); // Find package config for the controller
2834

2935
if ('undefined' === typeof packageConfig.symfony.controllers[controllerName]) {
3036
throw new Error('Controller "' + controllerReference + '" does not exist in the package and cannot be compiled.');
@@ -38,8 +44,28 @@ module.exports = function createControllersModule(config) {
3844
}
3945

4046
var controllerMain = packageName + '/' + controllerPackageConfig.main;
41-
var webpackMode = controllerUserConfig.webpackMode;
42-
controllerContents += "\n '" + controllerReference + '\': import(/* webpackMode: "' + webpackMode + '" */ \'' + controllerMain + "'),";
47+
var fetchMode = 'eager';
48+
49+
if ('undefined' !== typeof controllerUserConfig.webpackMode) {
50+
deprecations.push('The "webpackMode" config key is deprecated in controllers.json. Use "fetch" instead, set to either "eager" or "lazy".');
51+
}
52+
53+
if ('undefined' !== typeof controllerUserConfig.fetch) {
54+
if (!['eager', 'lazy'].includes(controllerUserConfig.fetch)) {
55+
throw new Error("Invalid \"fetch\" value \"".concat(controllerUserConfig.fetch, "\" in controllers.json. Expected \"eager\" or \"lazy\"."));
56+
}
57+
58+
fetchMode = controllerUserConfig.fetch;
59+
}
60+
61+
var moduleValueContents = "import(/* webpackMode: \"eager\" */ '".concat(controllerMain, "')");
62+
63+
if (fetchMode === 'lazy') {
64+
hasLazyControllers = true;
65+
moduleValueContents = "\nnew Promise((resolve, reject) => resolve({ default:\n".concat(generateLazyController(controllerMain, 6), "\n }))\n ").trim();
66+
}
67+
68+
controllerContents += "\n '".concat(controllerNormalizedName, "': ").concat(moduleValueContents, ",");
4369

4470
for (var autoimport in controllerUserConfig.autoimport || []) {
4571
if (controllerUserConfig.autoimport[autoimport]) {
@@ -49,5 +75,12 @@ module.exports = function createControllersModule(config) {
4975
}
5076
}
5177

52-
return "".concat(autoImportContents).concat(controllerContents, "\n};");
78+
if (hasLazyControllers) {
79+
controllerContents = "import { Controller } from 'stimulus';\n".concat(controllerContents);
80+
}
81+
82+
return {
83+
finalSource: "".concat(autoImportContents).concat(controllerContents, "\n};"),
84+
deprecations: deprecations
85+
};
5386
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
'use strict';
10+
/**
11+
*
12+
* @param {string} controllerPath The importable path to the controller
13+
* @param {Number} indentationSpaces Amount each line should be indented
14+
*/
15+
16+
module.exports = function generateLazyController(controllerPath, indentationSpaces) {
17+
var spaces = ' '.repeat(indentationSpaces);
18+
return "".concat(spaces, "(function() {\n").concat(spaces, " function LazyController(context) {\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, " import('").concat(controllerPath, "').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, "})()");
19+
};

dist/webpack/loader.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
1-
"use strict";
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
'use strict';
210

311
var LoaderDependency = require('webpack/lib/dependencies/LoaderDependency');
412

513
var createControllersModule = require('./create-controllers-module');
14+
/**
15+
* Loader that processes the controllers.json file.
16+
*
17+
* This reads the controllers key and returns an object
18+
* where the keys are each controller name and the value
19+
* is the module name (or inline class for lazy controllers)
20+
* for that controller.
21+
*
22+
* @param {string} source controllers.json source
23+
* @return {string}
24+
*/
25+
626

727
module.exports = function (source) {
28+
var _this = this;
29+
830
var logger = this.getLogger('stimulus-bridge-loader');
931
/*
1032
* The following code prevents the normal JSON loader from
@@ -24,5 +46,12 @@ module.exports = function (source) {
2446
this._module.parser = factory.getParser(requiredType);
2547
/* End workaround */
2648

27-
return createControllersModule(JSON.parse(source));
49+
var _createControllersMod = createControllersModule(JSON.parse(source)),
50+
finalSource = _createControllersMod.finalSource,
51+
deprecations = _createControllersMod.deprecations;
52+
53+
deprecations.forEach(function (message) {
54+
_this.emitWarning(new Error(message));
55+
});
56+
return finalSource;
2857
};

src/index.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@ export function startStimulusApp(context) {
2929
}
3030

3131
symfonyControllers[controllerName].then((module) => {
32-
// Normalize the controller name: remove the initial @ and use Stimulus format
33-
controllerName = controllerName.substr(1).replace(/_/g, '-').replace(/\//g, '--');
34-
3532
application.register(controllerName, module.default);
3633
});
3734
}

src/webpack/create-controllers-module.js

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99

1010
'use strict';
1111

12+
const generateLazyController = require('./generate-lazy-controller');
13+
1214
module.exports = function createControllersModule(config) {
1315
let controllerContents = 'export default {';
1416
let autoImportContents = '';
17+
let hasLazyControllers = false;
18+
const deprecations = [];
1519

1620
if ('undefined' !== typeof config['placeholder']) {
1721
throw new Error(
@@ -28,6 +32,8 @@ module.exports = function createControllersModule(config) {
2832

2933
for (let controllerName in config.controllers[packageName]) {
3034
const controllerReference = packageName + '/' + controllerName;
35+
// Normalize the controller name: remove the initial @ and use Stimulus format
36+
const controllerNormalizedName = controllerReference.substr(1).replace(/_/g, '-').replace(/\//g, '--');
3137

3238
// Find package config for the controller
3339
if ('undefined' === typeof packageConfig.symfony.controllers[controllerName]) {
@@ -44,16 +50,35 @@ module.exports = function createControllersModule(config) {
4450
}
4551

4652
const controllerMain = packageName + '/' + controllerPackageConfig.main;
47-
const webpackMode = controllerUserConfig.webpackMode;
53+
let fetchMode = 'eager';
54+
55+
if ('undefined' !== typeof controllerUserConfig.webpackMode) {
56+
deprecations.push(
57+
'The "webpackMode" config key is deprecated in controllers.json. Use "fetch" instead, set to either "eager" or "lazy".'
58+
);
59+
}
60+
61+
if ('undefined' !== typeof controllerUserConfig.fetch) {
62+
if (!['eager', 'lazy'].includes(controllerUserConfig.fetch)) {
63+
throw new Error(
64+
`Invalid "fetch" value "${controllerUserConfig.fetch}" in controllers.json. Expected "eager" or "lazy".`
65+
);
66+
}
4867

49-
controllerContents +=
50-
"\n '" +
51-
controllerReference +
52-
'\': import(/* webpackMode: "' +
53-
webpackMode +
54-
'" */ \'' +
55-
controllerMain +
56-
"'),";
68+
fetchMode = controllerUserConfig.fetch;
69+
}
70+
71+
let moduleValueContents = `import(/* webpackMode: "eager" */ '${controllerMain}')`;
72+
if (fetchMode === 'lazy') {
73+
hasLazyControllers = true;
74+
moduleValueContents = `
75+
new Promise((resolve, reject) => resolve({ default:
76+
${generateLazyController(controllerMain, 6)}
77+
}))
78+
`.trim();
79+
}
80+
81+
controllerContents += `\n '${controllerNormalizedName}': ${moduleValueContents},`;
5782

5883
for (let autoimport in controllerUserConfig.autoimport || []) {
5984
if (controllerUserConfig.autoimport[autoimport]) {
@@ -63,5 +88,12 @@ module.exports = function createControllersModule(config) {
6388
}
6489
}
6590

66-
return `${autoImportContents}${controllerContents}\n};`;
91+
if (hasLazyControllers) {
92+
controllerContents = `import { Controller } from 'stimulus';\n${controllerContents}`;
93+
}
94+
95+
return {
96+
finalSource: `${autoImportContents}${controllerContents}\n};`,
97+
deprecations,
98+
};
6799
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
/**
13+
*
14+
* @param {string} controllerPath The importable path to the controller
15+
* @param {Number} indentationSpaces Amount each line should be indented
16+
*/
17+
module.exports = function generateLazyController(controllerPath, indentationSpaces) {
18+
const spaces = ' '.repeat(indentationSpaces);
19+
20+
return `${spaces}(function() {
21+
${spaces} function LazyController(context) {
22+
${spaces} Controller.call(this, context);
23+
${spaces} }
24+
${spaces} LazyController.prototype = Object.create(Controller && Controller.prototype, {
25+
${spaces} constructor: { value: LazyController, writable: true, configurable: true }
26+
${spaces} });
27+
${spaces} Object.setPrototypeOf(LazyController, Controller);
28+
${spaces} LazyController.prototype.initialize = function() {
29+
${spaces} var _this = this;
30+
${spaces} import('${controllerPath}').then(function(controller) {
31+
${spaces} _this.application.register(_this.identifier, controller.default);
32+
${spaces} });
33+
${spaces} }
34+
${spaces} return LazyController;
35+
${spaces}})()`;
36+
};

src/webpack/loader.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
112
const LoaderDependency = require('webpack/lib/dependencies/LoaderDependency');
213
const createControllersModule = require('./create-controllers-module');
314

15+
/**
16+
* Loader that processes the controllers.json file.
17+
*
18+
* This reads the controllers key and returns an object
19+
* where the keys are each controller name and the value
20+
* is the module name (or inline class for lazy controllers)
21+
* for that controller.
22+
*
23+
* @param {string} source controllers.json source
24+
* @return {string}
25+
*/
426
module.exports = function (source) {
527
const logger = this.getLogger('stimulus-bridge-loader');
628

@@ -18,5 +40,11 @@ module.exports = function (source) {
1840
this._module.parser = factory.getParser(requiredType);
1941
/* End workaround */
2042

21-
return createControllersModule(JSON.parse(source));
43+
const { finalSource, deprecations } = createControllersModule(JSON.parse(source));
44+
45+
deprecations.forEach((message) => {
46+
this.emitWarning(new Error(message));
47+
});
48+
49+
return finalSource;
2250
};

0 commit comments

Comments
 (0)