Wrap all plugin's JS assets into same IIFE

As discussed in #2200.

For this to work we've introduced sub-bundles per plugin for the JS
assets. CSS and LESS on the other hand are basically handled as before.
We use a custom bundle class here since it's apparently not possible to
keep a sub bundle from inheriting the parent's filters, thus not
allowing us something like

  packed_plugins [js_plugin_delimiter_bundler]
    packed_plugin_a [js_delimiter_bundler]
    packed_plugin_b [js_delimiter_bundler]
    ...

The individiual bundles per plugin would inherit
js_plugin_delimiter_bundler and we'd get a double wrapper - not helpful.

Instead we now have a modified JsPluginBundle that overwrites
Bundle._merge_and_apply to further wrap the returned hunk.
This commit is contained in:
Gina Häußge 2017-11-27 16:18:21 +01:00
parent ca7aa322c3
commit 328c08b775
3 changed files with 140 additions and 56 deletions

View file

@ -1093,12 +1093,14 @@ class Server(object):
self._logger.debug("Deleting {path}...".format(**locals()))
shutil.rmtree(path)
except:
self._logger.exception("Error while trying to delete {path}, leaving it alone".format(**locals()))
self._logger.exception("Error while trying to delete {path}, "
"leaving it alone".format(**locals()))
continue
# re-create path
self._logger.debug("Creating {path}...".format(**locals()))
error_text = "Error while trying to re-create {path}, that might cause errors with the webassets cache".format(**locals())
error_text = "Error while trying to re-create {path}, that might cause " \
"errors with the webassets cache".format(**locals())
try:
os.makedirs(path)
except OSError as e:
@ -1108,13 +1110,16 @@ class Server(object):
import time
for n in range(3):
time.sleep(0.5)
self._logger.debug("Creating {path}: Retry #{retry} after {time}s".format(path=path, retry=n+1, time=(n + 1)*0.5))
self._logger.debug("Creating {path}: Retry #{retry} after {time}s".format(path=path,
retry=n+1,
time=(n + 1)*0.5))
try:
os.makedirs(path)
break
except:
if self._logger.isEnabledFor(logging.DEBUG):
self._logger.exception("Ignored error while creating directory {path}".format(**locals()))
self._logger.exception("Ignored error while creating "
"directory {path}".format(**locals()))
pass
else:
# this will only get executed if we never did
@ -1134,9 +1139,9 @@ class Server(object):
self._logger.info("Reset webasset folder {path}...".format(**locals()))
AdjustedEnvironment = type(Environment)(Environment.__name__, (Environment,), dict(
resolver_class=util.flask.PluginAssetResolver
))
AdjustedEnvironment = type(Environment)(Environment.__name__,
(Environment,),
dict(resolver_class=util.flask.PluginAssetResolver))
class CustomDirectoryEnvironment(AdjustedEnvironment):
@property
def directory(self):
@ -1145,9 +1150,9 @@ class Server(object):
assets = CustomDirectoryEnvironment(app)
assets.debug = not self._settings.getBoolean(["devel", "webassets", "bundle"])
UpdaterType = type(util.flask.SettingsCheckUpdater)(util.flask.SettingsCheckUpdater.__name__, (util.flask.SettingsCheckUpdater,), dict(
updater=assets.updater
))
UpdaterType = type(util.flask.SettingsCheckUpdater)(util.flask.SettingsCheckUpdater.__name__,
(util.flask.SettingsCheckUpdater,),
dict(updater=assets.updater))
assets.updater = UpdaterType
enable_gcodeviewer = self._settings.getBoolean(["gcodeViewer", "enabled"])
@ -1223,12 +1228,6 @@ class Server(object):
"js/app/client/util.js",
"js/app/client/wizard.js"
]
js_core = dynamic_core_assets["js"] + \
dynamic_plugin_assets["bundled"]["js"] + \
["js/app/dataupdater.js",
"js/app/helpers.js",
"js/app/main.js"]
js_plugins = dynamic_plugin_assets["external"]["js"]
css_libs = [
"css/bootstrap.min.css",
@ -1242,80 +1241,143 @@ class Server(object):
"css/pnotify.buttons.min.css",
"css/pnotify.history.min.css"
]
css_core = list(dynamic_core_assets["css"]) + list(dynamic_plugin_assets["bundled"]["css"])
css_plugins = list(dynamic_plugin_assets["external"]["css"])
less_core = list(dynamic_core_assets["less"]) + list(dynamic_plugin_assets["bundled"]["less"])
less_plugins = list(dynamic_plugin_assets["external"]["less"])
# a couple of custom filters
from octoprint.server.util.webassets import LessImportRewrite, JsDelimiterBundler, JsPluginDelimiterBundler, \
SourceMapRewrite, SourceMapRemove
from octoprint.server.util.webassets import LessImportRewrite, JsDelimiterBundler, \
SourceMapRewrite, SourceMapRemove, JsPluginBundle
from webassets.filter import register_filter
register_filter(LessImportRewrite)
register_filter(SourceMapRewrite)
register_filter(SourceMapRemove)
register_filter(JsDelimiterBundler)
register_filter(JsPluginDelimiterBundler)
# JS
def all_assets_for_plugins(collection):
"""Gets all plugin assets for a dict of plugin->assets"""
result = []
for assets in collection.values():
result += assets
return result
# -- JS --------------------------------------------------------------------------------------------------------
js_filters = ["sourcemap_remove", "js_delimiter_bundler"]
js_plugin_filters = ["sourcemap_remove", "js_delimiter_bundler"]
if self._settings.getBoolean(["feature", "legacyPluginAssets"]):
# TODO remove again in 1.3.8
js_plugin_filters = ["sourcemap_remove", "js_delimiter_bundler"]
def js_bundles_for_plugins(collection, filters=None):
"""Produces Bundle instances"""
result = dict()
for plugin, assets in collection.items():
if len(assets):
result[plugin] = Bundle(*assets, filters=filters)
return result
else:
js_plugin_filters = ["sourcemap_remove", "js_plugin_delimiter_bundler"]
def js_bundles_for_plugins(collection, filters=None):
"""Produces JsPluginBundle instances that output IIFE wrapped assets"""
result = dict()
for plugin, assets in collection.items():
if len(assets):
result[plugin] = JsPluginBundle(plugin, *assets, filters=filters)
return result
js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js", filters=",".join(js_filters))
js_client_bundle = Bundle(*js_client, output="webassets/packed_client.js", filters=",".join(js_filters))
js_core_bundle = Bundle(*js_core, output="webassets/packed_core.js", filters=",".join(js_filters))
js_core = dynamic_core_assets["js"] + \
all_assets_for_plugins(dynamic_plugin_assets["bundled"]["js"]) + \
["js/app/dataupdater.js",
"js/app/helpers.js",
"js/app/main.js"]
js_plugins = js_bundles_for_plugins(dynamic_plugin_assets["external"]["js"],
filters="js_delimiter_bundler")
js_libs_bundle = Bundle(*js_libs,
output="webassets/packed_libs.js",
filters=",".join(js_filters))
js_client_bundle = Bundle(*js_client,
output="webassets/packed_client.js",
filters=",".join(js_filters))
js_core_bundle = Bundle(*js_core,
output="webassets/packed_core.js",
filters=",".join(js_filters))
if len(js_plugins) == 0:
js_plugins_bundle = Bundle(*[])
else:
js_plugins_bundle = Bundle(*js_plugins, output="webassets/packed_plugins.js", filters=",".join(js_plugin_filters))
js_plugins_bundle = Bundle(*js_plugins.values(),
output="webassets/packed_plugins.js",
filters=",".join(js_plugin_filters))
js_app_bundle = Bundle(js_plugins_bundle, js_core_bundle, output="webassets/packed_app.js", filters=",".join(js_filters))
js_app_bundle = Bundle(js_plugins_bundle, js_core_bundle,
output="webassets/packed_app.js",
filters=",".join(js_filters))
# -- CSS -------------------------------------------------------------------------------------------------------
# CSS
css_filters = ["cssrewrite"]
css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css", filters=",".join(css_filters))
css_core = list(dynamic_core_assets["css"]) \
+ all_assets_for_plugins(dynamic_plugin_assets["bundled"]["css"])
css_plugins = list(all_assets_for_plugins(dynamic_plugin_assets["external"]["css"]))
css_libs_bundle = Bundle(*css_libs,
output="webassets/packed_libs.css",
filters=",".join(css_filters))
if len(css_core) == 0:
css_core_bundle = Bundle(*[])
else:
css_core_bundle = Bundle(*css_core, output="webassets/packed_core.css", filters=",".join(css_filters))
css_core_bundle = Bundle(*css_core,
output="webassets/packed_core.css",
filters=",".join(css_filters))
if len(css_plugins) == 0:
css_plugins_bundle = Bundle(*[])
else:
css_plugins_bundle = Bundle(*css_plugins, output="webassets/packed_plugins.css", filters=",".join(css_filters))
css_plugins_bundle = Bundle(*css_plugins,
output="webassets/packed_plugins.css",
filters=",".join(css_filters))
css_app_bundle = Bundle(css_core, css_plugins, output="webassets/packed_app.css", filters=",".join(css_filters))
css_app_bundle = Bundle(css_core, css_plugins,
output="webassets/packed_app.css",
filters=",".join(css_filters))
# -- LESS ------------------------------------------------------------------------------------------------------
# LESS
less_filters = ["cssrewrite", "less_importrewrite"]
less_core = list(dynamic_core_assets["less"]) \
+ all_assets_for_plugins(dynamic_plugin_assets["bundled"]["less"])
less_plugins = all_assets_for_plugins(dynamic_plugin_assets["external"]["less"])
if len(less_core) == 0:
less_core_bundle = Bundle(*[])
else:
less_core_bundle = Bundle(*less_core, output="webassets/packed_core.less", filters=",".join(less_filters))
less_core_bundle = Bundle(*less_core,
output="webassets/packed_core.less",
filters=",".join(less_filters))
if len(less_plugins) == 0:
less_plugins_bundle = Bundle(*[])
else:
less_plugins_bundle = Bundle(*less_plugins, output="webassets/packed_plugins.less", filters=",".join(less_filters))
less_plugins_bundle = Bundle(*less_plugins,
output="webassets/packed_plugins.less",
filters=",".join(less_filters))
less_app_bundle = Bundle(less_core, less_plugins, output="webassets/packed_app.less", filters=",".join(less_filters))
less_app_bundle = Bundle(less_core, less_plugins,
output="webassets/packed_app.less",
filters=",".join(less_filters))
# -- asset registration ----------------------------------------------------------------------------------------
# asset registration
assets.register("js_libs", js_libs_bundle)
assets.register("js_client", js_client_bundle)
assets.register("js_core", js_core_bundle)
for plugin, bundle in js_plugins.items():
# register our collected plugin bundles so that they are bound to the environment
assets.register("js_plugin_{}".format(plugin), bundle)
assets.register("js_plugins", js_plugins_bundle)
assets.register("js_app", js_app_bundle)
assets.register("css_libs", css_libs_bundle)

