decorate `script` directive to prevent re-overriding of templates

by default `<script>` will install to template cache whenever it is processed as a directive.
this decorates it to be no-op if there is already something in the cache with that name,
allowing programmatic overrides of templates configured using `<script id="...">` notation.
diff --git a/ui-modules/blueprint-composer/app/index.js b/ui-modules/blueprint-composer/app/index.js
index 34b3c5b..08019ee 100755
--- a/ui-modules/blueprint-composer/app/index.js
+++ b/ui-modules/blueprint-composer/app/index.js
@@ -69,13 +69,14 @@
 import bottomSheet from "brooklyn-ui-utils/bottom-sheet/bottom-sheet";
 import stackViewer from 'angular-java-stack-viewer';
 import {EntityFamily} from "./components/util/model/entity.model";
+import scriptTagDecorator from 'brooklyn-ui-utils/script-tag-non-overwrite/script-tag-non-overwrite';
 
 angular.module('app', [ngAnimate, ngResource, ngCookies, ngClipboard, uiRouter, 'ui.router.state.events', brCore,
     brServerStatus, brAutoFocus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement,
     brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop,
     customActionDirective, customConfigSuggestionDropdown, paletteApiProvider, paletteServiceProvider, blueprintLoaderApiProvider,
     breadcrumbs, catalogSelector, designer, objectCache, entityFilters, locationFilter, actionService, blueprintService,
-    dslService, paletteDragAndDropService, recentlyUsedService])
+    dslService, paletteDragAndDropService, recentlyUsedService, scriptTagDecorator])
     .provider('composerOverrides', composerOverridesProvider)
     .filter('dslParamLabel', ['$filter', dslParamLabelFilter])
     .config(['$urlRouterProvider', '$stateProvider', '$logProvider', applicationConfig])
diff --git a/ui-modules/utils/script-tag-non-overwrite/script-tag-non-overwrite.js b/ui-modules/utils/script-tag-non-overwrite/script-tag-non-overwrite.js
new file mode 100644
index 0000000..387a0bf
--- /dev/null
+++ b/ui-modules/utils/script-tag-non-overwrite/script-tag-non-overwrite.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import angular from 'angular';
+
+/**
+ * If included, this decorates the default angular `<script>` tag so that it checks the
+ * template cache and does _not_ put the contents of the `script` into the cache if there
+ * is already an element with that ID present.
+ */
+
+const MODULE_NAME = 'brooklyn.components.script-tag-non-overwrite';
+
+angular.module(MODULE_NAME, [])
+    .decorator('scriptDirective', ['$delegate', '$templateCache', scriptTagDirectiveDecorator]);
+
+export default MODULE_NAME;
+
+const BROOKLYN_CONFIG = 'brooklyn.config';
+
+function scriptTagDirectiveDecorator($delegate, $templateCache) {
+    let base = $delegate[0];
+    return [ Object.assign({}, base, { compile: function(el, attr) {
+        let match = $templateCache.get(attr.id);
+        if (!(match === null || typeof match === 'undefined')) {
+            // no-op if this ID is already in the cache (e.g. manually overridden)
+            return function() {};
+        }
+        // otherwise do default behaviour
+        return base.compile(el, attr);
+    } }) ];
+}