]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
build: read & dynamically resolve `imports` on plugins build (#34509)
authorGeoSot <geo.sotis@gmail.com>
Tue, 23 Nov 2021 15:37:14 +0000 (17:37 +0200)
committerGitHub <noreply@github.com>
Tue, 23 Nov 2021 15:37:14 +0000 (17:37 +0200)
Our individual js/dist files are now deduplicated properly thus resulting in a size reduction, which varies from ~25% to ~60% depending on the components used. The average savings are 20% uncompressed and ~15% with gzip.

This will mostly benefit cases that more than one component is imported from js/dist. In all other cases it doesn't have any effect.

Co-authored-by: XhmikosR <xhmikosr@gmail.com>
build/build-plugins.js

index 2e16e4f03b815db769553fe67d5ba80e050aa970..0443447436e4ccccda98468cefeaec17f1662b3b 100644 (file)
 
 const path = require('path')
 const rollup = require('rollup')
+const glob = require('glob')
 const { babel } = require('@rollup/plugin-babel')
 const banner = require('./banner.js')
 
-const rootPath = path.resolve(__dirname, '../js/dist/')
-const plugins = [
-  babel({
-    // Only transpile our source code
-    exclude: 'node_modules/**',
-    // Include the helpers in each file, at most one copy of each
-    babelHelpers: 'bundled'
-  })
-]
-const bsPlugins = {
-  Data: path.resolve(__dirname, '../js/src/dom/data.js'),
-  EventHandler: path.resolve(__dirname, '../js/src/dom/event-handler.js'),
-  Manipulator: path.resolve(__dirname, '../js/src/dom/manipulator.js'),
-  SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'),
-  Alert: path.resolve(__dirname, '../js/src/alert.js'),
-  Base: path.resolve(__dirname, '../js/src/base-component.js'),
-  Button: path.resolve(__dirname, '../js/src/button.js'),
-  Carousel: path.resolve(__dirname, '../js/src/carousel.js'),
-  Collapse: path.resolve(__dirname, '../js/src/collapse.js'),
-  Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'),
-  Modal: path.resolve(__dirname, '../js/src/modal.js'),
-  Offcanvas: path.resolve(__dirname, '../js/src/offcanvas.js'),
-  Popover: path.resolve(__dirname, '../js/src/popover.js'),
-  ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'),
-  Tab: path.resolve(__dirname, '../js/src/tab.js'),
-  Toast: path.resolve(__dirname, '../js/src/toast.js'),
-  Tooltip: path.resolve(__dirname, '../js/src/tooltip.js')
-}
-
-const defaultPluginConfig = {
-  external: [
-    bsPlugins.Data,
-    bsPlugins.Base,
-    bsPlugins.EventHandler,
-    bsPlugins.SelectorEngine
-  ],
-  globals: {
-    [bsPlugins.Data]: 'Data',
-    [bsPlugins.Base]: 'Base',
-    [bsPlugins.EventHandler]: 'EventHandler',
-    [bsPlugins.SelectorEngine]: 'SelectorEngine'
-  }
-}
+const srcPath = path.resolve(__dirname, '../js/src/')
+const jsFiles = glob.sync(srcPath + '/**/*.js')
 
-const getConfigByPluginKey = pluginKey => {
-  switch (pluginKey) {
-    case 'Alert':
-    case 'Offcanvas':
-    case 'Tab':
-      return defaultPluginConfig
-
-    case 'Base':
-    case 'Button':
-    case 'Carousel':
-    case 'Collapse':
-    case 'Modal':
-    case 'ScrollSpy': {
-      const config = Object.assign(defaultPluginConfig)
-      config.external.push(bsPlugins.Manipulator)
-      config.globals[bsPlugins.Manipulator] = 'Manipulator'
-      return config
-    }
+// Array which holds the resolved plugins
+const resolvedPlugins = []
 
-    case 'Dropdown':
-    case 'Tooltip': {
-      const config = Object.assign(defaultPluginConfig)
-      config.external.push(bsPlugins.Manipulator, '@popperjs/core')
-      config.globals[bsPlugins.Manipulator] = 'Manipulator'
-      config.globals['@popperjs/core'] = 'Popper'
-      return config
-    }
+// Trims the "js" extension and uppercases => first letter, hyphens, backslashes & slashes
+const filenameToEntity = filename => filename.replace('.js', '')
+  .replace(/(?:^|-|\/|\\)[a-z]/g, str => str.slice(-1).toUpperCase())
 
-    case 'Popover':
-      return {
-        external: [
-          bsPlugins.Data,
-          bsPlugins.SelectorEngine,
-          bsPlugins.Tooltip
-        ],
-        globals: {
-          [bsPlugins.Data]: 'Data',
-          [bsPlugins.SelectorEngine]: 'SelectorEngine',
-          [bsPlugins.Tooltip]: 'Tooltip'
-        }
-      }
-
-    case 'Toast':
-      return {
-        external: [
-          bsPlugins.Data,
-          bsPlugins.Base,
-          bsPlugins.EventHandler,
-          bsPlugins.Manipulator
-        ],
-        globals: {
-          [bsPlugins.Data]: 'Data',
-          [bsPlugins.Base]: 'Base',
-          [bsPlugins.EventHandler]: 'EventHandler',
-          [bsPlugins.Manipulator]: 'Manipulator'
-        }
-      }
-
-    default:
-      return {
-        external: []
-      }
-  }
+for (const file of jsFiles) {
+  resolvedPlugins.push({
+    src: file.replace('.js', ''),
+    dist: file.replace('src', 'dist'),
+    fileName: path.basename(file),
+    className: filenameToEntity(path.basename(file))
+    // safeClassName: filenameToEntity(path.relative(srcPath, file))
+  })
 }
 
-const utilObjects = new Set([
-  'Util',
-  'Sanitizer',
-  'Backdrop'
-])
-
-const domObjects = new Set([
-  'Data',
-  'EventHandler',
-  'Manipulator',
-  'SelectorEngine'
-])
-
 const build = async plugin => {
-  console.log(`Building ${plugin} plugin...`)
+  const globals = {}
 
-  const { external, globals } = getConfigByPluginKey(plugin)
-  const pluginFilename = path.basename(bsPlugins[plugin])
-  let pluginPath = rootPath
+  const bundle = await rollup.rollup({
+    input: plugin.src,
+    plugins: [
+      babel({
+        // Only transpile our source code
+        exclude: 'node_modules/**',
+        // Include the helpers in each file, at most one copy of each
+        babelHelpers: 'bundled'
+      })
+    ],
+    external: source => {
+      // Pattern to identify local files
+      const pattern = /^(\.{1,2})\//
+
+      // It's not a local file, e.g a Node.js package
+      if (!pattern.test(source)) {
+        globals[source] = source
+        return true
+      }
 
-  if (utilObjects.has(plugin)) {
-    pluginPath = `${rootPath}/util/`
-  }
+      const usedPlugin = resolvedPlugins.find(plugin => {
+        return plugin.src.includes(source.replace(pattern, ''))
+      })
 
-  if (domObjects.has(plugin)) {
-    pluginPath = `${rootPath}/dom/`
-  }
+      if (!usedPlugin) {
+        throw new Error(`Source ${source} is not mapped!`)
+      }
 
-  const bundle = await rollup.rollup({
-    input: bsPlugins[plugin],
-    plugins,
-    external
+      // We can change `Index` with `UtilIndex` etc if we use
+      // `safeClassName` instead of `className` everywhere
+      globals[path.normalize(usedPlugin.src)] = usedPlugin.className
+      return true
+    }
   })
 
   await bundle.write({
-    banner: banner(pluginFilename),
+    banner: banner(plugin.fileName),
     format: 'umd',
-    name: plugin,
+    name: plugin.className,
     sourcemap: true,
     globals,
     generatedCode: 'es2015',
-    file: path.resolve(__dirname, `${pluginPath}/${pluginFilename}`)
+    file: plugin.dist
   })
 
-  console.log(`Building ${plugin} plugin... Done!`)
+  console.log(`Built ${plugin.className}`)
 }
 
-const main = async () => {
+(async () => {
   try {
-    await Promise.all(Object.keys(bsPlugins).map(plugin => build(plugin)))
+    const basename = path.basename(__filename)
+    const timeLabel = `[${basename}] finished`
+
+    console.log('Building individual plugins...')
+    console.time(timeLabel)
+
+    await Promise.all(Object.values(resolvedPlugins).map(plugin => build(plugin)))
+
+    console.timeEnd(timeLabel)
   } catch (error) {
     console.error(error)
-
     process.exit(1)
   }
-}
-
-main()
+})()