Repository: buxlabs/boxwood
Pull request #636 (2020-04-28 20:18:34)

Improve scoped scripts


Patch:
diff --git a/src/Renderer.js b/src/Renderer.js
index cdc1754..90a2c2c 100644
--- a/src/Renderer.js
+++ b/src/Renderer.js
@@ -106,10 +106,21 @@ class Renderer {
       if (style) {
         tree.append(getTemplateAssignmentExpression(TEMPLATE_VARIABLE, getLiteral(`<style>${style}</style>`)))
       }
-      const script = unique(scripts).join(' ')
-      if (script) {
-        tree.append(getTemplateAssignmentExpression(TEMPLATE_VARIABLE, getLiteral(`<script>${script}</script>`)))
+      // TODO check if this is slow, unique on objects
+      if (scripts.length > 0) {
+        tree.append(getTemplateAssignmentExpression(TEMPLATE_VARIABLE, getLiteral('<script>')))
+        unique(scripts).forEach(script => {
+          if (typeof script === 'string') {
+            tree.append(getTemplateAssignmentExpression(TEMPLATE_VARIABLE, getLiteral(script)))
+          } else if (Array.isArray(script)) {
+            script.forEach(node => {
+              tree.append(getTemplateAssignmentExpression(TEMPLATE_VARIABLE, node))
+            })
+          }
+        })
+        tree.append(getTemplateAssignmentExpression(TEMPLATE_VARIABLE, getLiteral('</script>')))
       }
+
       const used = []
       unique(filters).forEach(name => {
         const filter = getFilter(name, translations, options)
diff --git a/src/tags/script.js b/src/tags/script.js
index 043f2e5..d5a5772 100644
--- a/src/tags/script.js
+++ b/src/tags/script.js
@@ -9,6 +9,7 @@ const { findAsset } = require('../utilities/files')
 const { isCurlyTag } = require('../utilities/string')
 const { convertAttribute } = require('../utilities/convert')
 let asyncCounter = 0
+let scopeCounter = 0
 
 module.exports = async function ({ tree, keys, attrs, fragment, assets, variables, promises, warnings, filters, translations, languages, append, scripts, options }) {
   if (keys.includes('inline') || options.inline.includes('scripts')) {
@@ -49,10 +50,13 @@ module.exports = async function ({ tree, keys, attrs, fragment, assets, variable
     const scope = new AbstractSyntaxTree(leaf.content)
     const properties = getScopeProperties(scope)
     leaf.used = true
-    tree.append(getTemplateAssignmentExpression(options.variables.template, getLiteral('<script>')))
+    const script = []
+    let scopeName = ''
     if (properties.length > 0) {
-      tree.append(getTemplateAssignmentExpression(options.variables.template, getLiteral('const scope = ')))
-      tree.append(getTemplateAssignmentExpression(options.variables.template, {
+      scopeCounter++
+      scopeName = `__scope_${scopeCounter}__`
+      script.push(getLiteral(`const ${scopeName} = `))
+      script.push({
         type: 'CallExpression',
         callee: {
           type: 'MemberExpression',
@@ -72,25 +76,29 @@ module.exports = async function ({ tree, keys, attrs, fragment, assets, variable
             properties
           }
         ]
-      }))
-      tree.append(getTemplateAssignmentExpression(options.variables.template, getLiteral(';')))
+      })
+      script.push(getLiteral(';'))
     }
     asyncCounter += 1
-    const ASYNC_PLACEHOLDER_TEXT = `ASYNC_PLACEHOLDER_${asyncCounter}`
-    tree.append(getLiteral(ASYNC_PLACEHOLDER_TEXT))
     const bundler = new Bundler()
     const promise = bundler.bundle(leaf.content, { paths: options.script.paths, resolve: options.script.resolve })
     promises.push(promise)
     const result = await promise
-    tree.walk((node, parent) => {
-      if (node.type === 'Literal' && node.value === ASYNC_PLACEHOLDER_TEXT) {
-        const index = parent.body.findIndex(element => {
-          return element.type === 'Literal' && node.value === ASYNC_PLACEHOLDER_TEXT
-        })
-        parent.body.splice(index, 1)
-        parent.body.splice(index + 0, 0, getTemplateAssignmentExpression(options.variables.template, getLiteral(`\n${result}</script>`)))
-      }
-    })
+    let output = result
+    if (scopeName) {
+      const newTree = new AbstractSyntaxTree(result)
+      newTree.walk(node => {
+        // TODO do not replace scope inside of member expressions, e.g.
+        // foo.scope
+        // foo.bar.scope
+        if (node.type === 'Identifier' && node.name === 'scope') {
+          node.name = scopeName
+        }
+      })
+      output = newTree.source
+    }
+    script.push(getLiteral(`\n${output}`))
+    scripts.push(script)
   } else if (keys.includes('compiler')) {
     const { value } = attrs.find(attr => attr.key === 'compiler')
     const compiler = options.compilers[value]


Test patch:
diff --git a/test/fixtures/acceptance/event-listeners/expected.html b/test/fixtures/acceptance/event-listeners/expected.html
index 1a590ac..93e35f3 100644
--- a/test/fixtures/acceptance/event-listeners/expected.html
+++ b/test/fixtures/acceptance/event-listeners/expected.html
@@ -2,13 +2,13 @@
   <a id="submit">Submit</a>
 </main>
 <script>
-const scope = { "language": "en" };
+const __scope_1__ = { "language": "en" };
 (function () {
-  'use strict';
-  const { language } = scope;
+  "use strict";
+  const { language } = __scope_1__;
   const node = document.getElementById("submit");
   node.addEventListener("click", () => {
     console.log(language);
   });
-}());
+})();
 </script>
diff --git a/test/spec/tags/script/scoped.spec.js b/test/spec/tags/script/scoped.spec.js
index 231c49b..52d623f 100644
--- a/test/spec/tags/script/scoped.spec.js
+++ b/test/spec/tags/script/scoped.spec.js
@@ -10,12 +10,12 @@ test('script[scoped]: returns empty string when script does not have a body', as
 
 test('script[scoped]: access to passed variables in script tag through object destructuring assignment', async assert => {
   const { template } = await compile('<script scoped>const { foo } = scope;\nconsole.log(foo);</script>')
-  assert.truthy(template({ foo: 2 }, escape).includes('const scope = {"foo":2}'))
+  assert.truthy(template({ foo: 2 }, escape).includes('{"foo":2}'))
 })
 
 test('script[scoped]: access to passed variables through scope variable', async assert => {
   const { template } = await compile('<script scoped>console.log(scope.foo);</script>')
-  assert.truthy(template({ foo: 2, bar: 'bar' }, escape).includes('const scope = {"foo":2}'))
+  assert.truthy(template({ foo: 2, bar: 'bar' }, escape).includes('{"foo":2}'))
 })
 
 test('script[scoped]: only used properties from scope variable should be inlined', async assert => {
@@ -24,7 +24,7 @@ test('script[scoped]: only used properties from scope variable should be inlined
     foo: 2, 
     bar: 'bar',
     ban: [{ one: 1, two: 2, three: 3, four: 4 }] 
-  }, escape).includes('const scope = {"ban":[{"one":1,"two":2,"three":3,"four":4}]}'))
+  }, escape).includes('{"ban":[{"one":1,"two":2,"three":3,"four":4}]}'))
 })
 
 test('script[scoped]: import local dependencies', async assert => {