From 81429f619319da663d4dc0a993c3933f3910fdf6 Mon Sep 17 00:00:00 2001
From: Tim Zallmann <tzallmann@gitlab.com>
Date: Wed, 6 Feb 2019 12:58:29 +0000
Subject: [PATCH] Reduce Bundle Size by lazy loading markdown-it

---
 .../behaviors/markdown/copy_as_gfm.js         |  33 +++--
 .../behaviors/shortcuts/shortcuts_issuable.js |  42 +++---
 spec/features/markdown/copy_as_gfm_spec.rb    |  17 ++-
 .../javascripts/behaviors/copy_as_gfm_spec.js |  33 ++++-
 .../shortcuts/shortcuts_issuable_spec.js      | 126 +++++++++++++-----
 5 files changed, 177 insertions(+), 74 deletions(-)

diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 947d019c725..52d9f2f0322 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -1,8 +1,5 @@
 import $ from 'jquery';
-import { DOMParser } from 'prosemirror-model';
 import { getSelectedFragment } from '~/lib/utils/common_utils';
-import schema from './schema';
-import markdownSerializer from './serializer';
 
 export class CopyAsGFM {
   constructor() {
@@ -39,9 +36,13 @@ export class CopyAsGFM {
     div.appendChild(el.cloneNode(true));
     const html = div.innerHTML;
 
-    clipboardData.setData('text/plain', el.textContent);
-    clipboardData.setData('text/x-gfm', this.nodeToGFM(el));
-    clipboardData.setData('text/html', html);
+    CopyAsGFM.nodeToGFM(el)
+      .then(res => {
+        clipboardData.setData('text/plain', el.textContent);
+        clipboardData.setData('text/x-gfm', res);
+        clipboardData.setData('text/html', html);
+      })
+      .catch(() => {});
   }
 
   static pasteGFM(e) {
@@ -137,11 +138,21 @@ export class CopyAsGFM {
   }
 
   static nodeToGFM(node) {
-    const wrapEl = document.createElement('div');
-    wrapEl.appendChild(node.cloneNode(true));
-    const doc = DOMParser.fromSchema(schema).parse(wrapEl);
-
-    return markdownSerializer.serialize(doc);
+    return Promise.all([
+      import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
+      import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
+      import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
+    ])
+      .then(([prosemirrorModel, schema, markdownSerializer]) => {
+        const { DOMParser } = prosemirrorModel;
+        const wrapEl = document.createElement('div');
+        wrapEl.appendChild(node.cloneNode(true));
+        const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);
+
+        const res = markdownSerializer.default.serialize(doc);
+        return res;
+      })
+      .catch(() => {});
   }
 }
 
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 0eb067d4963..680f2031409 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -64,26 +64,30 @@ export default class ShortcutsIssuable extends Shortcuts {
     const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
     const blockquoteEl = document.createElement('blockquote');
     blockquoteEl.appendChild(el);
-    const text = CopyAsGFM.nodeToGFM(blockquoteEl);
-
-    if (text.trim() === '') {
-      return false;
-    }
-
-    // If replyField already has some content, add a newline before our quote
-    const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
-    $replyField
-      .val((a, current) => `${current}${separator}${text}\n\n`)
-      .trigger('input')
-      .trigger('change');
-
-    // Trigger autosize
-    const event = document.createEvent('Event');
-    event.initEvent('autosize:update', true, false);
-    $replyField.get(0).dispatchEvent(event);
+    CopyAsGFM.nodeToGFM(blockquoteEl)
+      .then(text => {
+        if (text.trim() === '') {
+          return false;
+        }
+
+        // If replyField already has some content, add a newline before our quote
+        const separator = ($replyField.val().trim() !== '' && '\n\n') || '';
+        $replyField
+          .val((a, current) => `${current}${separator}${text}\n\n`)
+          .trigger('input')
+          .trigger('change');
+
+        // Trigger autosize
+        const event = document.createEvent('Event');
+        event.initEvent('autosize:update', true, false);
+        $replyField.get(0).dispatchEvent(event);
+
+        // Focus the input field
+        $replyField.focus();
 
-    // Focus the input field
-    $replyField.focus();
+        return false;
+      })
+      .catch(() => {});
 
     return false;
   }
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 16754035076..60ddb02da2c 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -843,6 +843,7 @@ describe 'Copy as GFM', :js do
     def verify(selector, gfm, target: nil)
       html = html_for_selector(selector)
       output_gfm = html_to_gfm(html, 'transformCodeSelection', target: target)
+      wait_for_requests
       expect(output_gfm.strip).to eq(gfm.strip)
     end
   end
@@ -861,6 +862,9 @@ describe 'Copy as GFM', :js do
   def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil)
     js = <<~JS
       (function(html) {
+        // Setting it off so the import already starts
+        window.CopyAsGFM.nodeToGFM(document.createElement('div'));
+
         var transformer = window.CopyAsGFM[#{transformer.inspect}];
 
         var node = document.createElement('div');
@@ -875,9 +879,18 @@ describe 'Copy as GFM', :js do
         node = transformer(node, target);
         if (!node) return null;
 
-        return window.CopyAsGFM.nodeToGFM(node);
+
+        window.gfmCopytestRes = null;
+        window.CopyAsGFM.nodeToGFM(node)
+        .then((res) => {
+          window.gfmCopytestRes = res;
+        });
       })("#{escape_javascript(html)}")
     JS
-    page.evaluate_script(js)
+    page.execute_script(js)
+
+    loop until page.evaluate_script('window.gfmCopytestRes !== null')
+
+    page.evaluate_script('window.gfmCopytestRes')
   end
 end
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index 6179a02ce16..ca849f75860 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -1,4 +1,4 @@
-import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
+import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
 
 describe('CopyAsGFM', () => {
   describe('CopyAsGFM.pasteGFM', () => {
@@ -79,27 +79,46 @@ describe('CopyAsGFM', () => {
       return clipboardData;
     };
 
+    beforeAll(done => {
+      initCopyAsGFM();
+
+      // Fake call to nodeToGfm so the import of lazy bundle happened
+      CopyAsGFM.nodeToGFM(document.createElement('div'))
+        .then(() => {
+          done();
+        })
+        .catch(done.fail);
+    });
+
     beforeEach(() => spyOn(clipboardData, 'setData'));
 
     describe('list handling', () => {
-      it('uses correct gfm for unordered lists', () => {
+      it('uses correct gfm for unordered lists', done => {
         const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL');
+
         spyOn(window, 'getSelection').and.returnValue(selection);
         simulateCopy();
 
-        const expectedGFM = '* List Item1\n\n* List Item2';
+        setTimeout(() => {
+          const expectedGFM = '* List Item1\n\n* List Item2';
 
-        expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+          expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+          done();
+        });
       });
 
-      it('uses correct gfm for ordered lists', () => {
+      it('uses correct gfm for ordered lists', done => {
         const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL');
+
         spyOn(window, 'getSelection').and.returnValue(selection);
         simulateCopy();
 
-        const expectedGFM = '1. List Item1\n\n1. List Item2';
+        setTimeout(() => {
+          const expectedGFM = '1. List Item1\n\n1. List Item2';
 
-        expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+          expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+          done();
+        });
       });
     });
   });
diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
index fe827bb1e18..4843a0386b5 100644
--- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -3,17 +3,26 @@
 */
 
 import $ from 'jquery';
-import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm';
+import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
 import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
 
-initCopyAsGFM();
-
 const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
 
 describe('ShortcutsIssuable', function() {
   const fixtureName = 'snippets/show.html.raw';
   preloadFixtures(fixtureName);
 
+  beforeAll(done => {
+    initCopyAsGFM();
+
+    // Fake call to nodeToGfm so the import of lazy bundle happened
+    CopyAsGFM.nodeToGFM(document.createElement('div'))
+      .then(() => {
+        done();
+      })
+      .catch(done.fail);
+  });
+
   beforeEach(() => {
     loadFixtures(fixtureName);
     $('body').append(
@@ -63,17 +72,22 @@ describe('ShortcutsIssuable', function() {
         stubSelection('<p>Selected text.</p>');
       });
 
-      it('leaves existing input intact', () => {
+      it('leaves existing input intact', done => {
         $(FORM_SELECTOR).val('This text was already here.');
 
         expect($(FORM_SELECTOR).val()).toBe('This text was already here.');
 
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe(
+            'This text was already here.\n\n> Selected text.\n\n',
+          );
+          done();
+        });
       });
 
-      it('triggers `input`', () => {
+      it('triggers `input`', done => {
         let triggered = false;
         $(FORM_SELECTOR).on('input', () => {
           triggered = true;
@@ -81,36 +95,48 @@ describe('ShortcutsIssuable', function() {
 
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(triggered).toBe(true);
+        setTimeout(() => {
+          expect(triggered).toBe(true);
+          done();
+        });
       });
 
-      it('triggers `focus`', () => {
+      it('triggers `focus`', done => {
         const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(spy).toHaveBeenCalled();
+        setTimeout(() => {
+          expect(spy).toHaveBeenCalled();
+          done();
+        });
       });
     });
 
     describe('with a one-line selection', () => {
-      it('quotes the selection', () => {
+      it('quotes the selection', done => {
         stubSelection('<p>This text has been selected.</p>');
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe('> This text has been selected.\n\n');
+          done();
+        });
       });
     });
 
     describe('with a multi-line selection', () => {
-      it('quotes the selected lines as a group', () => {
+      it('quotes the selected lines as a group', done => {
         stubSelection(
           '<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>',
         );
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe(
-          '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
-        );
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe(
+            '> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n',
+          );
+          done();
+        });
       });
     });
 
@@ -119,17 +145,23 @@ describe('ShortcutsIssuable', function() {
         stubSelection('<p>Selected text.</p>', true);
       });
 
-      it('does not add anything to the input', () => {
+      it('does not add anything to the input', done => {
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe('');
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe('');
+          done();
+        });
       });
 
-      it('triggers `focus`', () => {
+      it('triggers `focus`', done => {
         const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(spy).toHaveBeenCalled();
+        setTimeout(() => {
+          expect(spy).toHaveBeenCalled();
+          done();
+        });
       });
     });
 
@@ -138,20 +170,26 @@ describe('ShortcutsIssuable', function() {
         stubSelection('<div class="md">Selected text.</div><p>Invalid selected text.</p>', true);
       });
 
-      it('only adds the valid part to the input', () => {
+      it('only adds the valid part to the input', done => {
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe('> Selected text.\n\n');
+          done();
+        });
       });
 
-      it('triggers `focus`', () => {
+      it('triggers `focus`', done => {
         const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(spy).toHaveBeenCalled();
+        setTimeout(() => {
+          expect(spy).toHaveBeenCalled();
+          done();
+        });
       });
 
-      it('triggers `input`', () => {
+      it('triggers `input`', done => {
         let triggered = false;
         $(FORM_SELECTOR).on('input', () => {
           triggered = true;
@@ -159,7 +197,10 @@ describe('ShortcutsIssuable', function() {
 
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(triggered).toBe(true);
+        setTimeout(() => {
+          expect(triggered).toBe(true);
+          done();
+        });
       });
     });
 
@@ -183,20 +224,26 @@ describe('ShortcutsIssuable', function() {
         });
       });
 
-      it('adds the quoted selection to the input', () => {
+      it('adds the quoted selection to the input', done => {
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n');
+          done();
+        });
       });
 
-      it('triggers `focus`', () => {
+      it('triggers `focus`', done => {
         const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(spy).toHaveBeenCalled();
+        setTimeout(() => {
+          expect(spy).toHaveBeenCalled();
+          done();
+        });
       });
 
-      it('triggers `input`', () => {
+      it('triggers `input`', done => {
         let triggered = false;
         $(FORM_SELECTOR).on('input', () => {
           triggered = true;
@@ -204,7 +251,10 @@ describe('ShortcutsIssuable', function() {
 
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(triggered).toBe(true);
+        setTimeout(() => {
+          expect(triggered).toBe(true);
+          done();
+        });
       });
     });
 
@@ -228,17 +278,23 @@ describe('ShortcutsIssuable', function() {
         });
       });
 
-      it('does not add anything to the input', () => {
+      it('does not add anything to the input', done => {
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect($(FORM_SELECTOR).val()).toBe('');
+        setTimeout(() => {
+          expect($(FORM_SELECTOR).val()).toBe('');
+          done();
+        });
       });
 
-      it('triggers `focus`', () => {
+      it('triggers `focus`', done => {
         const spy = spyOn(document.querySelector(FORM_SELECTOR), 'focus');
         ShortcutsIssuable.replyWithSelectedText(true);
 
-        expect(spy).toHaveBeenCalled();
+        setTimeout(() => {
+          expect(spy).toHaveBeenCalled();
+          done();
+        });
       });
     });
   });
-- 
2.30.9