UNPKG

@appnest/web-config

Version:

A Rollup configuration to help you build modern web applications.

1,450 lines (1,295 loc) 36.9 kB
import ts from '@wessberg/rollup-plugin-ts'; import autoprefixer from 'autoprefixer'; import cssnano from 'cssnano'; import precss from 'precss'; import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; import license from 'rollup-plugin-license'; import resolve$1 from '@rollup/plugin-node-resolve'; import progress from 'rollup-plugin-progress'; import serve from 'rollup-plugin-serve'; import { terser } from 'rollup-plugin-terser'; import visualizer from 'rollup-plugin-visualizer'; import boxen from 'boxen'; import colors, { green, yellow } from 'colors'; import fileSize from 'filesize'; import fse, { createWriteStream, readFileSync, existsSync, emptyDirSync, appendFile, copy as copy$1 } from 'fs-extra'; import gzipSize from 'gzip-size'; import readdir from 'recursive-readdir-sync'; import { normalize, resolve, dirname, parse, join } from 'path'; import brotli from 'brotli'; import { createFilter } from '@rollup/pluginutils'; import { compress as compress$1 } from 'targz'; import { readFile } from 'fs'; import MagicString from 'magic-string'; import postcss from 'postcss'; import sass from 'node-sass'; import { createServer } from 'livereload'; import { generate } from 'escodegen'; import { parseModule, parseScript } from 'esprima'; import { replace as replace$1 } from 'estraverse'; import { minify } from 'html-minifier'; import { injectManifest, generateSW } from 'workbox-build'; const defaultConfig$9 = { sizes: {}, render: defaultRender, fileName: "budget.txt", silent: true, threshold: 0, timeout: 2000 }; /** * Returns the gzipped bundle size. * @param content * @returns {Number} */ function getGzippedSizeBytes(content) { return gzipSize.sync(content); } /** * Returns the bundle size. * @param content * @returns {number} */ function getSizeBytes(content) { return Buffer.byteLength(content); } /** * Clamps a value between a max and a min (inclusive). * @param value * @param min * @param max * @returns {number} */ function clamp(value, min, max) { return Math.max(Math.min(value, max), min); } /** * Rounds a number to two digits. * @param num * @returns {number} */ function roundNumber(num) { return Math.round(num * 100) / 100; } /** * Initializes an array with a length and a start value. * @param length * @param value */ function initArray(length, value) { return Array(length).fill(value); } /** * Returns the file name for a path. * @param path */ function fileNameForPath(path) { return path.replace(/^.*[\\\/]/, ""); } /** * Returns a formatted string containing the status of the file budget. * @param gzippedSize * @param max * @param sizePerc * @param aboveMax * @param name * @param format */ function defaultRender({ gzippedSize, max, sizePerc, aboveMax, name, formatted }) { // Create methods for formatting the colors const titleColor = formatted ? colors["green"].bold : text => text; const valueColor = formatted ? colors["yellow"] : text => text; const statusColor = formatted ? colors[aboveMax ? "red" : "yellow"] : text => text; const barMaxLength = 20; const values = [`${titleColor("File Name:")} ${valueColor(name)}`, `${titleColor("Budget Size:")} ${valueColor(fileSize(max))}`, // `${titleColor("Actual Size:")} ${statusColor(fileSize(actualSize))}`, `${titleColor("Gzipped Size:")} ${statusColor(fileSize(gzippedSize))}`, `${statusColor("[")}${statusColor(initArray(Math.round(clamp(sizePerc * barMaxLength, 0, barMaxLength)), "#").join(""))}${statusColor(initArray(clamp(Math.round((1 - sizePerc) * barMaxLength), 0, barMaxLength), ".").join(""))}${statusColor("]")} ${statusColor("(" + roundNumber(sizePerc * 100) + "%)")}`]; return boxen(values.join("\n"), { padding: 1 }); } /** * Returns the budget for a specific path. * If no budget has been specified null is returned. * @param path * @param sizes * @returns {*} */ function budgetForPath(path, sizes) { for (const [name, max] of Object.entries(sizes)) { const isExtension = name.startsWith("."); if (path.match(name + (isExtension ? "$" : ""))) { return max; } } return null; } /** * A Rollup plugin that compares the sizes of the files to a specified budget. * @param config * @returns {{name: string, generateBundle(*, *, *): (undefined|void)}} */ function budget(config = {}) { const { sizes, timeout, render, silent, fileName, threshold } = { ...defaultConfig$9, ...config }; const isOutputJson = fileName.endsWith(".json"); return { name: "budget", generateBundle: async (outputOptions, bundle, isWrite) => { // If no sizes has been specifies we can already abort now. if (Object.keys(sizes).length === 0) { return; } // Wait a small amount of time for the bundle to finish before analyzing the bundle. setTimeout(() => { const target = outputOptions.dir; const stream = createWriteStream(`${outputOptions.dir}/${fileName}`); const results = readdir(target).map(path => { return { max: budgetForPath(path, sizes), path }; }).filter(({ max }) => max != null && max > 0).map(({ path, max }) => { const content = readFileSync(path); const name = fileNameForPath(path); const gzippedSize = getGzippedSizeBytes(content); const sizePerc = gzippedSize / max; const aboveMax = sizePerc > 1; return { name, gzippedSize, sizePerc, aboveMax, max, path }; }) // Ensure the ones closest to the budget are in top .sort((a, b) => b.sizePerc - a.sizePerc); // Go through all results and report them for (const result of results) { // Skip the reporting if the size perc is below the threshold if (result.sizePerc < threshold) { return; } // Print to the console if not silent if (!silent) { console.log(render({ ...result, formatted: true })); } // Write to the file if (!isOutputJson) { stream.write(render(result) + "\n\n"); } } // Write the output as json instead if (isOutputJson) { stream.write(JSON.stringify(results, null, 2)); } stream.end(); }, timeout); } }; } const defaultConfig$8 = { targets: [], verbose: true }; /** * A Rollup plugin that clean directories before rebuilding. * @param config */ function clean(config = {}) { const { targets, verbose } = { ...defaultConfig$8, ...config }; return { name: "clean", generateBundle: () => { for (const target of targets) { const path = normalize(target); if (existsSync(path)) { if (verbose) { console.log(green(`[clean] - Cleaning "${path}"`)); } emptyDirSync(path); } } } }; } const defaultConfig$7 = { verbose: true, include: [], exclude: [], compressors: [compressGzip], timeout: 2000 }; /** * Compresses a file using gzip. * @param src * @param verbose */ function compressGzip({ src, verbose }) { const dest = `${src}.gz`; compress$1({ src, dest }, err => { if (verbose && err != null) { console.log(yellow(`[gzip] - Could not compress "${src}" to "${dest}"\n`), err); } }); } /** * Compresses a file using brotli. * @param src * @param verbose */ function compressBrotli({ src, verbose }) { const buffer = brotli.compress(readFileSync(src)); const dest = `${src}.br`; appendFile(dest, buffer, err => { if (verbose && err != null) { console.log(yellow(`[brotli] - Could not compress "${src}" to "${dest}"\n`), err); } }); } /** * A Rollup plugin that compresses the files in the bundle after building. * @param config * @returns {{name: string, generateBundle: generateBundle}} */ function compress(config = {}) { const { verbose, dir, timeout, compressors, include, exclude } = { ...defaultConfig$7, ...config }; const filter = createFilter(include, exclude); return { name: "compress", generateBundle: (outputOptions, bundle, isWrite) => { if (!isWrite) return; // Start the timeout to make sure the rollup bundle and all of the files // will be in the target folder when we are compressing. setTimeout(() => { // Grab the files from the build folder const target = dir || outputOptions.dir; const files = readdir(target).filter(path => !path.endsWith(".gz") && filter(path)); // Compress all files for (const src of files) { for (const compress of compressors) { compress({ src, verbose }); } } // Tell the user that everything went fine if (verbose) { console.log(green(`[compress] - Successfully compressed ${files.length} files`)); } }, timeout); } }; } /** * Default configuration for the copy plugin. * @type {{resources: Array}} */ const defaultConfig$6 = { resources: [], verbose: true, overwrite: true }; /** * A Rollup plugin that copies resources from one location to another. * @param config */ function copy(config = {}) { const { resources, verbose, overwrite } = { ...defaultConfig$6, ...config }; return { name: "copy", generateBundle: async (outputOptions, bundle, isWrite) => { if (!isWrite) return; for (const [from, to] of resources) { try { if (overwrite || !existsSync(to)) { await copy$1(from, to); } } catch (err) { if (verbose) { console.log(yellow(`[copy] - The file "${from}" could not be copied to "${to}"\n`), err.message); } } } } }; } /** * Default configuration for the polyfill. */ const defaultPolyfillConfig = { src: "https://polyfill.app/api/polyfill", crossorigin: true, force: false, context: "window", features: [], options: [] }; /** * Default configuration for the html template plugin. * Note that both template and target are required. */ const defaultConfig$5 = { transform: transformTemplate, transformScript: transformScript, verbose: true, include: [], exclude: [], scriptType: "module", polyfillConfig: defaultPolyfillConfig }; /** * Returns the script tag for the polyfill config. * @param crossorigin * @param features * @param src * @param options */ function getPolyfillScript({ crossorigin, features, src, options }) { src = `${src}?${features.length > 0 ? `features=${features.join(",")}` : ""}${options.length > 0 ? `|${options.join("|")}` : ""}`; return `<script ${crossorigin ? "crossorigin" : ""} src="${src}"></script>`; } /** * Transform the script tag. * @param filename * @param scriptType */ function transformScript({ filename, scriptType }) { return `<script src="${filename}" type="${scriptType}"></script>`; } /** * Transform the template and inserts a script tag for each file. * Injects the script tags before the body close tag. * @param template * @param bodyCloseTagIndex * @param fileNames * @param scriptType * @param polyfillConfig * @param transformScript */ function transformTemplate({ template, bodyCloseTagIndex, fileNames, scriptType, polyfillConfig, transformScript }) { return [template.slice(0, bodyCloseTagIndex), polyfillConfig.features.length > 0 ? `${getPolyfillScript(polyfillConfig)}\n` : "", ...fileNames.map(filename => transformScript({ filename, scriptType })).join("\n"), template.slice(bodyCloseTagIndex, template.length)].join(""); } /** * Injects the sources for the bundle entrypoints and generates a HTML file. * Inspired by https://github.com/bengsfort/rollup-plugin-generate-html-template/blob/master/src/index.js * @param bundle * @param template * @param target * @param filter * @param scriptType * @param verbose * @param include * @param exclude * @param transform * @param polyfillConfig * @param transformScript */ async function generateFile({ bundle, template, target, filter, scriptType, verbose, include, exclude, transform, polyfillConfig, transformScript }) { return new Promise((res, rej) => { readFile(template, (err, buffer) => { // If the file could not be read, abort! if (err) { return rej(err); } // Convert the buffer into a string const template = buffer.toString("utf8"); // Grab the index of the body close tag const bodyCloseTagIndex = template.lastIndexOf("</body>"); // Grab fileNames of the entry points const unfilteredFilenames = Object.values(bundle).map(value => value.fileName); const fileNames = unfilteredFilenames.filter(name => filter(name)); // Error handling if (verbose && fileNames.length === 0) { console.log(colors.yellow(`[htmlTemplate] - No scripts were injected into the "${target}" template file. Make sure to specify the files that should be injected using the include option. Currently the include option has been set to "${include}" and the exclude option to "${exclude}". The filenames passed to the plugin are "${unfilteredFilenames.join(", ")}"\n`)); } // Transform the template const html = transform({ template, bodyCloseTagIndex, fileNames, scriptType, transformScript, polyfillConfig: polyfillConfig }); // Write the injected template to a file. try { fse.outputFileSync(target, html); res(); } catch (err) { rej(err); } }); }); } /** * A Rollup plugin that injects the bundle entry points into a HTML file. * @param config */ function htmlTemplate(config = {}) { config = { ...defaultConfig$5, ...config }; const { template, target, include, exclude, polyfillConfig } = { ...defaultConfig$5, ...config }; const filter = createFilter(include, exclude); // Throw error if neither the template nor the target has been defined if (template == null || target == null) { throw new Error(`The htmlTemplate plugin needs both a template and a target.`); } return { name: "htmlTemplate", generateBundle: async (outputOptions, bundle, isWrite) => { if (!isWrite) return; // @ts-ignore return generateFile({ ...config, polyfillConfig: { ...defaultPolyfillConfig, ...polyfillConfig }, bundle, filter }); } }; } /** * Default empty source map. * @type {{version: number, file: null, sources: *[], sourcesContent: *[], names: Array, mappings: string}} */ const emptySourcemap = { version: 3, file: null, sources: [null], sourcesContent: [null], names: [], mappings: "" }; /** * Default configuration for the import SCSS plugin. * @type {{plugins: Array, extensions: string[], globals: Array}} */ const defaultConfig$4 = { plugins: [], extensions: [".css", ".scss"], globals: [], postcssConfig: {}, sassConfig: {}, transform: transformImport }; /** * Default transform. * @param id * @param isGlobal */ function transformImport(id, isGlobal) { return isGlobal ? transformGlobal : transformDefault; } /** * Overwrites the css file with "export default". * @param css * @returns {string} */ function transformDefault(css) { return `export default \`${css}\``; } /** * Overwrites the css file with a global inject into the head. * @param css * @returns {string} */ function transformGlobal(css) { return ` const css = \`${css}\`; const $styles = document.createElement("style"); $styles.innerText = css; document.head.appendChild($styles); export default css; `; } /** * Processes a SCSS file by running it through a processor and generating the code and its corresponding sourcemap. * @param data * @param id * @param processor * @param overwrite * @param postcssConfig * @param sassConfig */ async function processFile$1({ data, id, processor, overwrite, postcssConfig, sassConfig }) { return new Promise(res => { // Compile the data using the sass compiler const css = sass.renderSync({ file: resolve(id), sourceMap: false /* We generate sourcemaps later */ , ...(sassConfig || {}) }).css.toString(); // The magic strings cannot handle empty strings, therefore we test whether we should already abort now. if (css.trim() === "") { return res({ code: overwrite(""), map: emptySourcemap }); } // Create a magic string container to generate source map const stringContainer = new MagicString(css); // Construct the options const processOptions = { from: id, to: id, map: { inline: false, annotation: false }, ...(postcssConfig || {}) }; // Process the file content processor.process(stringContainer.toString(), processOptions).then(result => { const css = result.css; stringContainer.overwrite(0, stringContainer.length(), overwrite(css)); res({ code: stringContainer.toString(), map: stringContainer.generateMap() }); }); }); } /** * A Rollup plugin that makes it possible to import style files using postcss. * Looks for the "import css from 'styles.scss'" and "import 'styles.scss'" syntax as default. * @param config */ function importStyles(config = {}) { config = { ...defaultConfig$4, ...config }; const { plugins, extensions, globals, transform } = config; // Determines whether the file should be handled by the plugin or not. const filter = id => extensions.find(ext => id.endsWith(ext)) != null; // Determines whether the file is global or not const isGlobal = id => globals.find(name => id.endsWith(name)) != null; // Create the postcss processor based on the plugins. const processor = postcss(plugins); return { name: "importStyles", resolveId: (id, importer) => { if (!importer || !filter(id)) return; return resolve(dirname(importer), id); }, transform: async (data, id) => { if (!filter(id)) return; const overwrite = transform(id, isGlobal(id)); // @ts-ignore return processFile$1({ ...config, processor, overwrite, id, data }); } }; } /** * ######################################### * Parts of this code is heavily inspired by https://github.com/thgh/rollup-plugin-livereload. * The license has therefore been included. * ######################################### */ /** * Default configuration for the livereload plugin. */ const defaultConfig$3 = { watch: "dist", port: 35729, verbose: true }; /** * Returns the livereload html. */ function livereloadHtml(port) { return `/* Inserted by the Livereload plugin */ if (typeof document !== 'undefined') { (function(doc, id) { /* Ensure that a script does not exist in the doc yet */ var $container = doc.head || doc.body; if ($container == null || $container.querySelector("#" + id) != null) { return; } /* Create script that takes care of reloading each time a watched file changes */ var $script = doc.createElement("script"); $script.async = true; $script.id = id; $script.src = "//" + (location.host || "localhost").split(":")[0] + ":${port}/livereload.js?snipver=1"; /* Inject the script as the first one */ $container.insertBefore($script, $container.firstChild); })(document, "rollup-plugin-livereload"); }`; } /** * Subscribes the server to terminate when required. * @param server */ function attachTerminationListeners(server) { // Hook up listeners that kills the server if the process is terminated for some reason const terminationSignals = ["SIGINT", "SIGTERM", "SIGQUIT"]; for (const signal of terminationSignals) { process.on(signal, () => killServer(server)); } // Rethrow the error server.on("error", err => { server.close(); throw err; }); } /** * Tears down the server. * @param server */ function killServer(server) { server.close(); process.exit(); } /** * A Rollup plugin that live reload files as they changes. * @param config * @returns {*} */ function livereload(config = {}) { const { watch, port, verbose } = { ...defaultConfig$3, ...config }; // Start watching the files const server = createServer({ watch, port, verbose }); const paths = Array.isArray(watch) ? watch : [watch]; // @ts-ignore // TODO server.watch(paths.map(p => resolve(process.cwd(), p))); attachTerminationListeners(server); return { name: "livereload", banner: () => livereloadHtml(port), generateBundle: () => { if (verbose) { console.log(colors.green(`[livereload] - Enabled.`)); } } }; } /** * ######################################### * Parts of this code is heavily inspired by https://github.com/edge0701/minify-lit-html-loader. * The license has therefore been included. * ######################################### */ /** * The default configuration for the minify-lit-html plugin. */ const defaultConfig$2 = { include: [/\.js$/, /\.ts$/], exclude: [], verbose: true, esprima: { loc: true, range: true, tolerant: true, tokens: false }, htmlMinifier: { caseSensitive: true, minifyCSS: false /* Should be set as true, but the HTML minifier won't allow the <style>${css}</style> syntax, so I disabled it */ , preventAttributesEscaping: true, preserveLineBreaks: false, collapseWhitespace: true, conservativeCollapse: true, removeComments: true, ignoreCustomFragments: [/<\s/, /<=/, /\$\{/, /\}/, /* The HTML minifier won't parse parts with double quote inside (eg. @click="${() => alert("Hello World")}") */ /"\${[^}]+"[^}]+}"/] } }; /** * Creates a transformer that traverses an ast, minifying the html`...` parts from lit-html. * This function is heavily inspired by https://github.com/edge0701/minify-lit-html-loader/blob/master/src/index.ts. * @param code * @param config */ function createTransformer({ code, config }) { const chunks = code.split(""); return ast => { return replace$1(ast, { enter: node => { // If the node type is a TaggedTemplateExpression we know we are looking at a TemplateResult. if (node.type === "TaggedTemplateExpression") { // If the tag name or property name is html we know we are looking at a html`...` part. if (node.tag.type === "Identifier" && node.tag.name === "html" || node.tag.type === "MemberExpression" && node.tag.property.type === "Identifier" && node.tag.property.name === "html") { // Minify the HTML inside the html tagged template literals. const mini = minify(chunks.slice(node.quasi.range[0] + 1, node.quasi.range[1] - 1).join(""), config.htmlMinifier); // Return the new node return { ...node, quasi: { ...node.quasi, quasis: [{ type: "TemplateElement", value: { raw: mini }, range: [node.quasi.range[0], mini.length] }] } }; } } }, fallback: "iteration" }); }; } /** * Figures out whether the code is a script type or module type. * @param code * @param config */ function parseAst({ code, config }) { try { return parseModule(code, config.esprima); } catch (e) { return parseScript(code, config.esprima); } } /** * Processes the code by minifying the html using in the tagged template literals. * @param code * @param id * @param config * @returns {Promise<void>} */ function processFile({ code, id, config }) { return new Promise(res => { try { // Create transformer that traverses the ast and minifies the html`...` parts. const transform = createTransformer({ code, config }); // Build an ast from the current config const ast = parseAst({ code, config }); // // Create new ast using the transformer const newAst = transform(ast); // Regenerate the code based on the new ast. // If sourceMapWithCode is truthy, an object is returned from // generate() of the form: { code: .. , map: .. } const { code: minifiedCode, map } = generate(newAst, { sourceMapWithCode: true, sourceMap: id, sourceContent: code, sourceCode: code }); return res({ code: minifiedCode, map: map.toString() }); } catch (err) { if (config.verbose) { console.log(colors.yellow(`[minifyLitHTML] - Could not parse "${err.message}" in "${id}"\n`)); } // Sometimes we cannot parse the file. This should however not stop the build from finishing. res({ code, map: emptySourcemap }); } }); } /** * A Rollup plugin that minifies lit-html templates. * @param config * @returns {{name: string, resolveId: (function(*=, *=): *), transform: (function(*, *=): Promise<void>)}} */ function minifyLitHTML(config = {}) { config = { ...defaultConfig$2, ...config }; const { include, exclude } = config; // Generate a filter that determines whether the file should be handled by the plugin or not. const filter = createFilter(include, exclude); return { name: "minifyLitHTML", resolveId: (id, importer) => { if (!importer || !filter(id)) return; return resolve(dirname(importer), id); }, transform: (code, id) => { if (!filter(id)) return; return processFile({ code, id, config: config }); } }; } const defaultConfig$1 = { verbose: true, resources: [] }; /** * A Rollup plugin that replaces an import with another import. * @param config */ function replace(config = {}) { const { resources, verbose } = { ...defaultConfig$1, ...config }; return { name: "replace", resolveId: (id, importer) => { if (!importer) return; const { name: idName } = parse(id); for (const [from, to] of resources) { if (from == null || to == null) return; const { name: fromName } = parse(from); // If the id and the from name are the same, we simply need to resolve // it to the "to name" instead and we have replaced the file. if (idName === fromName) { if (verbose) { console.log(colors.green(`[replace] - Replaced "${id}" with "${to}"\n`)); } return resolve(to); } } } }; } const isProd = process.env.NODE_ENV === "prod" || process.env.NODE_ENV === "production"; const isDev = process.env.NODE_ENV === "dev" || process.env.NODE_ENV === "development"; const isLibrary = process.env.NODE_ENV === "lib" || process.env.NODE_ENV === "library"; const isServe = process.env.ROLLUP_WATCH || false; /** * Returns the config or an empty default. * @param config */ const configOrDefault = config => { return config || {}; }; /** * The default scss plugins. */ const postcssPlugins = [precss(), autoprefixer(), ...(isProd ? [// Currently there's an issue with nested calcs and custom variables. // It can be reproduced by entering the following in https://cssnano.co/playground/: font-size: calc(var(--padding, calc(24 * var(--base-size, 1px))) + 1); // cssnano uses the following postcss plugins: https://cssnano.co/guides/optimisations. // https://cssnano.co/optimisations/calc is therefore probably the cause for this issue. // Read here for configuration: https://cssnano.co/guides/presets cssnano({ preset: ["default", { calc: false }] })] : [])]; /** * Default configuration for the output. * @param config */ const defaultOutputConfig = config => { // format: (system, amd, cjs, esm, iife, umd) return { entryFileNames: "[name]-[hash].js", chunkFileNames: "[name]-[hash].js", sourcemap: true, format: "esm", ...configOrDefault(config) }; }; /** * Default plugins for resolve. * @param importStylesConfig * @param replaceConfig * @param tsConfig * @param commonjsConfig * @param jsonConfig * @param resolveConfig */ const defaultResolvePlugins = ({ importStylesConfig, jsonConfig, resolveConfig, tsConfig, commonjsConfig, replaceConfig } = {}) => [// Teaches Rollup what files should be replaced replace({ ...configOrDefault(replaceConfig) }), // Teaches Rollup how to find external modules // https://github.com/rollup/rollup-plugin-node-resolve resolve$1({ modulesOnly: false, mainFields: ["module", "browser", "jsnext:main"], ...configOrDefault(resolveConfig) }), // Teaches Rollup how to import styles when using the "import css from "./styles.scss" syntax. importStyles({ plugins: postcssPlugins, ...configOrDefault(importStylesConfig) }), // Teaches Rollup how to import json files json({ preferConst: true, compact: true, ...configOrDefault(jsonConfig) }), // Teaches Rollup how to transpile Typescript // https://github.com/wessberg/rollup-plugin-ts ts({ /* @ts-ignore */ transpiler: "babel", ...configOrDefault(tsConfig) }), // At the moment, the majority of packages on NPM are exposed as CommonJS modules commonjs({ include: "**/node_modules/**", ...configOrDefault(commonjsConfig) })]; /** * Default configuration for the plugins that runs every time the bundle is created. * @param cleanConfig * @param copyConfig * @param importStylesConfig * @param jsonConfig * @param htmlTemplateConfig * @param resolveConfig * @param progressConfig * @param tsConfig * @param commonjsConfig * @param replaceConfig */ const defaultPlugins = ({ cleanConfig, copyConfig, importStylesConfig, jsonConfig, htmlTemplateConfig, resolveConfig, progressConfig, tsConfig, commonjsConfig, replaceConfig } = {}) => [// Shows a progress indicator while building progress({ ...configOrDefault(progressConfig) }), // Cleans the dist folder to get rid of files from the previous build clean({ ...configOrDefault(cleanConfig) }), // Teach rollup how to resolve imports ...defaultResolvePlugins({ importStylesConfig, jsonConfig, resolveConfig, tsConfig, commonjsConfig, replaceConfig }), // Copies resources copy({ ...configOrDefault(copyConfig) }), // Creates a HTML template with the injected scripts from the entry points htmlTemplate({ ...configOrDefault(htmlTemplateConfig) })]; /** * Default plugins that only run when the bundle is being served. * @param dist * @param serveConfig * @param livereloadConfig */ const defaultServePlugins = ({ dist, serveConfig, livereloadConfig } = {}) => [// Serves the application files serve({ open: true, port: 1337, historyApiFallback: true, host: "localhost", headers: { "Access-Control-Allow-Origin": "*" }, ...(dist != null ? { contentBase: dist } : {}), ...configOrDefault(serveConfig) }), // Reloads the page when run in watch mode livereload({ ...(dist != null ? { watch: dist } : {}), ...configOrDefault(livereloadConfig) })]; /** * Default plugins that only run when the bundle is being created in prod mode. * @param dist * @param minifyLitHtmlConfig * @param licenseConfig * @param terserConfig * @param budgetConfig * @param visualizerConfig * @param compressConfig */ const defaultProdPlugins = ({ dist, minifyLitHtmlConfig, licenseConfig, terserConfig, budgetConfig, visualizerConfig, compressConfig } = {}) => [// Minifies the lit-html files minifyLitHTML({ ...configOrDefault(minifyLitHtmlConfig) }), // Collects all the license files license({ sourcemap: true, ...(dist != null ? { thirdParty: { output: join(dist, "licenses.txt") } } : {}), ...configOrDefault(licenseConfig) }), // Minifies the code terser({ output: { // Don't preserve any comments, we create a license.txt instead. comments: () => false }, ...configOrDefault(terserConfig) }), // Prints the budget and sizes of the files to the console budget({ ...configOrDefault(budgetConfig) }), // Create a HTML file visualizing the size of each module visualizer({ sourcemap: true, ...(dist != null ? { filename: join(dist, "stats.html") } : {}), ...configOrDefault(visualizerConfig) }), // Compresses all of the files compress({ ...configOrDefault(compressConfig) })]; /** * Default external dependencies. * @param dependencies * @param devDependencies * @param peerDependencies */ const defaultExternals = ({ dependencies, devDependencies, peerDependencies }) => [...Object.keys(configOrDefault(dependencies)), ...Object.keys(configOrDefault(devDependencies)), ...Object.keys(configOrDefault(peerDependencies))]; /** * Creates a default karma configuration. * @param files * @param mime * @param preprocessors * @param karmaPlugins * @param rollupPlugins */ const defaultKarmaConfig = ({ files, mime, preprocessors, karmaPlugins, rollupPlugins } = {}) => { return { concurrency: Infinity, colors: true, autoWatch: true, singleRun: true, captureTimeout: 60000, browsers: ["ChromeHeadless"], frameworks: ["mocha", "chai", "iframes"], reporters: ["progress"], plugins: ["karma-mocha", "karma-chai", "karma-chrome-launcher", "karma-rollup-preprocessor", "karma-iframes", ...(karmaPlugins || [])], files: [ /** * Make sure to disable Karma’s file watcher * because the preprocessor will use its own. */ { pattern: "**/*.test.+(ts|js)", watched: false }, ...(files || [])], preprocessors: { "**/*.test.+(ts|js)": ["rollup", "iframes"], ...(preprocessors || {}) }, rollupPreprocessor: { /** * This is just a normal Rollup config object, * except that `input` is handled for you. */ plugins: rollupPlugins, output: { format: "iife", name: "wutwut", sourcemap: "inline" } }, // The below line tricks karma into thinking .ts files are cool // See https://github.com/webpack-contrib/karma-webpack/issues/298#issuecomment-367081075 for more info mime: { "text/x-typescript": ["ts"], ...(mime || {}) } }; }; var GenerateServiceWorkerKind; (function (GenerateServiceWorkerKind) { GenerateServiceWorkerKind["generateSw"] = "generateSW"; GenerateServiceWorkerKind["injectManifest"] = "injectManifest"; })(GenerateServiceWorkerKind || (GenerateServiceWorkerKind = {})); /** * Default configuration for the workbox rollup plugin. */ const defaultConfig = { mode: GenerateServiceWorkerKind.generateSw, verbose: true, timeout: 2000, workboxConfig: {} }; /** * Returns the correct method to for generating the Service Worker. * @param mode */ function workboxFactory(mode) { switch (mode) { case GenerateServiceWorkerKind.generateSw: return generateSW; case GenerateServiceWorkerKind.injectManifest: return injectManifest; } throw new Error(`[workbox] - The mode "${mode} is not valid"`); } /** * A Rollup plugin that uses workbox to generate a service worker. * @param config */ function workbox(config = {}) { const { workboxConfig, mode, verbose, timeout } = { ...defaultConfig, ...config }; // Ensure a workbox config exists if (workboxConfig == null) { throw new Error(`[workbox] - The workboxConfig needs to be defined`); } return { name: "workbox", generateBundle: async (outputOptions, bundle, isWrite) => { if (!isWrite) return; try { setTimeout(async () => { await workboxFactory(mode)(workboxConfig); }, timeout); await workboxFactory(mode)(workboxConfig); } catch (ex) { if (verbose) { console.log(colors.red(`[workbox] - The Service Worker could not be generated: "${ex.message}"`)); } } } }; } export { GenerateServiceWorkerKind, budget, clean, compress, compressBrotli, compressGzip, copy, defaultExternals, defaultKarmaConfig, defaultOutputConfig, defaultPlugins, defaultProdPlugins, defaultResolvePlugins, defaultServePlugins, getPolyfillScript, htmlTemplate, importStyles, isDev, isLibrary, isProd, isServe, livereload, minifyLitHTML, postcssPlugins, replace, transformScript, transformTemplate, workbox };