Skip to content

Lazy controllers for user-land controllers #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
in `controllers.json` to `lazy`, your controller will not
be downloaded until the controller element first appears on the page.

* The `webpackMode` option in `controllers.json` was deprecated. Use
the new `fetch` option instead.
* Support for making your own controllers "lazy" (as described above)
can now be achieved by loading your controllers through the
`@symfony/stimulus-bridge/lazy-controller-loader` loader and
adding a `/* stimulusFetch: 'lazy' */` comment above your controller.

## 1.1.0

Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,37 @@ ordered from least to most lazy:
separate file and only downloaded asynchronously if (and when) the `data-controller`
HTML appears on the page.

## Lazy Controllers

You can also make your own controllers "lazy": giving them the same behavior
as the `lazy-controller` explained above. In this case, your controller isn't
downloaded until an element for that controller first appears on the page.

To activate this, first make sure that you're using the special loader -
`@symfony/stimulus-bridge/lazy-controller-loader` - when loading your controllers:

```js
// assets/bootstrap.js

export const app = startStimulusApp(require.context(
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
true,
/\.(j|t)sx?$/
));
```

Next, you can make any controllers lazy by adding a `/* stimulusFetch: 'lazy' */`
comment above that controller:

```js
import { Controller } from 'stimulus';

/* stimulusFetch: 'lazy' */
export default class extends Controller {
// ...
}
```

## Run tests

```sh
Expand Down
86 changes: 86 additions & 0 deletions dist/util/get-stimulus-comment-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';

function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

var acorn = require('acorn');

var vm = require('vm');

var stimulusCommentRegExp = new RegExp(/(^|\W)stimulus[A-Z]{1,}[A-Za-z]{1,}:/);
var EMPTY_COMMENT_OPTIONS = {
options: {},
errors: []
};

function getCommentsFromSource(source) {
var comments = [];
acorn.parse(source, {
onComment: comments,
sourceType: 'module',
ecmaVersion: 2020
});
return comments;
}
/**
* Inspired by Webpack's JavaScriptParser
*/


module.exports = function parseComments(source) {
var comments;

try {
comments = getCommentsFromSource(source);
} catch (e) {
return EMPTY_COMMENT_OPTIONS;
}

if (comments.length === 0) {
return EMPTY_COMMENT_OPTIONS;
}

var options = {};
var errors = [];

var _iterator = _createForOfIteratorHelper(comments),
_step;

try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var comment = _step.value;
var value = comment.value;

if (value && stimulusCommentRegExp.test(value)) {
// try compile only if stimulus options comment is present
try {
var val = vm.runInNewContext("(function(){return {".concat(value, "};})()"));
Object.assign(options, val);
} catch (e) {
e.comment = comment;
errors.push(e);
}
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}

return {
options: options,
errors: errors
};
};
66 changes: 66 additions & 0 deletions dist/webpack/lazy-controller-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';

function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

var generateLazyController = require('./generate-lazy-controller');

var getStimulusCommentOptions = require('../util/get-stimulus-comment-options');
/**
* Loader that can make a Stimulus controller lazy.
*
* This loader is meant to be used to load the Stimulus controllers
* themselves. It detects a stimulusFetch: 'lazy' comment above the
* controller. If present, the controller is replaced by a controller
* that will lazily import the real controller the first time the
* element appears.
*
* @param {string} source of a module that exports a Stimulus controller
* @return {string}
*/


module.exports = function (source) {
var _getStimulusCommentOp = getStimulusCommentOptions(source),
options = _getStimulusCommentOp.options,
errors = _getStimulusCommentOp.errors;

var _iterator = _createForOfIteratorHelper(errors),
_step;

try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var error = _step.value;
this.emitError(new Error("Invalid comment found:\n\n \"/* ".concat(error.comment.value.trim(), " */\".\n\nCheck your syntax.")));
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}

var stimulusFetch = typeof options.stimulusFetch !== 'undefined' ? options.stimulusFetch : 'eager';

if (!['eager', 'lazy'].includes(stimulusFetch)) {
this.emitError(new Error("Invalid value \"".concat(stimulusFetch, "\" found for \"stimulusFetch\". Allowed values are \"lazy\" or \"eager\"")));
}

var isLazy = stimulusFetch === 'lazy';

if (!isLazy) {
return source;
}

return "import { Controller } from 'stimulus';\nexport default ".concat(generateLazyController(this.resource, 0));
};
12 changes: 12 additions & 0 deletions lazy-controller-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

module.exports = require('./dist/webpack/lazy-controller-loader');
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"peerDependencies": {
"stimulus": "^2.0"
},
"dependencies": {},
"dependencies": {
"acorn": "^8.0.5"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
Expand All @@ -35,6 +37,7 @@
"files": [
"src/",
"dist/",
"controllers.json"
"controllers.json",
"lazy-controller-loader.js"
]
}
64 changes: 64 additions & 0 deletions src/util/get-stimulus-comment-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const acorn = require('acorn');
const vm = require('vm');

const stimulusCommentRegExp = new RegExp(/(^|\W)stimulus[A-Z]{1,}[A-Za-z]{1,}:/);

const EMPTY_COMMENT_OPTIONS = {
options: {},
errors: [],
};

function getCommentsFromSource(source) {
const comments = [];
acorn.parse(source, {
onComment: comments,
sourceType: 'module',
ecmaVersion: 2020,
});

return comments;
}

/**
* Inspired by Webpack's JavaScriptParser
*/
module.exports = function parseComments(source) {
let comments;
try {
comments = getCommentsFromSource(source);
} catch (e) {
return EMPTY_COMMENT_OPTIONS;
}

if (comments.length === 0) {
return EMPTY_COMMENT_OPTIONS;
}

let options = {};
let errors = [];
for (const comment of comments) {
const { value } = comment;
if (value && stimulusCommentRegExp.test(value)) {
// try compile only if stimulus options comment is present
try {
const val = vm.runInNewContext(`(function(){return {${value}};})()`);
Object.assign(options, val);
} catch (e) {
e.comment = comment;
errors.push(e);
}
}
}
return { options, errors };
};
52 changes: 52 additions & 0 deletions src/webpack/lazy-controller-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

'use strict';

const generateLazyController = require('./generate-lazy-controller');
const getStimulusCommentOptions = require('../util/get-stimulus-comment-options');

/**
* Loader that can make a Stimulus controller lazy.
*
* This loader is meant to be used to load the Stimulus controllers
* themselves. It detects a stimulusFetch: 'lazy' comment above the
* controller. If present, the controller is replaced by a controller
* that will lazily import the real controller the first time the
* element appears.
*
* @param {string} source of a module that exports a Stimulus controller
* @return {string}
*/
module.exports = function (source) {
const { options, errors } = getStimulusCommentOptions(source);

for (const error of errors) {
this.emitError(
new Error(`Invalid comment found:\n\n "/* ${error.comment.value.trim()} */".\n\nCheck your syntax.`)
);
}

const stimulusFetch = typeof options.stimulusFetch !== 'undefined' ? options.stimulusFetch : 'eager';
if (!['eager', 'lazy'].includes(stimulusFetch)) {
this.emitError(
new Error(
`Invalid value "${stimulusFetch}" found for "stimulusFetch". Allowed values are "lazy" or "eager"`
)
);
}
const isLazy = stimulusFetch === 'lazy';

if (!isLazy) {
return source;
}

return `import { Controller } from 'stimulus';
export default ${generateLazyController(this.resource, 0)}`;
};
Loading