/* * 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 { EMPTY, concat, defer, from, merge, of } from "rxjs" import { concatMap, map, mergeMap, switchMap, toArray } from "rxjs/operators" import { extendDefaultPlugins, optimize } from "svgo" import { copy, copyAll } from "./copy" import { base, cachebust, 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(() => process.argv.includes("--optimize") ? resolve("**/*.{css,js}", { cwd: base }) : EMPTY ) .pipe( concatMap(file => from(fs.readFile(`${base}/${file}`, "utf8")) .pipe( map(data => createHash("sha256").update(data).digest("hex")), switchMap(hash => of(`${file}`, `${file}.map`) .pipe( concatMap(part => cachebust(part, hash, { cwd: base })) ) ) ) ), toArray(), map(tuples => new Map(tuples)), mergeMap(manifest => concat( // TODO: split this into two. manifest + cachebust! ...["base.html", "overrides/main.html"] .map(file => copy({ src: `${base}/${file}`, out: `${base}/${file}`, transform: async data => [...manifest.entries()] .reduce((content, [key, value]) => content .replace( new RegExp(`('|")${key}\\1`, "g"), `$1${value}$1` ), data ) })), // TODO: interate this into the actual compilation... ...[...manifest.keys()] .filter(file => !file.endsWith(".map")) .map(file => copy({ src: `${base}/${manifest.get(file)!}`, out: `${base}/${manifest.get(file)!}`, transform: async data => data.replace( path.basename(file), path.basename(manifest.get(file)!), ) })) )) ) /* 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)