/* * Copyright (c) 2016-2021 Martin Donath * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ import { createHash } from "crypto" import * as fs from "fs/promises" import { minify as minhtml } from "html-minifier" import * as path from "path" import { concat, defer, from, merge, of } from "rxjs" import { concatMap, map, switchMap, takeWhile } from "rxjs/operators" import { extendDefaultPlugins, optimize } from "svgo" import { copyAll } from "./copy" import { base, resolve } from "./resolve" import { transformScript, transformStyle } from "./transform" /* ---------------------------------------------------------------------------- * Helper functions * ------------------------------------------------------------------------- */ /** * Replace file extension * * @param file - File * @param extension - New extension * * @returns File with new extension */ function ext(file: string, extension: string): string { return file.replace(path.extname(file), extension) } /** * Optimize SVG data * * This function will just pass-through non-SVG data, which makes the pipeline * much simpler, as we can reuse it for the license texts. * * @param data - SVG data * * @returns Minified SVG data */ function minsvg(data: string): string { const result = optimize(data, { plugins: extendDefaultPlugins([ { name: "removeDimensions", active: true }, { name: "removeViewBox", active: false } ]) }) return result.data || data } /* ---------------------------------------------------------------------------- * Program * ------------------------------------------------------------------------- */ /* Copy all dependencies */ const dependencies$ = concat( /* Copy Material Design icons */ ...["*.svg", "../LICENSE"] .map(pattern => copyAll(pattern, { src: "node_modules/@mdi/svg/svg", out: `${base}/.icons/material`, ...process.argv.includes("--optimize") && { transform: async data => minsvg(data) } })), /* Copy GitHub octicons */ ...["*.svg", "../../LICENSE"] .map(pattern => copyAll(pattern, { src: "node_modules/@primer/octicons/build/svg", out: `${base}/.icons/octicons`, ...process.argv.includes("--optimize") && { transform: async data => minsvg(data) } })), /* Copy FontAwesome icons */ ...["**/*.svg", "../LICENSE.txt"] .map(pattern => copyAll(pattern, { src: "node_modules/@fortawesome/fontawesome-free/svgs", out: `${base}/.icons/fontawesome`, ...process.argv.includes("--optimize") && { transform: async data => minsvg(data) } })) ) /* Copy all assets */ const assets$ = concat( /* Copy icons, images and configurations */ ...[".icons/*.svg", "assets/images/*", "**/*.{py,yml}"] .map(pattern => copyAll(pattern, { src: "src", out: base })), /* Copy and minify template files */ copyAll("**/*.html", { src: "src", out: base, transform: async data => { const metadata = require("../package.json") const banner = "{#-\n" + " This file was automatically generated - do not edit\n" + "-#}\n" /* Normalize line feeds and minify HTML */ const html = data.replace(/\r\n/gm, "\n") return banner + minhtml(html, { collapseBooleanAttributes: true, includeAutoGeneratedTags: false, minifyCSS: true, minifyJS: true, removeComments: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true }) /* Remove empty lines without collapsing everything */ .replace(/^\s*[\r\n]/gm, "") /* Write theme version into template */ .replace("$md-name$", metadata.name) .replace("$md-version$", metadata.version) } }) ) /* Transform stylesheets with SASS and PostCSS */ const stylesheets$ = resolve("**/[!_]*.scss", { cwd: "src" }) .pipe( concatMap(file => transformStyle({ src: `src/${file}`, out: ext(`${base}/${file}`, ".css") })) ) /* Transform stylesheets with SASS and PostCSS */ const javascripts$ = resolve("**/{bundle,search}.ts", { cwd: "src" }) .pipe( concatMap(file => transformScript({ src: `src/${file}`, out: ext(`${base}/${file}`, ".js") })) ) /* Add content hashes to assets and replace occurrences */ const manifest$ = defer(() => resolve(`${base}/**/*.{css,js}`) .pipe( takeWhile(() => process.argv.includes("--optimize")), concatMap(asset => from(fs.readFile(asset, "utf8")) .pipe( map(data => createHash("sha256").update(data).digest("hex")), switchMap(hash => of(`${asset}`, `${asset}.map`) .pipe( switchMap(file => fs.rename( file, file.replace(/\b(?=\.)/, `.${hash.slice(0, 8)}.min`) )) ) ) ) ) ) ) /* Copy Lunr.js search stemmers and segmenter */ const stemmers$ = ["min/*.js", "tinyseg.js"] .map(pattern => copyAll(pattern, { src: "node_modules/lunr-languages", out: `${base}/assets/javascripts/lunr` })) /* ------------------------------------------------------------------------- */ /* Put everything together */ concat( dependencies$, merge( assets$, stylesheets$, javascripts$ ), manifest$, stemmers$ ) .subscribe() // .subscribe(console.log)