From 9c6d94ada0f2b401aeb5c41b3dbf31a912a80840 Mon Sep 17 00:00:00 2001
From: Brian Jacobel <bjacobel@gmail.com>
Date: Mon, 8 May 2017 17:07:35 -0400
Subject: [PATCH] Require and Webpack can eat the same files

---
 cms/static/cms/js/main.js                     |   2 +-
 .../js/features/import/factories/import.js    |   2 +-
 .../common/js/components/views/feedback.js    | 357 +++++++++---------
 .../components/views/feedback_notification.js |  56 +--
 .../acceptance/pages/studio/import_export.py  |  18 -
 .../tests/studio/test_import_export.py        |  10 -
 package.json                                  |   2 +
 pavelib/assets.py                             |  18 +-
 scripts/safe_template_linter.py               |   2 +
 webpack.config.js                             |  32 +-
 10 files changed, 256 insertions(+), 243 deletions(-)

diff --git a/cms/static/cms/js/main.js b/cms/static/cms/js/main.js
index 0e1a36911d3..a126bc01687 100644
--- a/cms/static/cms/js/main.js
+++ b/cms/static/cms/js/main.js
@@ -8,7 +8,7 @@
         'underscore.string',
         'backbone',
         'gettext',
-        '../../../../common/static/common/js/components/views/feedback_notification',
+        '../../common/js/components/views/feedback_notification',
         'jquery.cookie'
     ], function(domReady, $, str, Backbone, gettext, NotificationView) {
         var main, sendJSON;
diff --git a/cms/static/js/features/import/factories/import.js b/cms/static/js/features/import/factories/import.js
index 7a5e4f02468..28bee92cec9 100644
--- a/cms/static/js/features/import/factories/import.js
+++ b/cms/static/js/features/import/factories/import.js
@@ -116,7 +116,7 @@ define([
                                         Import.reset();
                                         onComplete();
 
-                                        alert(gettext('Your import has failed.') + '\n\n' + errMsg);
+                                        alert(gettext('Your import has failed.') + '\n\n' + errMsg);  // eslint-disable-line max-len, no-alert
                                     }
                                 }
                             });
