Skip to content

Commit

Permalink
Refactor to simplify plugin logic (#9)
Browse files Browse the repository at this point in the history
- Removes unneeded complex logic from the plugin.
- Less code.
  • Loading branch information
tusharsnx authored Jul 5, 2024
1 parent c8b64c6 commit 9a8dbc5
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 354 deletions.
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug tests",
"preLaunchTask": "Build",
"request": "launch",
"cwd": "${workspaceFolder}",
"type": "node",
"runtimeExecutable": "npm",
"args": [
"run",
"test"
],
"env": {
"NODE_OPTIONS": "--inspect",
},
},
{
"name": "Next.js: debug server-side",
"preLaunchTask": "Build",
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ This improves readability and follows the same pattern as regular CSS.
When the plugin finds `'.module.css'` import in the file, it will transform
**all** CSS classnames to use the imported CSS module. However, you may want
to use regular CSS classnames and prevent transformations on them. This
can be done by adding `:g` at the end of the classname:
can be done by adding `g:` at the start of the classname:

```jsx
import "./style.module.css"

function Component() {
return <div className="card-layout:g card-rnd-1"></div>
return <div className="g:card-layout card-rnd-1"></div>
}
```

Expand Down Expand Up @@ -120,21 +120,21 @@ function Component() {
You can use multiple CSS module within a file using Named modules.

To use Named CSS modules, you can add labels to each CSS module import
in the file by adding `:<module-name>` at the end of the path:
in the file by adding `<module-name>:` at the end of the path:

```jsx
import "./layout.module.css:layout"
import "./component.module.css:com"
import "layout:./layout.module.css"
import "com:./component.module.css"
```

And use the same labels for writing your classnames:

```jsx
function Component() {
return (
<ul className="food-items:layout">
<li className="food-item:com"></li>
<li className="food-item:com"></li>
<ul className="layout:food-items">
<li className="com:food-item"></li>
<li className="com:food-item"></li>
</ul>
)
}
Expand Down
105 changes: 49 additions & 56 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { types as t } from "@babel/core";
import type babel from "@babel/core";
import chalk from "chalk";

import { getImportInfo, getTemplFromStrCls } from "./transforms.js";
import { transformClassNames, transformImport } from "./transforms.js";
import { CSSModuleError } from "./utils.js";

function ImportDeclaration(path: NodePath<t.ImportDeclaration>, state: PluginPass) {
function ImportDeclaration(path: NodePath<t.ImportDeclaration>, { pluginState }: PluginPass) {
// we're only interested in scss/sass/css imports
if (!/.module.(s[ac]ss|css)(:.*)?$/iu.test(path.node.source.value)) {
return;
Expand All @@ -15,90 +15,67 @@ function ImportDeclaration(path: NodePath<t.ImportDeclaration>, state: PluginPas
// saving path for error messages
CSSModuleError.path = path;

if (path.node.specifiers.length > 1 && !t.isImportDefaultSpecifier(path.node.specifiers[0])) {
// Syntax: import { classA, classB } from "./m1.module.css"
throw new CSSModuleError(`Import CSS-Module as a default import on '${chalk.cyan(path.node.source.value)}'`);
}
if (path.node.specifiers.length > 1) {
// Syntax: import style, { classA, classB } from "./m1.module.css"
throw new CSSModuleError(`More than one import found on '${chalk.cyan(path.node.source.value)}'`);
}

let moduleInfo = getImportInfo(path.node);
if (moduleInfo.hasSpecifier) {
let importSpecifier = path.node.specifiers[0].local;
if (importSpecifier.name in state.pluginState.modules.namedModules) {
throw new CSSModuleError(`CSS-Module ${chalk.yellow(`'${importSpecifier.name}'`)} has already been declared`);
}
// 1. Transform import declaration
const idGenerator = (hint: string) => path.scope.generateUidIdentifier(hint);
const res = transformImport(path.node, idGenerator);
path.replaceWith(res.transformedNode);
path.skip();

// saving new module
state.pluginState.modules.namedModules[importSpecifier.name] = importSpecifier.name;
} else if (moduleInfo.default) {
if (state.pluginState.modules.defaultModule) {
throw new CSSModuleError(`Only one default css-module import is allowed. Provide names for all except the default module`);
// 2. Add CSS module to the list
const importSpecifier = res.transformedNode.specifiers[0].local.name;
if (res.generatedSpecifier) {
if (res.moduleLabel) {
addCheckedModule(res.moduleLabel, importSpecifier, pluginState.modules);
} else {
// this is a default module
addCheckedDefaultModule(importSpecifier, pluginState.modules);
}

let importSpecifier = path.scope.generateUidIdentifier("style");
let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)];
let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value));
path.replaceWith<t.ImportDeclaration>(newImportDeclaration);

// saving this module as the default module for the current translation unit.
state.pluginState.modules.defaultModule = importSpecifier.name;
} else {
if (moduleInfo.moduleName in state.pluginState.modules.namedModules) {
throw new CSSModuleError(`CSS-Module ${chalk.yellow(`'${moduleInfo.moduleName}'`)} has already been declared`);
// Verify that the module label is unique.
// Prevents scenarios where the same value is used as both a module
// label and an import specifier in different import declarations.
addCheckedModule(importSpecifier, importSpecifier, pluginState.modules);

if (res.moduleLabel && res.moduleLabel != importSpecifier) {
// Make module label an alias to the provided specifier
addCheckedModule(res.moduleLabel, importSpecifier, pluginState.modules);
}

let importSpecifier = path.scope.generateUidIdentifier(moduleInfo.moduleName);
let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)];
let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value));
path.replaceWith<t.ImportDeclaration>(newImportDeclaration);

// saving new module
state.pluginState.modules.namedModules[moduleInfo.moduleName] = importSpecifier.name;
}

// strips away module name from the source
path.node.source.value = moduleInfo.moduleSource; // this inplace replacment does not causes any problem with the ast
path.skip();
}

function JSXAttribute(path: NodePath<t.JSXAttribute>, state: PluginPass) {
const firstNamedModule = getFirstNamedModule(state.pluginState.modules.namedModules);
function JSXAttribute(path: NodePath<t.JSXAttribute>, { pluginState }: PluginPass) {
const firstNamedModule = getFirstNamedModule(pluginState.modules.namedModules);

// we only support className attribute having a string value
if (path.node.name.name != "className" || !t.isStringLiteral(path.node.value)) {
if (path.node.name.name != "className" || !path.node.value || !t.isStringLiteral(path.node.value)) {
return;
}
// className values should be transformed only if we ever found a css module.
// FirstNamedModule signifies that we found at least one named css module.
if (!state.pluginState.modules.defaultModule && !firstNamedModule) {
if (!pluginState.modules.defaultModule && !firstNamedModule) {
return;
}

// saving path for error messages
CSSModuleError.path = path;

// if no default modules is available, make the first modules as default
if (!state.pluginState.modules.defaultModule) {
if (!pluginState.modules.defaultModule) {
if (firstNamedModule) {
state.pluginState.modules.defaultModule = state.pluginState.modules.namedModules[firstNamedModule];
pluginState.modules.defaultModule = pluginState.modules.namedModules[firstNamedModule];
}
}

let fileCSSModules = state.pluginState.modules;
let templateLiteral = getTemplFromStrCls(path.node.value.value, fileCSSModules);
let classNames = path.node.value.value;
let templateLiteral = transformClassNames(classNames, pluginState.modules);
let jsxExpressionContainer = t.jsxExpressionContainer(templateLiteral);
let newJSXAttr = t.jsxAttribute(t.jsxIdentifier("className"), jsxExpressionContainer);
path.replaceWith(newJSXAttr);
path.skip();
}

function API(): PluginObj<PluginPass> {
/**
* Sets up the initial state of the plugin
*/
// Set up the initial state for the plugin
function pre(this: PluginPass): void {
this.pluginState = {
modules: {
Expand All @@ -116,16 +93,32 @@ function API(): PluginObj<PluginPass> {
};
}

function addCheckedModule(moduleLabel: string, module: string, modules: Modules) {
if (moduleLabel in modules.namedModules) {
throw new CSSModuleError(`Duplicate CSS module '${chalk.yellow(module)}' found`);
}
modules.namedModules[moduleLabel] = module;
}

function addCheckedDefaultModule(module: string, modules: Modules) {
if (modules.defaultModule) {
throw new CSSModuleError(`Only one default css-module import is allowed. Provide names for all except the default module`);
}
modules.defaultModule = module;
}

export default API;

function getFirstNamedModule(namedModules: Modules["namedModules"]): string | null {
for (let module in namedModules) return module;
return null;
}

type CSSModuleLabel = string;
type CSSModuleIdentifier = string;
export type Modules = {
defaultModule?: string;
namedModules: { [moduleName: string]: string };
namedModules: { [moduleLabel: CSSModuleLabel]: CSSModuleIdentifier };
};

type PluginState = {
Expand Down
Loading

0 comments on commit 9a8dbc5

Please sign in to comment.