import { autoprefixer, postcss, postcssImport } from "../deps/postcss.ts";
import { merge } from "../core/utils/object.ts";
import { concurrent } from "../core/utils/concurrent.ts";
import { resolveInclude } from "../core/utils/path.ts";
import { readFile } from "../core/utils/read.ts";
import { Page } from "../core/file.ts";
import { prepareAsset, saveAsset } from "./source_maps.ts";
import { warnUntil } from "../core/utils/log.ts";
import { bytes } from "../core/utils/format.ts";
import { browsers, versionString } from "../core/utils/browsers.ts";
import { getFile, isFromCdn } from "../core/utils/cdn.ts";
import type Site from "../core/site.ts";
import type { SourceMap } from "./source_maps.ts";
import type { Item } from "../core/debugbar.ts";
export interface Options {
/**
* Custom includes path for `postcss-import`
* @default `site.options.includes`
*/
includes?: string | false;
/**
* Plugins to use by postcss
* @default `[autoprefixer()]`
*/
plugins?: unknown[];
/** Set `false` to remove the default plugins */
useDefaultPlugins?: boolean;
}
// Default options
export const defaults = {
useDefaultPlugins: true,
} satisfies Options;
const defaultPlugins = [
autoprefixer({
overrideBrowserslist: [
`android >= ${versionString(browsers.chrome_android)}`,
`chrome >= ${versionString(browsers.chrome)}`,
`edge >= ${versionString(browsers.edge)}`,
`firefox >= ${versionString(browsers.firefox)}`,
`ios_saf >= ${versionString(browsers.safari_ios)}`,
`safari >= ${versionString(browsers.safari)}`,
],
}),
];
/**
* A plugin to load all CSS files and process them using PostCSS
* @see https://lume.land/plugins/postcss/
*/
export function postCSS(userOptions?: Options) {
return (site: Site) => {
const options = merge(
{ ...defaults, includes: site.options.includes },
userOptions,
);
const plugins = [...options.plugins ?? []];
if (options.useDefaultPlugins) {
plugins.unshift(...defaultPlugins);
}
if (options.includes) {
plugins.unshift(configureImport(site, options.includes));
site.ignore(options.includes);
}
// @ts-ignore: Argument of type 'unknown[]' is not assignable to parameter of type 'AcceptedPlugin[]'.
const runner = postcss(plugins);
site.hooks.postcss = (callback) => callback(runner);
site.hooks.addPostcssPlugin = (plugin) => {
runner.use(plugin);
};
site.process([".css"], processPostcss);
site.filter("postcss", filter, true);
function processPostcss(files: Page[]) {
const hasPages = warnUntil(
"[postcss plugin] No CSS files found. Make sure to add the CSS files with site.add()",
files.length,
);
if (!hasPages) {
return;
}
const item = site.debugBar?.buildItem(
"[postcss plugin] Processing completed",
);
return concurrent(files, (file) => postCss(file, item));
}
async function postCss(file: Page, item?: Item) {
const { content, filename, sourceMap, enableSourceMap } = prepareAsset(
site,
file,
);
const to = site.dest(file.outputPath);
const map = enableSourceMap
? {
inline: false,
prev: sourceMap,
annotation: false,
}
: undefined;
// Process the code with PostCSS
const result = await runner.process(content, { from: filename, to, map });
if (item) {
item.items ??= [];
item.items.push({
title: file.data.url,
details: bytes(result.css.length),
items: result.warnings().map((warning) => ({
title: warning.toString(),
context: "warning",
})),
});
}
saveAsset(
site,
file,
result.css,
result.map?.toJSON() as unknown as SourceMap,
);
}
async function filter(code: string): Promise {
const result = await runner.process(code, { from: undefined });
return result.css;
}
};
}
/**
* Function to configure the postcssImport
* using the Lume reader and the includes loader
*/
function configureImport(site: Site, includes: string) {
return postcssImport({
/** Resolve the import path */
resolve(id: string, basedir: string) {
if (isFromCdn(id)) {
return "/" + id;
}
return resolveInclude(id, includes, basedir);
},
/** Load the content (using the Lume reader) */
async load(file: string) {
if (file.startsWith("/") && isFromCdn(file.slice(1))) {
const url = getFile(file.slice(1));
return await readFile(url);
}
const content = await site.getContent(file, false);
if (content === undefined) {
throw new Error(`File ${file} not found`);
}
return content;
},
});
}
export default postCSS;
/** Extends Helpers interface */
declare global {
namespace Lume {
export interface Helpers {
/** @see https://lume.land/plugins/postcss/ */
postcss: (code: string) => Promise;
}
}
}