diff options
Diffstat (limited to 'build')
-rw-r--r-- | build/.eslintrc.json | 15 | ||||
-rw-r--r-- | build/banner.js | 14 | ||||
-rw-r--r-- | build/build-plugins.js | 104 | ||||
-rw-r--r-- | build/change-version.js | 81 | ||||
-rw-r--r-- | build/generate-sri.js | 64 | ||||
-rw-r--r-- | build/postcss.config.js | 19 | ||||
-rw-r--r-- | build/rollup.config.js | 57 | ||||
-rw-r--r-- | build/vnu-jar.js | 57 | ||||
-rw-r--r-- | build/zip-examples.js | 90 |
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) |