"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; };