UNPKG

@modular-css/css-to-js

Version:

modular-css powered conversion of CSS to JavaScript

368 lines (275 loc) 10.7 kB
"use strict"; const path = require("path"); const dedent = require("dedent"); const identifierfy = require("identifierfy"); const extend = require("just-extend"); const Processor = require("@modular-css/processor"); const relative = require("@modular-css/processor/lib/relative.js"); const slash = (file) => file.replace(/\/|\\/g, "/"); const DEFAULT_VALUES = "$values"; const DEFAULTS = { __proto__ : null, dev : false, variableDeclaration : "const", defaultExport : true, styleExport : false, namedExports : { rewriteInvalid : true, warn : true, }, }; const { selectorKey, isFile, isValue, isSelector, } = Processor; const deconflict = (map, source) => { const safe = identifierfy(source); let idx = 0; let proposal = safe; while(map.has(proposal)) { proposal = `${safe}${++idx}`; } map.set(proposal, source); return proposal; }; const prop = ([ key, value ]) => (key === value ? key : `${JSON.stringify(key)} : ${value}`); const esm = (key, value) => { const safeKey = identifierfy(key); const safeValue = identifierfy(value); return safeKey === safeValue ? safeKey : `${safeKey} as ${safeValue}`; }; // eslint-disable-next-line max-statements -- too hard to split out exports.transform = (file, processor, opts = {}) => { // Remove processor from opts so just-extend doesn't recurse forever const { processor : _optcessor, ..._opts } = opts; const options = extend(true, Object.create(null), DEFAULTS, _opts); let { rewriteInvalid, warn : warnOnInvalid } = options.namedExports; if(typeof options.namedExports === "boolean") { rewriteInvalid = options.namedExports; warnOnInvalid = options.namedExports; } const { variableDeclaration } = options; const { graph } = processor; const id = processor.normalize(file); const details = processor.files[id]; const warnings = []; const dependencies = new Set(); // All used identifiers const identifiers = new Map(); // External identifiers mapped to their unique names const externalsMap = new Map(); // Internal identifiers mapped to their unique names const internalsMap = new Map(); // Map of files & their imports const importsMap = new Map(); const out = []; const defaultExports = []; const namedExports = []; const valueExports = new Map(); // All the class keys exported by this module const exportedKeys = new Set(); // Bail early if we were given a file that doesn't exist in the processor instance if(!details) { warnings.push(`${file} doesn't exist in the processor instance`); return { code : "export default null;", dependencies, namedExports, warnings, }; } // Only want direct dependencies and any first-level dependencies // of this file to be processed graph.directDependenciesOf(Processor.fileKey(id)).forEach((dep) => { dependencies.add(dep); graph.directDependenciesOf(dep).forEach((d) => { dependencies.add(d); }); }); // create import statements for all of the values used in compositions /* eslint-disable-next-line max-statements -- this function is long */ dependencies.forEach((depKey) => { const data = graph.getNodeData(depKey); const { file : depFile } = data; if(!importsMap.has(depFile)) { importsMap.set(depFile, new Map()); } const imported = importsMap.get(depFile); // Local deps are ignored at this point if(depFile === id) { return; } if(isFile(depKey)) { // Add each selector this file depends on to the imports list data.selectors.forEach((key) => { const { selector: name } = graph.getNodeData(key); const unique = deconflict(identifiers, name); externalsMap.set(selectorKey(depFile, name), unique); imported.set(name, unique); }); return; } // @value references need to be specially imported & handled if(isValue(depKey)) { const { value, namespace, alias } = data; const { name : filename } = path.parse(depFile); const importName = `$${filename}Values`; let unique; if(!externalsMap.has(importName)) { unique = deconflict(identifiers, importName); // Add a values import to the imports list externalsMap.set(importName, unique); } else { unique = externalsMap.get(importName); } imported.set(DEFAULT_VALUES, unique); // Add @values namespace to the exported values block if(namespace) { // Don't want to import namespaces multiple times // if(!imported.has(DEFAULT_VALUES)) { valueExports.set(value, unique); // } } else { valueExports.set(value, `${unique}[${JSON.stringify(alias || value)}]`); } return; } if(data.global) { // wrap selector in quotes so it isn't exported as a variable externalsMap.set(selectorKey(depFile, data.selector), `"${data.selector}"`); return; } }); // Write out all the imports importsMap.forEach((imports, from) => { if(!imports.size) { return; } const source = options.relativeImports ? `./${path.relative(path.dirname(file), from)}` : from; const names = [ ...imports ].map(([ key, value ]) => esm(key, value)); out.push(`import { ${names.join(", ")} } from "${slash(source)}";`); }); // Add the rest of the exported keys in whatever order because it doesn't matter Object.keys(details.classes).forEach((key) => exportedKeys.add(key)); // Add default exports for all the @values Object.keys(details.values).forEach((key) => { const { value, external } = details.values[key]; // Externally-imported @values were already added, so skip them if(external) { return; } const unique = deconflict(identifiers, key); internalsMap.set(value, unique); out.push(`${variableDeclaration} ${unique} = ${JSON.stringify(value)};`); valueExports.set(key, unique); }); if(valueExports.size) { const unique = deconflict(identifiers, DEFAULT_VALUES); out.push(dedent(`${variableDeclaration} ${unique} = { ${[ ...valueExports ].map(prop).join(",\n")}, };`)); defaultExports.push([ DEFAULT_VALUES, unique ]); namedExports.push(esm(unique, DEFAULT_VALUES)); } // Collect local exports first exportedKeys.forEach((key) => { const unique = deconflict(identifiers, key); internalsMap.set(key, unique); }); // Create vars representing exported classes & use them in local var definitions exportedKeys.forEach((key) => { const elements = []; const sKey = selectorKey(id, key); const unique = internalsMap.get(key); // Build the list of composed classes for this class if(graph.hasNode(sKey)) { graph.directDependenciesOf(sKey).forEach((dep) => { const { file: src, selector } = graph.getNodeData(dep); // Get the value from the right place elements.push(src !== id ? externalsMap.get(dep) : internalsMap.get(selector) ); }); } elements.push(...details.classes[key].map((t) => JSON.stringify(t))); out.push(`${variableDeclaration} ${unique} = ${elements.join(` + " " + `)};`); defaultExports.push([ key, unique ]); const namedExport = identifierfy(key); if(namedExport === key) { namedExports.push(esm(unique, key)); } else if(rewriteInvalid) { if(warnOnInvalid) { warnings.push(`"${key}" is not a valid JS identifier, exported as "${namedExport}"`); } namedExports.push(esm(unique, namedExport)); } else if(warnOnInvalid) { warnings.push(`"${key}" is not a valid JS identifier`); } }); const classes = defaultExports.map(prop).join(",\n"); if(options.dev) { const source = relative(processor.options.cwd, id); const issue = options.dev.warn ? `console.warn(key, "is not exported by ${source}")` : `throw new ReferenceError(key + " is not exported by ${source}");`; if(options.dev.coverage) { out.push(dedent(` if(!globalThis.mcssCoverage) { globalThis.mcssCoverage = Object.create(null); } globalThis.mcssCoverage["${source}"] = Object.create(null); `)); for(const key of exportedKeys) { const graphKey = selectorKey(file, key); const graphDeps = graph.hasNode(graphKey) ? graph.directDependentsOf(graphKey) : []; const selectorDeps = graphDeps.filter(isSelector); out.push(dedent(` globalThis.mcssCoverage["${source}"]["${key}"] = ${selectorDeps.length}; `)); } out.push(""); } out.push(dedent(` const data = { ${classes} }; export default new Proxy(data, { get(tgt, key) { if(key in tgt) { ${options.dev.coverage ? `globalThis.mcssCoverage["${source}"][key]++;\n` : ""} return tgt[key]; } ${issue} } }) `)); } else if(options.defaultExport) { out.push(dedent(` export default { ${classes} }; `)); } if(namedExports.length) { out.push(""); out.push(dedent(` export { ${namedExports.join(",\n")} }; `)); } if(options.styleExport) { out.push(`export const styles = ${JSON.stringify(details.result.css)};`); } const code = out.join("\n"); // Return JS representation return { code, dependencies, namedExports, warnings, }; };