View file

@ -21,6 +21,7 @@ import threading
import logging
import netaddr
import os
import collections
from octoprint.settings import settings
import octoprint.server
@ -1347,8 +1348,12 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
logger = logging.getLogger(__name__ + ".collect_plugin_assets")
supported_stylesheets = ("css", "less")
assets = dict(bundled=dict(js=[], css=[], less=[]),
external=dict(js=[], css=[], less=[]))
assets = dict(bundled=dict(js=collections.defaultdict(list),
css=collections.defaultdict(list),
less=collections.defaultdict(list)),
external=dict(js=collections.defaultdict(list),
css=collections.defaultdict(list),
less=collections.defaultdict(list)))
asset_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.AssetPlugin)
for implementation in asset_plugins:
@ -1374,13 +1379,13 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
for asset in all_assets["js"]:
if not asset_exists("js", asset):
continue
assets[asset_key]["js"].append('plugin/{name}/{asset}'.format(**locals()))
assets[asset_key]["js"][name].append('plugin/{name}/{asset}'.format(**locals()))
if preferred_stylesheet in all_assets:
for asset in all_assets[preferred_stylesheet]:
if not asset_exists(preferred_stylesheet, asset):
continue
assets[asset_key][preferred_stylesheet].append('plugin/{name}/{asset}'.format(**locals()))
assets[asset_key][preferred_stylesheet][name].append('plugin/{name}/{asset}'.format(**locals()))
else:
for stylesheet in supported_stylesheets:
if not stylesheet in all_assets:
@ -1389,7 +1394,7 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"):
for asset in all_assets[stylesheet]:
if not asset_exists(stylesheet, asset):
continue
assets[asset_key][stylesheet].append('plugin/{name}/{asset}'.format(**locals()))
assets[asset_key][stylesheet][name].append('plugin/{name}/{asset}'.format(**locals()))
break
return assets

