A CSS Modules transform to extract local aliases for inline imports
npx @tessl/cli install tessl/npm-postcss-modules-extract-imports@3.1.0PostCSS Modules Extract Imports is a CSS Modules PostCSS plugin that transforms composes declarations by extracting import statements and converting them into :import pseudo-selectors. It processes CSS Modules compose declarations that reference external CSS files and transforms them into standardized import statements with temporary class name aliases, enabling CSS Modules bundlers to properly handle cross-file composition dependencies.
npm install postcss-modules-extract-importsconst extractImports = require("postcss-modules-extract-imports");For PostCSS integration:
const postcss = require("postcss");
const extractImports = require("postcss-modules-extract-imports");For standalone topological sort utility:
const topologicalSort = require("postcss-modules-extract-imports/src/topologicalSort");const postcss = require("postcss");
const extractImports = require("postcss-modules-extract-imports");
// Basic plugin usage
const processor = postcss([extractImports()]);
// Process CSS with compose declarations
const input = `
:local(.continueButton) {
composes: button from "library/button.css";
color: green;
}
`;
const result = processor.process(input);
console.log(result.css);
// Output:
// :import("library/button.css") {
// button: i__imported_button_0;
// }
// :local(.continueButton) {
// composes: i__imported_button_0;
// color: green;
// }Complete Processing Example:
const postcss = require("postcss");
const extractImports = require("postcss-modules-extract-imports");
// Configure plugin with custom options
const processor = postcss([
extractImports({
createImportedName: (name, path) => `imported_${name}_${Math.random().toString(36).substr(2, 5)}`,
failOnWrongOrder: false // Allow flexible import ordering
})
]);
// Complex CSS with multiple imports and compositions
const complexInput = `
.header {
composes: container from "./layout.css";
composes: primary-theme from "./themes.css";
}
.button {
composes: btn base-button from "./components.css";
composes: shadow from "./effects.css";
}
.navigation {
composes: flex-row from "./layout.css";
composes: navbar from global;
}
`;
const result = processor.process(complexInput);
console.log(result.css);Creates a PostCSS plugin instance that transforms CSS Modules compose declarations.
/**
* Creates a PostCSS plugin that extracts and transforms CSS Modules compose declarations
* @param options - Configuration options for the plugin
* @returns PostCSS plugin object
*/
function extractImports(options = {}): PostCSSPlugin;Usage Example:
const plugin = extractImports({
failOnWrongOrder: true,
createImportedName: (importName, path) => `custom_${importName}_from_${path.replace(/[^a-zA-Z0-9]/g, '_')}`
});
const processor = postcss([plugin]);The plugin accepts an options object with the following properties:
interface ExtractImportsOptions {
/** Custom function to generate imported symbol names */
createImportedName?: (importName: string, path: string) => string;
/** When true, throws exception for unpredictable import order dependencies */
failOnWrongOrder?: boolean;
}Override the default import name generation strategy.
/**
* Custom function to generate imported symbol names
* @param importName - The original class name being imported
* @param path - The file path being imported from
* @returns Custom generated name for the imported symbol
*/
type CreateImportedNameFunction = (importName: string, path: string) => string;Default behavior: Generates names like i__imported_button_0, i__imported_card_1, etc. Non-word characters in import names are replaced with underscores.
Usage Example:
const plugin = extractImports({
createImportedName: (importName, path) => {
const cleanPath = path.replace(/[^a-zA-Z0-9]/g, '_');
return `${importName}_from_${cleanPath}`;
}
});
// Input: composes: button from "ui/button.css"
// Output: button_from_ui_button_css instead of i__imported_button_0Advanced Usage Examples:
// Multiple classes from same file
const input = `
.navigation {
composes: button primary large from "./ui/buttons.css";
margin: 10px;
}
`;
// Global composition
const globalInput = `
.localButton {
composes: btn-primary btn-large from global;
color: blue;
}
`;
// Mixed local and external composition
const mixedInput = `
.complexButton {
composes: button from "./base.css", primary-style;
composes: hover-effect from "./animations.css";
}
`;Enable strict validation of import dependency order.
/**
* When true, throws exception for unpredictable import order dependencies
* @default undefined (falsy)
*/
failOnWrongOrder?: boolean;Usage Example:
// This will throw an error due to inconsistent import order
const plugin = extractImports({ failOnWrongOrder: true });
const problematicCSS = `
.aa {
composes: b from "./b.css";
composes: c from "./c.css";
}
.bb {
/* c.css should come before b.css based on .aa rule */
composes: c from "./c.css";
composes: b from "./b.css";
}
`;
// Throws: "Failed to resolve order of composed modules `./b.css`, `./c.css`"The plugin factory function returns a standard PostCSS plugin object.
interface PostCSSPlugin {
/** Plugin identifier for PostCSS */
postcssPlugin: "postcss-modules-extract-imports";
/** Plugin preparation function */
prepare(): PluginMethods;
}
interface PluginMethods {
/** Main processing function called once per CSS root */
Once(root: PostCSSRoot, postcss: PostCSSAPI): void;
}The plugin processes CSS according to these rules:
/* Single class from external file */
composes: button from "library/button.css";
/* Multiple classes from external file */
composes: button primary from "library/button.css";
/* Mixed composition (external and local) */
composes: button from "library/button.css", local-class;
/* Global composition */
composes: button from global;
/* Multiple files in one rule */
composes: button from "ui/button.css", card from "ui/card.css";Input:
:local(.continueButton) {
composes: button primary from "library/button.css";
color: green;
}Output:
:import("library/button.css") {
button: i__imported_button_0;
primary: i__imported_primary_1;
}
:local(.continueButton) {
composes: i__imported_button_0 i__imported_primary_1;
color: green;
}/* Input */
.button {
composes: btn-primary from global;
}
/* Output */
.button {
composes: global(btn-primary);
}The plugin throws PostCSS CssSyntaxError instances for import order conflicts when failOnWrongOrder is enabled.
interface ImportOrderError extends CssSyntaxError {
/** Plugin identifier */
plugin: "postcss-modules-extract-imports";
/** The CSS property that caused the error */
word: "composes";
/** Error message format */
message: string; // "Failed to resolve order of composed modules `path1`, `path2`."
}Error Examples:
const postcss = require("postcss");
const extractImports = require("postcss-modules-extract-imports");
try {
const processor = postcss([extractImports({ failOnWrongOrder: true })]);
// This CSS creates conflicting import order
const result = processor.process(`
.classA {
composes: btn from "./buttons.css";
composes: card from "./cards.css";
}
.classB {
composes: card from "./cards.css";
composes: btn from "./buttons.css"; /* Different order - creates conflict */
}
`);
} catch (error) {
console.log(error.plugin); // "postcss-modules-extract-imports"
console.log(error.word); // "composes"
console.log(error.message); // "Failed to resolve order of composed modules `./buttons.css`, `./cards.css`."
}The package includes a standalone topological sort utility that can be used independently for dependency graph resolution.
/**
* Performs topological sort on a dependency graph
* @param graph - Object where keys are nodes and values are arrays of dependencies
* @param strict - When true, throws error for circular dependencies; when false, resolves best possible order
* @returns Array of nodes in topological order, or Error object if circular dependency detected in strict mode
*/
function topologicalSort(graph: DependencyGraph, strict?: boolean): string[] | TopologicalSortError;
interface DependencyGraph {
[node: string]: string[];
}
interface TopologicalSortError extends Error {
/** Error message */
message: "Nondeterministic import's order";
/** Array of conflicting nodes that create circular dependency */
nodes: [string, string];
}Usage Example:
const topologicalSort = require("postcss-modules-extract-imports/src/topologicalSort");
// Define dependency graph
const graph = {
"file-a.css": ["file-b.css", "file-c.css"], // file-a depends on file-b and file-c
"file-b.css": [], // file-b has no dependencies
"file-c.css": ["file-b.css"], // file-c depends on file-b
};
// Get topological order
const order = topologicalSort(graph, true);
console.log(order); // ["file-b.css", "file-c.css", "file-a.css"]
// Example with circular dependency
const cyclicGraph = {
"file-a.css": ["file-b.css"],
"file-b.css": ["file-a.css"], // Creates cycle
};
const result = topologicalSort(cyclicGraph, true);
if (result instanceof Error) {
console.log(result.message); // "Nondeterministic import's order"
console.log(result.nodes); // ["file-a.css", "file-b.css"]
}The plugin exports a PostCSS identification marker:
/**
* PostCSS plugin identification marker
* @type {boolean}
*/
extractImports.postcss = true;This marker allows PostCSS to identify the function as a valid PostCSS plugin.
/**
* PostCSS plugin configuration options
*/
interface ExtractImportsOptions {
createImportedName?: (importName: string, path: string) => string;
failOnWrongOrder?: boolean;
}
/**
* PostCSS plugin factory function type
*/
type ExtractImportsFactory = (options?: ExtractImportsOptions) => PostCSSPlugin;
/**
* Topological sort function type
*/
type TopologicalSortFunction = (graph: DependencyGraph, strict?: boolean) => string[] | TopologicalSortError;
/**
* Standard PostCSS plugin interface
*/
interface PostCSSPlugin {
postcssPlugin: string;
prepare(): {
Once(root: PostCSSRoot, postcss: PostCSSAPI): void;
};
}
/**
* PostCSS AST Root node
*/
interface PostCSSRoot {
/** Walk through all rules in the CSS */
walkRules(callback: (rule: PostCSSRule) => void): void;
/** Walk through all declarations in the CSS */
walkDecls(pattern: RegExp | string, callback: (decl: PostCSSDeclaration) => void): void;
/** Prepend a node to the beginning */
prepend(node: PostCSSNode): void;
/** Insert a node after another node */
insertAfter(target: PostCSSNode, newNode: PostCSSNode): void;
/** Get the index of a child node */
index(child: PostCSSNode): number;
}
/**
* PostCSS API object passed to plugin methods
*/
interface PostCSSAPI {
/** Create a new CSS rule */
rule(props: { selector: string; raws?: { after?: string } }): PostCSSRule;
/** Create a new CSS declaration */
decl(props: { prop: string; value: string; raws?: { before?: string } }): PostCSSDeclaration;
}
/**
* PostCSS Rule node
*/
interface PostCSSRule {
/** CSS selector */
selector: string;
/** Parent node */
parent: PostCSSNode;
/** Append a declaration to this rule */
append(decl: PostCSSDeclaration): void;
}
/**
* PostCSS Declaration node
*/
interface PostCSSDeclaration {
/** CSS property name */
prop: string;
/** CSS property value */
value: string;
/** Parent rule */
parent: PostCSSRule;
/** Create an error associated with this declaration */
error(message: string, options?: { plugin?: string; word?: string }): Error;
}
/**
* Base PostCSS node
*/
interface PostCSSNode {
/** Node type */
type: string;
/** Parent node */
parent?: PostCSSNode;
}
/**
* Topological sort utility types
*/
interface DependencyGraph {
[node: string]: string[];
}
interface TopologicalSortError extends Error {
message: "Nondeterministic import's order";
nodes: [string, string];
}