summaryrefslogtreecommitdiffstats
path: root/build
diff options
context:
space:
mode:
Diffstat (limited to 'build')
-rw-r--r--build/.eslintrc.json15
-rw-r--r--build/banner.js14
-rw-r--r--build/build-plugins.js104
-rw-r--r--build/change-version.js81
-rw-r--r--build/generate-sri.js64
-rw-r--r--build/postcss.config.js19
-rw-r--r--build/rollup.config.js57
-rw-r--r--build/vnu-jar.js57
-rw-r--r--build/zip-examples.js90
9 files changed, 501 insertions, 0 deletions
diff --git a/build/.eslintrc.json b/build/.eslintrc.json
new file mode 100644
index 0000000..dec6323
--- /dev/null
+++ b/build/.eslintrc.json
@@ -0,0 +1,15 @@
+{
+ "env": {
+ "browser": false,
+ "node": true
+ },
+ "parserOptions": {
+ "sourceType": "script"
+ },
+ "extends": "../.eslintrc.json",
+ "rules": {
+ "no-console": "off",
+ "strict": "error",
+ "unicorn/prefer-top-level-await": "off"
+ }
+}
diff --git a/build/banner.js b/build/banner.js
new file mode 100644
index 0000000..df82ff3
--- /dev/null
+++ b/build/banner.js
@@ -0,0 +1,14 @@
+'use strict'
+
+const pkg = require('../package.json')
+const year = new Date().getFullYear()
+
+function getBanner(pluginFilename) {
+ return `/*!
+ * Bootstrap${pluginFilename ? ` ${pluginFilename}` : ''} v${pkg.version} (${pkg.homepage})
+ * Copyright 2011-${year} ${pkg.author}
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */`
+}
+
+module.exports = getBanner
diff --git a/build/build-plugins.js b/build/build-plugins.js
new file mode 100644
index 0000000..a160209
--- /dev/null
+++ b/build/build-plugins.js
@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+
+/*!
+ * Script to build our plugins to use them separately.
+ * Copyright 2020-2022 The Bootstrap Authors
+ * Copyright 2020-2022 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+'use strict'
+
+const path = require('node:path')
+const rollup = require('rollup')
+const globby = require('globby')
+const { babel } = require('@rollup/plugin-babel')
+const banner = require('./banner.js')
+
+const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/')
+const jsFiles = globby.sync(sourcePath + '/**/*.js')
+
+// Array which holds the resolved plugins
+const resolvedPlugins = []
+
+// 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())
+
+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(sourcePath, file))
+ })
+}
+
+const build = async plugin => {
+ const globals = {}
+
+ 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
+ }
+
+ const usedPlugin = resolvedPlugins.find(plugin => {
+ return plugin.src.includes(source.replace(pattern, ''))
+ })
+
+ if (!usedPlugin) {
+ throw new Error(`Source ${source} is not mapped!`)
+ }
+
+ // 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(plugin.fileName),
+ format: 'umd',
+ name: plugin.className,
+ sourcemap: true,
+ globals,
+ generatedCode: 'es2015',
+ file: plugin.dist
+ })
+
+ console.log(`Built ${plugin.className}`)
+}
+
+(async () => {
+ try {
+ 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)
+ }
+})()
diff --git a/build/change-version.js b/build/change-version.js
new file mode 100644
index 0000000..57c5fde
--- /dev/null
+++ b/build/change-version.js
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+
+/*!
+ * Script to update version number references in the project.
+ * Copyright 2017-2022 The Bootstrap Authors
+ * Copyright 2017-2022 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+'use strict'
+
+const fs = require('node:fs').promises
+const path = require('node:path')
+const globby = require('globby')
+
+const VERBOSE = process.argv.includes('--verbose')
+const DRY_RUN = process.argv.includes('--dry') || process.argv.includes('--dry-run')
+
+// These are the filetypes we only care about replacing the version
+const GLOB = [
+ '**/*.{css,html,js,json,md,scss,txt,yml}'
+]
+const GLOBBY_OPTIONS = {
+ cwd: path.join(__dirname, '..'),
+ gitignore: true
+}
+
+// Blame TC39... https://github.com/benjamingr/RegExp.escape/issues/37
+function regExpQuote(string) {
+ return string.replace(/[$()*+-.?[\\\]^{|}]/g, '\\$&')
+}
+
+function regExpQuoteReplacement(string) {
+ return string.replace(/\$/g, '$$')
+}
+
+async function replaceRecursively(file, oldVersion, newVersion) {
+ const originalString = await fs.readFile(file, 'utf8')
+ const newString = originalString.replace(
+ new RegExp(regExpQuote(oldVersion), 'g'), regExpQuoteReplacement(newVersion)
+ )
+
+ // No need to move any further if the strings are identical
+ if (originalString === newString) {
+ return
+ }
+
+ if (VERBOSE) {
+ console.log(`FILE: ${file}`)
+ }
+
+ if (DRY_RUN) {
+ return
+ }
+
+ await fs.writeFile(file, newString, 'utf8')
+}
+
+async function main(args) {
+ let [oldVersion, newVersion] = args
+
+ if (!oldVersion || !newVersion) {
+ console.error('USAGE: change-version old_version new_version [--verbose] [--dry[-run]]')
+ console.error('Got arguments:', args)
+ process.exit(1)
+ }
+
+ // Strip any leading `v` from arguments because otherwise we will end up with duplicate `v`s
+ [oldVersion, newVersion] = [oldVersion, newVersion].map(arg => arg.startsWith('v') ? arg.slice(1) : arg)
+
+ try {
+ const files = await globby(GLOB, GLOBBY_OPTIONS)
+
+ await Promise.all(files.map(file => replaceRecursively(file, oldVersion, newVersion)))
+ } catch (error) {
+ console.error(error)
+ process.exit(1)
+ }
+}
+
+main(process.argv.slice(2))
diff --git a/build/generate-sri.js b/build/generate-sri.js
new file mode 100644
index 0000000..ef1b39f
--- /dev/null
+++ b/build/generate-sri.js
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+
+/*!
+ * Script to generate SRI hashes for use in our docs.
+ * Remember to use the same vendor files as the CDN ones,
+ * otherwise the hashes won't match!
+ *
+ * Copyright 2017-2022 The Bootstrap Authors
+ * Copyright 2017-2022 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+'use strict'
+
+const crypto = require('node:crypto')
+const fs = require('node:fs')
+const path = require('node:path')
+const sh = require('shelljs')
+
+sh.config.fatal = true
+
+const configFile = path.join(__dirname, '../config.yml')
+
+// Array of objects which holds the files to generate SRI hashes for.
+// `file` is the path from the root folder
+// `configPropertyName` is the config.yml variable's name of the file
+const files = [
+ {
+ file: 'dist/css/bootstrap.min.css',
+ configPropertyName: 'css_hash'
+ },
+ {
+ file: 'dist/css/bootstrap.rtl.min.css',
+ configPropertyName: 'css_rtl_hash'
+ },
+ {
+ file: 'dist/js/bootstrap.min.js',
+ configPropertyName: 'js_hash'
+ },
+ {
+ file: 'dist/js/bootstrap.bundle.min.js',
+ configPropertyName: 'js_bundle_hash'
+ },
+ {
+ file: 'node_modules/@popperjs/core/dist/umd/popper.min.js',
+ configPropertyName: 'popper_hash'
+ }
+]
+
+for (const file of files) {
+ fs.readFile(file.file, 'utf8', (error, data) => {
+ if (error) {
+ throw error
+ }
+
+ const algo = 'sha384'
+ const hash = crypto.createHash(algo).update(data, 'utf8').digest('base64')
+ const integrity = `${algo}-${hash}`
+
+ console.log(`${file.configPropertyName}: ${integrity}`)
+
+ sh.sed('-i', new RegExp(`^(\\s+${file.configPropertyName}:\\s+["'])\\S*(["'])`), `$1${integrity}$2`, configFile)
+ })
+}
diff --git a/build/postcss.config.js b/build/postcss.config.js
new file mode 100644
index 0000000..7f8186d
--- /dev/null
+++ b/build/postcss.config.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const mapConfig = {
+ inline: false,
+ annotation: true,
+ sourcesContent: true
+}
+
+module.exports = context => {
+ return {
+ map: context.file.dirname.includes('examples') ? false : mapConfig,
+ plugins: {
+ autoprefixer: {
+ cascade: false
+ },
+ rtlcss: context.env === 'RTL'
+ }
+ }
+}
diff --git a/build/rollup.config.js b/build/rollup.config.js
new file mode 100644
index 0000000..27f12ac
--- /dev/null
+++ b/build/rollup.config.js
@@ -0,0 +1,57 @@
+'use strict'
+
+const path = require('node:path')
+const { babel } = require('@rollup/plugin-babel')
+const { nodeResolve } = require('@rollup/plugin-node-resolve')
+const replace = require('@rollup/plugin-replace')
+const banner = require('./banner.js')
+
+const BUNDLE = process.env.BUNDLE === 'true'
+const ESM = process.env.ESM === 'true'
+
+let fileDestination = `bootstrap${ESM ? '.esm' : ''}`
+const external = ['@popperjs/core']
+const plugins = [
+ babel({
+ // Only transpile our source code
+ exclude: 'node_modules/**',
+ // Include the helpers in the bundle, at most one copy of each
+ babelHelpers: 'bundled'
+ })
+]
+const globals = {
+ '@popperjs/core': 'Popper'
+}
+
+if (BUNDLE) {
+ fileDestination += '.bundle'
+ // Remove last entry in external array to bundle Popper
+ external.pop()
+ delete globals['@popperjs/core']
+ plugins.push(
+ replace({
+ 'process.env.NODE_ENV': '"production"',
+ preventAssignment: true
+ }),
+ nodeResolve()
+ )
+}
+
+const rollupConfig = {
+ input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`),
+ output: {
+ banner,
+ file: path.resolve(__dirname, `../dist/js/${fileDestination}.js`),
+ format: ESM ? 'esm' : 'umd',
+ globals,
+ generatedCode: 'es2015'
+ },
+ external,
+ plugins
+}
+
+if (!ESM) {
+ rollupConfig.output.name = 'bootstrap'
+}
+
+module.exports = rollupConfig
diff --git a/build/vnu-jar.js b/build/vnu-jar.js
new file mode 100644
index 0000000..f29eeb7
--- /dev/null
+++ b/build/vnu-jar.js
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+
+/*!
+ * Script to run vnu-jar if Java is available.
+ * Copyright 2017-2022 The Bootstrap Authors
+ * Copyright 2017-2022 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+'use strict'
+
+const { execFile, spawn } = require('node:child_process')
+const vnu = require('vnu-jar')
+
+execFile('java', ['-version'], (error, stdout, stderr) => {
+ if (error) {
+ console.error('Skipping vnu-jar test; Java is missing.')
+ return
+ }
+
+ const is32bitJava = !/64-Bit/.test(stderr)
+
+ // vnu-jar accepts multiple ignores joined with a `|`.
+ // Also note that the ignores are string regular expressions.
+ const ignores = [
+ // "autocomplete" is included in <button> and checkboxes and radio <input>s due to
+ // Firefox's non-standard autocomplete behavior - see https://bugzilla.mozilla.org/show_bug.cgi?id=654072
+ 'Attribute “autocomplete” is only allowed when the input type is.*',
+ 'Attribute “autocomplete” not allowed on element “button” at this point.',
+ // Per https://www.w3.org/TR/html-aria/#docconformance having "aria-disabled" on a link is
+ // NOT RECOMMENDED, but it's still valid - we explain in the docs that it's not ideal,
+ // and offer more robust alternatives, but also need to show a less-than-ideal example
+ 'An “aria-disabled” attribute whose value is “true” should not be specified on an “a” element that has an “href” attribute.'
+ ].join('|')
+
+ const args = [
+ '-jar',
+ `"${vnu}"`,
+ '--asciiquotes',
+ '--skip-non-html',
+ '--Werror',
+ `--filterpattern "${ignores}"`,
+ '_site/',
+ 'js/tests/'
+ ]
+
+ // For the 32-bit Java we need to pass `-Xss512k`
+ if (is32bitJava) {
+ args.splice(0, 0, '-Xss512k')
+ }
+
+ return spawn('java', args, {
+ shell: true,
+ stdio: 'inherit'
+ })
+ .on('exit', process.exit)
+})
diff --git a/build/zip-examples.js b/build/zip-examples.js
new file mode 100644
index 0000000..077901e
--- /dev/null
+++ b/build/zip-examples.js
@@ -0,0 +1,90 @@
+#!/usr/bin/env node
+
+/*!
+ * Script to create the built examples zip archive;
+ * requires the `zip` command to be present!
+ * Copyright 2020-2022 The Bootstrap Authors
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+'use strict'
+
+const path = require('node:path')
+const sh = require('shelljs')
+
+const pkg = require('../package.json')
+
+const versionShort = pkg.config.version_short
+const distFolder = `bootstrap-${pkg.version}-examples`
+const rootDocsDir = '_site'
+const docsDir = `${rootDocsDir}/docs/${versionShort}/`
+
+// these are the files we need in the examples
+const cssFiles = [
+ 'bootstrap.min.css',
+ 'bootstrap.min.css.map',
+ 'bootstrap.rtl.min.css',
+ 'bootstrap.rtl.min.css.map'
+]
+const jsFiles = [
+ 'bootstrap.bundle.min.js',
+ 'bootstrap.bundle.min.js.map'
+]
+const imgFiles = [
+ 'bootstrap-logo.svg',
+ 'bootstrap-logo-white.svg'
+]
+
+sh.config.fatal = true
+
+if (!sh.test('-d', rootDocsDir)) {
+ throw new Error(`The "${rootDocsDir}" folder does not exist, did you forget building the docs?`)
+}
+
+// switch to the root dir
+sh.cd(path.join(__dirname, '..'))
+
+// remove any previously created folder/zip with the same name
+sh.rm('-rf', [distFolder, `${distFolder}.zip`])
+
+// create any folders so that `cp` works
+sh.mkdir('-p', [
+ distFolder,
+ `${distFolder}/assets/brand/`,
+ `${distFolder}/assets/dist/css/`,
+ `${distFolder}/assets/dist/js/`
+])
+
+sh.cp('-Rf', `${docsDir}/examples/*`, distFolder)
+
+for (const file of cssFiles) {
+ sh.cp('-f', `${docsDir}/dist/css/${file}`, `${distFolder}/assets/dist/css/`)
+}
+
+for (const file of jsFiles) {
+ sh.cp('-f', `${docsDir}/dist/js/${file}`, `${distFolder}/assets/dist/js/`)
+}
+
+for (const file of imgFiles) {
+ sh.cp('-f', `${docsDir}/assets/brand/${file}`, `${distFolder}/assets/brand/`)
+}
+
+sh.rm(`${distFolder}/index.html`)
+
+// get all examples' HTML files
+for (const file of sh.find(`${distFolder}/**/*.html`)) {
+ const fileContents = sh.cat(file)
+ .toString()
+ .replace(new RegExp(`"/docs/${versionShort}/`, 'g'), '"../')
+ .replace(/"..\/dist\//g, '"../assets/dist/')
+ .replace(/(<link href="\.\.\/.*) integrity=".*>/g, '$1>')
+ .replace(/(<script src="\.\.\/.*) integrity=".*>/g, '$1></script>')
+ .replace(/( +)<!-- favicons(.|\n)+<style>/i, ' <style>')
+ new sh.ShellString(fileContents).to(file)
+}
+
+// create the zip file
+sh.exec(`zip -r9 "${distFolder}.zip" "${distFolder}"`)
+
+// remove the folder we created
+sh.rm('-rf', distFolder)