You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

115 lines
4.0 KiB
JavaScript

"use strict";
const pirates = require("pirates");
const path = require("path");
const resolveFrom = require("resolve-from");
const syncpipe = require("syncpipe");
const insertCSS = require("insert-css");
const unreachable = require("@joepie91/unreachable")("icss-register");
const postcss = require("postcss");
const postcssLocalByDefault = require("postcss-modules-local-by-default");
const postcssExtractImports = require("postcss-modules-extract-imports");
const postcssScope = require("postcss-modules-scope");
const postcssValues = require("postcss-modules-values");
const postcssICSSParser = require("postcss-icss-parser");
const genericNames = require("generic-names");
const { validateOptions } = require("@validatem/core");
const isArray = require("@validatem/is-array");
const isBoolean = require("@validatem/is-boolean");
const isFunction = require("@validatem/is-function");
const isString = require("@validatem/is-string");
const oneOf = require("@validatem/one-of");
const arrayOf = require("@validatem/array-of");
const defaultTo = require("@validatem/default-to");
let defaultNameGenerator = genericNames("[name]__[local]---[hash:base64:5]");
module.exports = function registerICSS(_options) {
let options = validateOptions(arguments, [ defaultTo({}), {
generateScopedName: [ isFunction, defaultTo.literal(defaultNameGenerator) ],
mode: [ oneOf([ "local", "global" ]), defaultTo("local") ],
autoExportImports: [ isBoolean, defaultTo(true) ],
// TODO: Actually validate that the array items are compatible PostCSS plugins, using array-of
before: [ isArray, defaultTo([]) ],
after: [ isArray, defaultTo([]) ],
extensions: [ arrayOf(isString), defaultTo([ ".css" ]) ],
postcssOptions: [ defaultTo({}) ] // TODO: Validate for plain object, once is-plain-object works cross-realm
} ]);
let processedFiles = new Map();
let generateScopedName = options.generateScopedName;
function getExports(fullPath) {
if (path.isAbsolute(fullPath)) {
if (processedFiles.has(fullPath)) {
return processedFiles.get(fullPath);
} else {
let htmlExports = require(fullPath);
processedFiles.set(fullPath, htmlExports);
return htmlExports;
}
} else {
throw unreachable(`Path must be absolute, but isn't (${fullPath})`);
}
}
function process(fullPath, code) {
// We cannot reuse the PostCSS instance across files here yet, because the keyReplacer fallback is dependent on the path of the file we're currently processing.
// TODO: Figure out a sensible way to improve upon that.
let postcssInstance = postcss([
... options.before,
postcssValues,
postcssLocalByDefault({ mode: options.mode }),
postcssExtractImports(),
postcssScope({
generateScopedName: generateScopedName
}),
postcssICSSParser({
autoExportImports: options.autoExportImports,
keyReplacer: ({ url, remoteKey }) => {
let resolvedSourcePath = resolveFrom(path.dirname(fullPath), url);
let htmlExports = getExports(resolvedSourcePath);
let mangledName = htmlExports[remoteKey];
if (mangledName == null) {
// TODO: Error type
throw new Error(`No export named '${remoteKey}' found in file ${url}`);
}
/* The replacement is to deal with the difference in multiple-class notation between CSS and HTML; in CSS they are dot-delimited, but in HTML (which the ICSS spec targets) they are space-delimited. We need the CSS notation here. */
return mangledName.replace(/ /g, ".");
}
}),
... options.after
]);
let result = postcssInstance.process(code, {
from: fullPath,
... options.postcssOptions
});
let icssExports = syncpipe(result.messages, [
(_) => _.filter((message) => message.pluginName === "postcss-icss-parser" && message.type === "icss-export"),
(_) => _.map(({ item }) => [ item.name, item.value ]),
(_) => Object.fromEntries(_)
]);
insertCSS(result.css);
return `module.exports = ${JSON.stringify(icssExports)}`;
}
let extensions = options.extensions;
let unhook = pirates.addHook(
(code, fullPath) => process(fullPath, code),
{ exts: extensions }
);
return unhook;
};