diff --git a/common/static/common/js/components/views/feedback.js b/common/static/common/js/components/views/feedback.js
index dce22284834..65dff2bda7b 100644
--- a/common/static/common/js/components/views/feedback.js
+++ b/common/static/common/js/components/views/feedback.js
@@ -1,197 +1,202 @@
-define(['jquery',
+(function(define) {
+    'use strict';
+    define([
+        'jquery',
         'underscore',
         'underscore.string',
         'backbone',
-        'text!../../../../common/templates/components/system-feedback.underscore'],
-    function($, _, str, Backbone, systemFeedbackTemplate) {
-        var tabbable_elements = [
-            "a[href]:not([tabindex='-1'])",
-            "area[href]:not([tabindex='-1'])",
-            "input:not([disabled]):not([tabindex='-1'])",
-            "select:not([disabled]):not([tabindex='-1'])",
-            "textarea:not([disabled]):not([tabindex='-1'])",
-            "button:not([disabled]):not([tabindex='-1'])",
-            "iframe:not([tabindex='-1'])",
-            "[tabindex]:not([tabindex='-1'])",
-            "[contentEditable=true]:not([tabindex='-1'])"
-        ];
-        var SystemFeedback = Backbone.View.extend({
-            options: {
-                title: '',
-                message: '',
-                intent: null,  // "warning", "confirmation", "error", "announcement", "step-required", etc
-                type: null, // "alert", "notification", or "prompt": set by subclass
-                shown: true,  // is this view currently being shown?
-                icon: true,  // should we render an icon related to the message intent?
-                closeIcon: true,  // should we render a close button in the top right corner?
-                minShown: 0,  // length of time after this view has been shown before it can be hidden (milliseconds)
-                maxShown: Infinity,  // length of time after this view has been shown before it will be automatically hidden (milliseconds)
-                outFocusElement: null  // element to send focus to on hide
+        'edx-ui-toolkit/js/utils/html-utils',
+        'text!../../../../common/templates/components/system-feedback.underscore'
+    ],
+        function($, _, str, Backbone, HtmlUtils, systemFeedbackTemplate) {
+            var tabbableElements = [
+                "a[href]:not([tabindex='-1'])",
+                "area[href]:not([tabindex='-1'])",
+                "input:not([disabled]):not([tabindex='-1'])",
+                "select:not([disabled]):not([tabindex='-1'])",
+                "textarea:not([disabled]):not([tabindex='-1'])",
+                "button:not([disabled]):not([tabindex='-1'])",
+                "iframe:not([tabindex='-1'])",
+                "[tabindex]:not([tabindex='-1'])",
+                "[contentEditable=true]:not([tabindex='-1'])"
+            ];
+            var SystemFeedback = Backbone.View.extend({
+                options: {
+                    title: '',
+                    message: '',
+                    intent: null,  // "warning", "confirmation", "error", "announcement", "step-required", etc
+                    type: null, // "alert", "notification", or "prompt": set by subclass
+                    shown: true,  // is this view currently being shown?
+                    icon: true,  // should we render an icon related to the message intent?
+                    closeIcon: true,  // should we render a close button in the top right corner?
+                    minShown: 0,  // ms after this view has been shown before it can be hidden
+                    maxShown: Infinity,  // ms after this view has been shown before it will be automatically hidden
+                    outFocusElement: null  // element to send focus to on hide
 
-            /* Could also have an "actions" hash: here is an example demonstrating
-                the expected structure. For each action, by default the framework
-                will call preventDefault on the click event before the function is
-                run; to make it not do that, just pass `preventDefault: false` in
-                the action object.
+                /* Could also have an "actions" hash: here is an example demonstrating
+                    the expected structure. For each action, by default the framework
+                    will call preventDefault on the click event before the function is
+                    run; to make it not do that, just pass `preventDefault: false` in
+                    the action object.
 
-            actions: {
-                primary: {
-                    "text": "Save",
-                    "class": "action-save",
-                    "click": function(view) {
-                        // do something when Save is clicked
-                    }
-                },
-                secondary: [
-                    {
-                        "text": "Cancel",
-                        "class": "action-cancel",
-                        "click": function(view) {}
-                    }, {
-                        "text": "Discard Changes",
-                        "class": "action-discard",
-                        "click": function(view) {}
-                    }
-                ]
-            }
-            */
-            },
-
-            initialize: function(options) {
-                this.options = _.extend({}, this.options, options);
-                if (!this.options.type) {
-                    throw 'SystemFeedback: type required (given ' +
-                        JSON.stringify(this.options) + ')';
-                }
-                if (!this.options.intent) {
-                    throw 'SystemFeedback: intent required (given ' +
-                        JSON.stringify(this.options) + ')';
-                }
-                this.setElement($('#page-' + this.options.type));
-                // handle single "secondary" action
-                if (this.options.actions && this.options.actions.secondary &&
-                        !_.isArray(this.options.actions.secondary)) {
-                    this.options.actions.secondary = [this.options.actions.secondary];
+                actions: {
+                    primary: {
+                        "text": "Save",
+                        "class": "action-save",
+                        "click": function(view) {
+                            // do something when Save is clicked
+                        }
+                    },
+                    secondary: [
+                        {
+                            "text": "Cancel",
+                            "class": "action-cancel",
+                            "click": function(view) {}
+                        }, {
+                            "text": "Discard Changes",
+                            "class": "action-discard",
+                            "click": function(view) {}
+                        }
+                    ]
                 }
-                return this;
-            },
-
-            inFocus: function(wrapperElementSelector) {
-                var wrapper = wrapperElementSelector || '.wrapper',
-                    tabbables;
-                this.options.outFocusElement = this.options.outFocusElement || document.activeElement;
-
-                // Set focus to the container.
-                this.$(wrapper).first().focus();
+                */
+                },
 
-                // Make tabs within the prompt loop rather than setting focus
-                // back to the main content of the page.
-                tabbables = this.$(tabbable_elements.join());
-                tabbables.on('keydown', function(event) {
-                    // On tab backward from the first tabbable item in the prompt
-                    if (event.which === 9 && event.shiftKey && event.target === tabbables.first()[0]) {
-                        event.preventDefault();
-                        tabbables.last().focus();
+                initialize: function(options) {
+                    this.options = _.extend({}, this.options, options);
+                    if (!this.options.type) {
+                        throw 'SystemFeedback: type required (given ' +  // eslint-disable-line no-throw-literal
+                            JSON.stringify(this.options) + ')';
                     }
-                    // On tab forward from the last tabbable item in the prompt
-                    else if (event.which === 9 && !event.shiftKey && event.target === tabbables.last()[0]) {
-                        event.preventDefault();
-                        tabbables.first().focus();
+                    if (!this.options.intent) {
+                        throw 'SystemFeedback: intent required (given ' +  // eslint-disable-line no-throw-literal
+                            JSON.stringify(this.options) + ')';
                     }
-                });
+                    this.setElement($('#page-' + this.options.type));
+                    // handle single "secondary" action
+                    if (this.options.actions && this.options.actions.secondary &&
+                            !_.isArray(this.options.actions.secondary)) {
+                        this.options.actions.secondary = [this.options.actions.secondary];
+                    }
+                    return this;
+                },
 
-                return this;
-            },
+                inFocus: function(wrapperElementSelector) {
+                    var wrapper = wrapperElementSelector || '.wrapper',
+                        tabbables;
+                    this.options.outFocusElement = this.options.outFocusElement || document.activeElement;
 
-            outFocus: function() {
-                var tabbables = this.$(tabbable_elements.join()).off('keydown');
-                if (this.options.outFocusElement) {
-                    this.options.outFocusElement.focus();
-                }
-                return this;
-            },
+                    // Set focus to the container.
+                    this.$(wrapper).first().focus();
 
-            // public API: show() and hide()
-            show: function() {
-                clearTimeout(this.hideTimeout);
-                this.options.shown = true;
-                this.shownAt = new Date();
-                this.render();
-                if ($.isNumeric(this.options.maxShown)) {
-                    this.hideTimeout = setTimeout(_.bind(this.hide, this),
-                        this.options.maxShown);
-                }
-                return this;
-            },
+                    // Make tabs within the prompt loop rather than setting focus
+                    // back to the main content of the page.
+                    tabbables = this.$(tabbableElements.join());
+                    tabbables.on('keydown', function(event) {
+                        // On tab backward from the first tabbable item in the prompt
+                        if (event.which === 9 && event.shiftKey && event.target === tabbables.first()[0]) {
+                            event.preventDefault();
+                            tabbables.last().focus();
+                        } else if (event.which === 9 && !event.shiftKey && event.target === tabbables.last()[0]) {
+                            // On tab forward from the last tabbable item in the prompt
+                            event.preventDefault();
+                            tabbables.first().focus();
+                        }
+                    });
+
+                    return this;
+                },
 
-            hide: function() {
-                if (this.shownAt && $.isNumeric(this.options.minShown) &&
-                        this.options.minShown > new Date() - this.shownAt) {
+                outFocus: function() {
+                    this.$(tabbableElements.join()).off('keydown');
+                    if (this.options.outFocusElement) {
+                        this.options.outFocusElement.focus();
+                    }
+                    return this;
+                },
+
+                // public API: show() and hide()
+                show: function() {
                     clearTimeout(this.hideTimeout);
-                    this.hideTimeout = setTimeout(_.bind(this.hide, this),
-                        this.options.minShown - (new Date() - this.shownAt));
-                } else {
-                    this.options.shown = false;
-                    delete this.shownAt;
+                    this.options.shown = true;
+                    this.shownAt = new Date();
                     this.render();
-                }
-                return this;
-            },
+                    if ($.isNumeric(this.options.maxShown)) {
+                        this.hideTimeout = setTimeout(_.bind(this.hide, this),
+                            this.options.maxShown);
+                    }
+                    return this;
+                },
 
-            // the rest of the API should be considered semi-private
-            events: {
-                'click .action-close': 'hide',
-                'click .action-primary': 'primaryClick',
-                'click .action-secondary': 'secondaryClick'
-            },
+                hide: function() {
+                    if (this.shownAt && $.isNumeric(this.options.minShown) &&
+                            this.options.minShown > new Date() - this.shownAt) {
+                        clearTimeout(this.hideTimeout);
+                        this.hideTimeout = setTimeout(_.bind(this.hide, this),
+                            this.options.minShown - (new Date() - this.shownAt));
+                    } else {
+                        this.options.shown = false;
+                        delete this.shownAt;
+                        this.render();
+                    }
+                    return this;
+                },
 
-            render: function() {
-                // there can be only one active view of a given type at a time: only
-                // one alert, only one notification, only one prompt. Therefore, we'll
-                // use a singleton approach.
-                var singleton = SystemFeedback['active_' + this.options.type];
-                if (singleton && singleton !== this) {
-                    singleton.stopListening();
-                    singleton.undelegateEvents();
-                }
-                this.$el.html(_.template(systemFeedbackTemplate)(this.options));
-                SystemFeedback['active_' + this.options.type] = this;
-                return this;
-            },
+                // the rest of the API should be considered semi-private
+                events: {
+                    'click .action-close': 'hide',
+                    'click .action-primary': 'primaryClick',
+                    'click .action-secondary': 'secondaryClick'
+                },
 
-            primaryClick: function(event) {
-                var actions, primary;
-                actions = this.options.actions;
-                if (!actions) { return; }
-                primary = actions.primary;
-                if (!primary) { return; }
-                if (primary.preventDefault !== false) {
-                    event.preventDefault();
-                }
-                if (primary.click) {
-                    primary.click.call(event.target, this, event);
-                }
-            },
+                render: function() {
+                    // there can be only one active view of a given type at a time: only
+                    // one alert, only one notification, only one prompt. Therefore, we'll
+                    // use a singleton approach.
+                    var singleton = SystemFeedback['active_' + this.options.type];
+                    if (singleton && singleton !== this) {
+                        singleton.stopListening();
+                        singleton.undelegateEvents();
+                    }
+                    HtmlUtils.setHtml(this.$el, HtmlUtils.template(systemFeedbackTemplate)(this.options));
+                    SystemFeedback['active_' + this.options.type] = this;
+                    return this;
+                },
 
-            secondaryClick: function(event) {
-                var actions, secondaryList, secondary, i;
-                actions = this.options.actions;
-                if (!actions) { return; }
-                secondaryList = actions.secondary;
-                if (!secondaryList) { return; }
-                // which secondary action was clicked?
-                i = 0;  // default to the first secondary action (easier for testing)
-                if (event && event.target) {
-                    i = _.indexOf(this.$('.action-secondary'), event.target);
-                }
-                secondary = secondaryList[i];
-                if (secondary.preventDefault !== false) {
-                    event.preventDefault();
-                }
-                if (secondary.click) {
-                    secondary.click.call(event.target, this, event);
+                primaryClick: function(event) {
+                    var actions, primary;
+                    actions = this.options.actions;
+                    if (!actions) { return; }
+                    primary = actions.primary;
+                    if (!primary) { return; }
+                    if (primary.preventDefault !== false) {
+                        event.preventDefault();
+                    }
+                    if (primary.click) {
+                        primary.click.call(event.target, this, event);
+                    }
+                },
+
+                secondaryClick: function(event) {
+                    var actions, secondaryList, secondary, i;
+                    actions = this.options.actions;
+                    if (!actions) { return; }
+                    secondaryList = actions.secondary;
+                    if (!secondaryList) { return; }
+                    // which secondary action was clicked?
+                    i = 0;  // default to the first secondary action (easier for testing)
+                    if (event && event.target) {
+                        i = _.indexOf(this.$('.action-secondary'), event.target);
+                    }
+                    secondary = secondaryList[i];
+                    if (secondary.preventDefault !== false) {
+                        event.preventDefault();
+                    }
+                    if (secondary.click) {
+                        secondary.click.call(event.target, this, event);
+                    }
                 }
-            }
+            });
+            return SystemFeedback;
         });
-        return SystemFeedback;
-    });
+}).call(this, define || RequireJS.define);
diff --git a/common/static/common/js/components/views/feedback_notification.js b/common/static/common/js/components/views/feedback_notification.js
index 9450a1ce4b8..79ff850ccbf 100644
--- a/common/static/common/js/components/views/feedback_notification.js
+++ b/common/static/common/js/components/views/feedback_notification.js
@@ -1,30 +1,34 @@
-define(['jquery', 'underscore', 'underscore.string', '../../../../common/js/components/views/feedback'],
-    function($, _, str, SystemFeedbackView) {
-        var Notification = SystemFeedbackView.extend({
-            options: $.extend({}, SystemFeedbackView.prototype.options, {
-                type: 'notification',
-                closeIcon: false
-            })
-        });
-
-    // create Notification.Warning, Notification.Confirmation, etc
-        var capitalCamel, intents;
-        capitalCamel = _.compose(str.capitalize, str.camelize);
-        intents = ['warning', 'error', 'confirmation', 'announcement', 'step-required', 'help', 'mini'];
-        _.each(intents, function(intent) {
-            var subclass;
-            subclass = Notification.extend({
-                options: $.extend({}, Notification.prototype.options, {
-                    intent: intent
+(function(define) {
+    'use strict';
+    define(['jquery', 'underscore', 'underscore.string', './feedback'],
+        function($, _, str, SystemFeedbackView) {
+            var Notification = SystemFeedbackView.extend({
+                options: $.extend({}, SystemFeedbackView.prototype.options, {
+                    type: 'notification',
+                    closeIcon: false
                 })
             });
-            Notification[capitalCamel(intent)] = subclass;
-        });
 
-    // set more sensible defaults for Notification.Mini views
-        var miniOptions = Notification.Mini.prototype.options;
-        miniOptions.minShown = 1250;
-        miniOptions.closeIcon = false;
+            // create Notification.Warning, Notification.Confirmation, etc
+            var capitalCamel, intents, miniOptions;
+            capitalCamel = _.compose(str.capitalize, str.camelize);
+            intents = ['warning', 'error', 'confirmation', 'announcement', 'step-required', 'help', 'mini'];
+            _.each(intents, function(intent) {
+                var subclass;
+                subclass = Notification.extend({
+                    options: $.extend({}, Notification.prototype.options, {
+                        intent: intent
+                    })
+                });
+                Notification[capitalCamel(intent)] = subclass;
+            });
+
+            // set more sensible defaults for Notification.Mini views
+            miniOptions = Notification.Mini.prototype.options;
+            miniOptions.minShown = 1250;
+            miniOptions.closeIcon = false;
 
-        return Notification;
-    });
+            return Notification;
+        }
+    );
+}).call(this, define || RequireJS.define);
diff --git a/common/test/acceptance/pages/studio/import_export.py b/common/test/acceptance/pages/studio/import_export.py
index 62e9f3ced9c..d49f66704b4 100644
--- a/common/test/acceptance/pages/studio/import_export.py
+++ b/common/test/acceptance/pages/studio/import_export.py
@@ -253,17 +253,6 @@ class ImportMixin(ImportExportMixin):
         """
         return self.q(css='.choose-file-button').present
 
-    def is_click_handler_registered(self):
-        """
-        Check if the click handler for the file selector button has been registered yet
-        """
-        script = """
-            var $ = require('jquery'),
-                    buttonEvents = $._data($('a.choose-file-button')[0], 'events');
-            return buttonEvents && buttonEvents.hasOwnProperty('click');"""
-        stripped_script = ''.join([line.strip() for line in script.split('\n')])
-        return self.browser.execute_script(stripped_script)
-
     @staticmethod
     def file_path(filename):
         """
@@ -325,13 +314,6 @@ class ImportMixin(ImportExportMixin):
         """
         return self.q(css='#fileupload .error-block').visible
 
-    def wait_for_choose_file_click_handler(self):
-        """
-        Wait for the choose file button click handler to be registered
-        """
-        EmptyPromise(self.is_click_handler_registered, 'Choose File Button Click Handler Registered',
-                     timeout=30).fulfill()
-
     def wait_for_filename_error(self):
         """
         Wait for the upload field to display an error.
diff --git a/common/test/acceptance/tests/studio/test_import_export.py b/common/test/acceptance/tests/studio/test_import_export.py
index de1a4a09c9f..1cfeb2ae302 100644
--- a/common/test/acceptance/tests/studio/test_import_export.py
+++ b/common/test/acceptance/tests/studio/test_import_export.py
@@ -168,7 +168,6 @@ class ImportTestMixin(object):
             I can select the file and upload it
             And the page will give me confirmation that it uploaded successfully
         """
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_upload()
 
@@ -184,7 +183,6 @@ class ImportTestMixin(object):
         # import_page timestamp is in (MM/DD/YYYY at HH:mm) so replacing (second, microsecond) to
         # keep the comparison consistent
         upload_start_time = datetime.utcnow().replace(microsecond=0, second=0)
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_upload()
 
@@ -218,7 +216,6 @@ class ImportTestMixin(object):
             Given that I upload a library or course
             A button will appear that contains the URL to the library or course's main page
         """
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.assertEqual(self.import_page.finished_target_url(), self.landing_page.url)
 
@@ -228,7 +225,6 @@ class ImportTestMixin(object):
             Given that I select a file that is an .mp4 for upload
             An error message will appear
         """
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball('funny_cat_video.mp4')
         self.import_page.wait_for_filename_error()
 
@@ -244,7 +240,6 @@ class ImportTestMixin(object):
         # The task list shouldn't be visible to start.
         self.assertFalse(self.import_page.is_task_list_showing(), "Task list shown too early.")
         self.import_page.wait_for_tasks()
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_tasks(completed=True)
         self.assertTrue(self.import_page.is_task_list_showing(), "Task list did not display.")
@@ -258,7 +253,6 @@ class ImportTestMixin(object):
             And the 'Updating' task should be marked failed
             And the remaining tasks should not be marked as started
         """
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.bad_tarball_name)
         self.import_page.wait_for_tasks(fail_on='Updating')
 
@@ -296,7 +290,6 @@ class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest):
         self.assertRaises(IndexError, self.landing_page.section, "Section")
         self.assertRaises(IndexError, self.landing_page.section, "Entrance Exam")
         self.import_page.visit()
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_upload()
         self.landing_page.visit()
@@ -346,7 +339,6 @@ class TestCourseImport(ImportTestMixin, StudioCourseTest):
         # Should not exist yet.
         self.assertRaises(IndexError, self.landing_page.section, "Section")
         self.import_page.visit()
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_upload()
         self.landing_page.visit()
@@ -373,7 +365,6 @@ class TestCourseImport(ImportTestMixin, StudioCourseTest):
         Then timestamp is not visible
         """
         self.import_page.visit()
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_upload()
         self.assertTrue(self.import_page.is_timestamp_visible())
@@ -419,7 +410,6 @@ class TestLibraryImport(ImportTestMixin, StudioLibraryTest):
         # No items should be in the library to start.
         self.assertEqual(len(self.landing_page.xblocks), 0)
         self.import_page.visit()
-        self.import_page.wait_for_choose_file_click_handler()
         self.import_page.upload_tarball(self.tarball_name)
         self.import_page.wait_for_upload()
         self.landing_page.visit()
diff --git a/package.json b/package.json
index 0ad73a2a70c..832ce7d89bf 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "picturefill": "~3.0.2",
     "raw-loader": "^0.5.1",
     "requirejs": "~2.3.2",
+    "string-replace-webpack-plugin": "^0.1.3",
     "uglify-js": "2.7.0",
     "underscore": "~1.8.3",
     "underscore.string": "~3.3.4",
@@ -32,6 +33,7 @@
     "edx-custom-a11y-rules": "0.1.3",
     "eslint-config-edx": "^2.0.1",
     "eslint-config-edx-es5": "^2.0.0",
+    "eslint-import-resolver-webpack": "^0.8.1",
     "jasmine-core": "^2.4.1",
     "jasmine-jquery": "^2.1.1",
     "karma": "^0.13.22",
diff --git a/pavelib/assets.py b/pavelib/assets.py
index 28bbf2aa4fc..f86cc37b365 100644
--- a/pavelib/assets.py
+++ b/pavelib/assets.py
@@ -708,11 +708,11 @@ def execute_webpack(prod, settings=None):
     sh(
         cmd(
             "NODE_ENV={node_env} STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} $(npm bin)/webpack"
-                .format(
-                    node_env="production" if prod else "development",
-                    static_root_lms=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings),
-                    static_root_cms=Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
-                )
+            .format(
+                node_env="production" if prod else "development",
+                static_root_lms=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings),
+                static_root_cms=Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
+            )
         )
     )
 
@@ -720,10 +720,10 @@ def execute_webpack(prod, settings=None):
 def execute_webpack_watch(settings=None):
     run_background_process(
         "STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} $(npm bin)/webpack --watch --watch-poll=200"
-            .format(
-                static_root_lms=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings),
-                static_root_cms=Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
-            )
+        .format(
+            static_root_lms=Env.get_django_setting("STATIC_ROOT", "lms", settings=settings),
+            static_root_cms=Env.get_django_setting("STATIC_ROOT", "cms", settings=settings)
+        )
     )
 
 
diff --git a/scripts/safe_template_linter.py b/scripts/safe_template_linter.py
index 9a50339278a..b2ebf182aa5 100755
--- a/scripts/safe_template_linter.py
+++ b/scripts/safe_template_linter.py
@@ -2378,6 +2378,8 @@ class MakoTemplateLinter(BaseLinter):
                 </script> |  # script tag end
                 <%static:require_module(_async)?.*?> |  # require js script tag start (optionally the _async version)
                 </%static:require_module(_async)?> | # require js script tag end (optionally the _async version)
+                <%static:webpack.*?> |  # webpack script tag start
+                </%static:webpack> | # webpack script tag end
                 <%block[ ]*name=['"]requirejs['"]\w*> |  # require js tag start
                 </%block>  # require js tag end
             """,
diff --git a/webpack.config.js b/webpack.config.js
index 58770212320..7c33a7defdb 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,9 +5,15 @@
 var path = require('path');
 var webpack = require('webpack');
 var BundleTracker = require('webpack-bundle-tracker');
+var StringReplace = require('string-replace-webpack-plugin');
 
 var isProd = process.env.NODE_ENV === 'production';
 
+var namespacedRequireFiles = [
+    path.resolve(__dirname, 'common/static/common/js/components/views/feedback_notification.js'),
+    path.resolve(__dirname, 'common/static/common/js/components/views/feedback.js')
+];
+
 var wpconfig = {
     context: __dirname,
 
@@ -51,9 +57,30 @@ var wpconfig = {
 
     module: {
         rules: [
+            {
+                test: namespacedRequireFiles,
+                loader: StringReplace.replace(
+                    ['babel-loader'],
+                    {
+                        replacements: [
+                            {
+                                pattern: /\(function ?\(define\) ?\{/,
+                                replacement: function() { return ''; }
+                            },
+                            {
+                                pattern: /\}\)\.call\(this, define \|\| RequireJS\.define\);/,
+                                replacement: function() { return ''; }
+                            }
+                        ]
+                    }
+                )
+            },
             {
                 test: /\.js$/,
-                exclude: /node_modules/,
+                exclude: [
+                    /node_modules/,
+                    namespacedRequireFiles
+                ],
                 use: 'babel-loader'
             },
             {
@@ -72,7 +99,8 @@ var wpconfig = {
                 use: {
                     loader: 'imports-loader',
                     options: {
-                        AjaxPrefix: 'exports-loader?this.AjaxPrefix!../../../../common/static/coffee/src/ajax_prefix.coffee'
+                        AjaxPrefix:
+                            'exports-loader?this.AjaxPrefix!../../../../common/static/coffee/src/ajax_prefix.coffee'
                     }
                 }
             }
-- 
GitLab