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.
116 lines
4.2 KiB
JavaScript
116 lines
4.2 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;
|
|
|
|
// TODO: Figure out a way to allow specifying custom PostCSS (and other transpilation?) configs from within a package
|
|
let unhook = pirates.addHook(
|
|
(code, fullPath) => process(fullPath, code),
|
|
{ exts: extensions, ignoreNodeModules: false }
|
|
);
|
|
|
|
return unhook;
|
|
};
|