@theemo/build
Version:
Theme Builder
203 lines (196 loc) • 5.76 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import { transform } from "lightningcss";
import { readPackageSync } from "read-pkg";
import { writePackageSync } from "write-package";
//#region ../theme/dist/index.js
/**
* Color Contrasts
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast
*/
const ColorContrast = {
NoPreference: "no-preference",
More: "more",
Less: "less",
Custom: "custom"
};
/**
* Color Scheme
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
*/
const ColorScheme = {
Light: "light",
Dark: "dark"
};
/**
* Motion
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
*/
const Motion = {
NoPreference: "no-preference",
Reduce: "reduce"
};
const BrowserMechanic = {
ColorScheme: "color-scheme",
ColorContrast: "color-contrast",
Motion: "motion"
};
/**
* Checks wether the given feature uses mode behavior
*
* @param feature the feature in question
* @returns `true` for a mode behavior, otherwise `false`
*/
function isModalFeature(feature) {
return feature.defaultOption !== void 0;
}
const queries = {
[BrowserMechanic.ColorScheme]: {
[ColorScheme.Dark]: "(prefers-color-scheme: dark)",
[ColorScheme.Light]: "(prefers-color-scheme: light)"
},
[BrowserMechanic.ColorContrast]: {
[ColorContrast.NoPreference]: "(prefers-color-contrast: no-preference)",
[ColorContrast.More]: "(prefers-color-contrast: more)",
[ColorContrast.Less]: "(prefers-color-contrast: less)",
[ColorContrast.Custom]: "(prefers-color-contrast: custom)"
},
[BrowserMechanic.Motion]: {
[Motion.NoPreference]: "(prefers-reduced-motion: no-preference)",
[Motion.Reduce]: "(prefers-reduced-motion)"
}
};
//#endregion
//#region src/config.ts
const DEFAULT_CONFIG = {
outDir: "dist",
features: [],
lightningcss: true
};
function configWithDefaults(config) {
return {
...DEFAULT_CONFIG,
...config
};
}
//#endregion
//#region src/features.ts
function isModalBuildFeature(feature) {
return isModalFeature(feature);
}
function isColorSchemeFeature(feature) {
return feature.browserFeature === BrowserMechanic.ColorScheme;
}
function isMediaQueryFeature(feature) {
return feature.browserFeature === BrowserMechanic.ColorContrast || feature.browserFeature === BrowserMechanic.Motion;
}
//#endregion
//#region src/build.ts
const MEDIA_QUERY = {
[BrowserMechanic.ColorScheme]: "prefers-color-scheme",
[BrowserMechanic.ColorContrast]: "prefers-contrast",
[BrowserMechanic.Motion]: "prefers-reduced-motion"
};
function readFile(file) {
const contents = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
return contents;
}
function combineFiles(config) {
const contents = [];
for (const file of config.files ?? []) contents.push(readFile(file).trim());
return contents;
}
function buildColorSchemeFeature(feature) {
return `
:root {
color-scheme: ${feature.options.join(" ")};
}
[data-theemo-color-scheme="light"] {
color-scheme: light;
}
[data-theemo-color-scheme="dark"] {
color-scheme: dark;
}
`.trim();
}
function buildFeature(feature) {
const contents = [];
for (const [option, file] of Object.entries(feature.options)) {
const fileContents = readFile(file);
const selectors = option === feature.defaultOption ? [":root"] : [];
selectors.push(`[data-theemo-${feature.name}="${option}"]`);
const css = fileContents.replace(":root", selectors.join(", ")).trim();
contents.push(css);
}
return contents;
}
function buildMediaQueryBrowserFeature(feature) {
const contents = [];
const mediaQuery = MEDIA_QUERY[feature.browserFeature];
for (const [option, file] of Object.entries(feature.options)) {
const fileContents = readFile(file);
const css = `@media (${mediaQuery}: ${option}) {
${fileContents}
}`;
contents.push(css);
}
return contents;
}
function buildFeatures(config) {
const contents = [];
for (const feature of config.features) {
if (isColorSchemeFeature(feature)) contents.push(buildColorSchemeFeature(feature));
else if (isModalBuildFeature(feature)) contents.push(...buildFeature(feature));
if (isMediaQueryFeature(feature)) contents.push(...buildMediaQueryBrowserFeature(feature));
}
return contents;
}
function getThemeName() {
const data = readPackageSync();
return data.theemo?.name ?? data.name;
}
function buildFiles(config) {
const name = getThemeName();
const contents = [...combineFiles(config), ...buildFeatures(config)];
if (!fs.existsSync(config.outDir)) fs.mkdirSync(config.outDir);
const outFile = path.join(config.outDir, `${name}.css`);
const css = contents.join("\n\n");
fs.writeFileSync(outFile, css);
if (config.lightningcss !== false) {
const options = typeof config.lightningcss === "object" ? config.lightningcss : {};
const { code } = transform({
code: Buffer.from(css),
filename: outFile,
...options
});
fs.writeFileSync(outFile, code);
}
}
function updatePackage(config) {
const packageJson = readPackageSync({ normalize: false });
const theemo = packageJson.theemo;
theemo.file = path.join(config.outDir, `${theemo.name}.css`);
theemo.features = config.features.map((f) => ({
...f,
options: Array.isArray(f.options) ? f.options : Object.keys(f.options)
}));
if (!Array.isArray(packageJson.keywords)) packageJson.keywords = [];
if (!packageJson.keywords.includes("theemo-theme")) packageJson.keywords.push("theemo-theme");
writePackageSync(packageJson);
}
/**
* Build a theme, which can be managed by theemo
*
* @param config The configuration for the theme and its behavior
*/
function build(config) {
const defaultConfig = configWithDefaults(config);
buildFiles(defaultConfig);
updatePackage(defaultConfig);
}
//#endregion
export { build };
//# sourceMappingURL=index.js.map