View file

@ -12,6 +12,8 @@ try:
except:
import urlparse
from webassets.bundle import Bundle
from webassets.merge import MemoryHunk
from webassets.filter import Filter
from webassets.filter.cssrewrite.base import PatternRewriter
import webassets.filter.cssrewrite.urlpath as urlpath
@ -103,14 +105,29 @@ class JsDelimiterBundler(Filter):
out.write("\n;\n")
class JsPluginDelimiterBundler(Filter):
name = "js_plugin_delimiter_bundler"
options = {}
_PLUGIN_BUNDLE_WRAPPER = \
u"""// JS assets for plugin {plugin}
(function () {{
try {{
{contents}
}} catch (error) {{
log.error("Error in JS assets for plugin {plugin}:", (error.stack || error));
}}
}})();
"""
def input(self, _in, out, **kwargs):
source = kwargs.get("source", "n/a")
class JsPluginBundle(Bundle):
def __init__(self, plugin, *args, **kwargs):
Bundle.__init__(self, *args, **kwargs)
self.plugin = plugin
out.write("// source: " + source + "\n")
out.write("(function () {\n try {\n ")
out.write(_in.read().replace('\n', '\n '))
out.write("\n } catch (error) {\n log.error(\"Error in bundled asset " + source + ":\", (error.stack || error));\n }\n})();\n")
def _merge_and_apply(self, ctx, output, force, parent_debug=None,
parent_filters=None, extra_filters=None,
disable_cache=None):
hunk = Bundle._merge_and_apply(self, ctx, output, force, parent_debug=parent_debug,
parent_filters=parent_filters, extra_filters=extra_filters,
disable_cache=disable_cache)
# TODO find a solution that eats less memory - maybe a ChainedMemoryHunk instead?
return MemoryHunk(_PLUGIN_BUNDLE_WRAPPER.format(contents=hunk.data().replace("\n", "\n "),
plugin=self.plugin))