mkdocs-material/tools/build/transform/index.ts

259 lines
6.9 KiB
TypeScript
Raw Normal View History

2021-02-20 20:03:53 +03:00
/*
* Copyright (c) 2016-2022 Martin Donath <martin.donath@squidfunk.com>
2021-02-20 20:03:53 +03:00
*
* 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.
*/
2021-02-22 20:19:00 +03:00
import { createHash } from "crypto"
import { build as esbuild } from "esbuild"
import * as fs from "fs/promises"
2021-02-20 20:03:53 +03:00
import * as path from "path"
import postcss, { Plugin, Rule } from "postcss"
2021-02-22 20:19:00 +03:00
import {
2021-11-28 16:54:14 +03:00
EMPTY,
2021-02-22 20:19:00 +03:00
Observable,
2021-11-13 14:05:24 +03:00
catchError,
2021-02-22 20:19:00 +03:00
concat,
defer,
2021-02-20 20:03:53 +03:00
endWith,
ignoreElements,
2021-11-13 14:05:24 +03:00
merge,
of,
2021-02-21 13:59:38 +03:00
switchMap
2021-11-13 14:05:24 +03:00
} from "rxjs"
2021-12-12 14:22:51 +03:00
import { compile } from "sass"
2021-02-20 20:03:53 +03:00
2021-02-26 18:14:18 +03:00
import { base, mkdir, write } from "../_"
2021-02-20 20:03:53 +03:00
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Transform options
*/
interface TransformOptions {
2021-02-22 20:19:00 +03:00
from: string /* Source destination */
to: string /* Target destination */
2021-02-20 20:03:53 +03:00
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
2021-02-22 20:19:00 +03:00
* Base directory for source map resolution
2021-02-20 20:03:53 +03:00
*/
const root = new RegExp(`file://${path.resolve(".")}/`, "g")
2021-02-22 20:19:00 +03:00
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Compute a digest for cachebusting a file
*
* @param file - File
* @param data - File data
*
* @returns File with digest
*/
function digest(file: string, data: string): string {
2021-02-26 18:14:18 +03:00
if (process.argv.includes("--optimize")) {
const hash = createHash("sha256").update(data).digest("hex")
return file.replace(/\b(?=\.)/, `.${hash.slice(0, 8)}.min`)
} else {
return file
}
2021-02-22 20:19:00 +03:00
}
/**
* Custom PostCSS plugin to polyfill newer CSS features
*
* @returns PostCSS plugin
*/
function plugin(): Plugin {
const rules = new Set<Rule>()
return {
postcssPlugin: 'mkdocs-material',
Root (root) {
/* Fallback for :is() */
root.walkRules(/:is\(/, rule => {
if (!rules.has(rule)) {
rules.add(rule)
/* Add prefixed versions */
for (const pseudo of [":-webkit-any(", ":-moz-any("])
rule.cloneBefore({
selectors: rule.selectors.map(selector => (
selector.replace(/:is\(/g, pseudo)
))
})
}
})
}
}
}
plugin.postcss = true
2021-02-20 20:03:53 +03:00
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Transform a stylesheet
*
* @param options - Options
*
* @returns File observable
*/
export function transformStyle(
2021-02-22 20:19:00 +03:00
options: TransformOptions
2021-02-20 20:03:53 +03:00
): Observable<string> {
2021-12-12 14:22:51 +03:00
return defer(() => of(compile(options.from, {
loadPaths: [
2021-02-20 20:03:53 +03:00
"src/assets/stylesheets",
"node_modules/modularscale-sass/stylesheets",
"node_modules/material-design-color",
"node_modules/material-shadows"
],
2021-12-12 14:22:51 +03:00
sourceMap: true
})))
2021-02-20 20:03:53 +03:00
.pipe(
2021-12-12 14:22:51 +03:00
switchMap(({ css, sourceMap }) => postcss([
2021-02-20 20:03:53 +03:00
require("autoprefixer"),
require("postcss-logical"),
require("postcss-dir-pseudo-class"),
plugin,
2021-02-20 20:03:53 +03:00
require("postcss-inline-svg")({
paths: [
`${base}/.icons`
],
encode: false
}),
...process.argv.includes("--optimize")
? [require("cssnano")]
: []
2021-02-20 20:03:53 +03:00
])
.process(css, {
2021-02-22 20:19:00 +03:00
from: options.from,
2021-12-12 14:22:51 +03:00
to: options.to,
2021-02-20 20:03:53 +03:00
map: {
2021-12-12 14:22:51 +03:00
prev: sourceMap,
2021-02-20 20:03:53 +03:00
inline: false
}
})
),
2021-02-27 20:21:02 +03:00
catchError(err => {
console.log(err.formatted || err.message)
2021-11-28 16:54:14 +03:00
return EMPTY
2021-02-27 20:21:02 +03:00
}),
2021-02-22 20:19:00 +03:00
switchMap(({ css, map }) => {
const file = digest(options.to, css)
return concat(
mkdir(path.dirname(file)),
2021-02-26 18:14:18 +03:00
merge(
write(`${file}.map`, `${map}`.replace(root, "")),
write(`${file}`, css.replace(
2021-02-22 20:19:00 +03:00
options.from,
path.basename(file)
)),
2021-02-26 18:14:18 +03:00
)
2021-02-22 20:19:00 +03:00
)
.pipe(
ignoreElements(),
endWith(file)
)
})
2021-02-20 20:03:53 +03:00
)
}
2021-02-20 20:46:28 +03:00
/**
* Transform a script
*
* @param options - Options
*
* @returns File observable
*/
export function transformScript(
2021-02-22 20:19:00 +03:00
options: TransformOptions
2021-02-20 20:46:28 +03:00
): Observable<string> {
2021-02-22 20:19:00 +03:00
return defer(() => esbuild({
entryPoints: [options.from],
2021-02-26 14:31:05 +03:00
target: "es2015",
2021-02-22 20:19:00 +03:00
write: false,
2021-02-20 20:46:28 +03:00
bundle: true,
sourcemap: true,
sourceRoot: "../../../..",
legalComments: "inline",
minify: process.argv.includes("--optimize"),
plugins: [
/* Plugin to minify inlined CSS (e.g. for Mermaid.js) */
{
name: "mkdocs-material/inline",
setup(build) {
build.onLoad({ filter: /\.css/ }, async args => {
const content = await fs.readFile(args.path, "utf8")
const { css } = await postcss([require("cssnano")])
.process(content, {
from: undefined
})
/* Return minified CSS */
return {
contents: css,
loader: "text"
}
2022-04-09 18:17:26 +03:00
})
}
}
]
2021-02-20 20:46:28 +03:00
}))
.pipe(
catchError(() => EMPTY),
2021-02-22 20:19:00 +03:00
switchMap(({ outputFiles: [file] }) => {
const contents = file.text.split("\n")
const [, data] = contents[contents.length - 2].split(",")
return of({
js: file.text,
map: Buffer.from(data, "base64")
})
}),
switchMap(({ js, map }) => {
const file = digest(options.to, js)
return concat(
mkdir(path.dirname(file)),
2021-02-26 18:14:18 +03:00
merge(
write(`${file}.map`, `${map}`),
write(`${file}`, js.replace(
2021-02-22 20:19:00 +03:00
/(sourceMappingURL=)(.*)/,
2021-02-25 17:56:08 +03:00
`$1${path.basename(file)}.map\n`
2021-02-22 20:19:00 +03:00
)),
2021-02-26 18:14:18 +03:00
)
2021-02-22 20:19:00 +03:00
)
.pipe(
ignoreElements(),
endWith(file)
)
})
2021-02-20 20:46:28 +03:00
)
}