Skip to content

Commit 9a8dbc5

Browse files
authored
Refactor to simplify plugin logic (#9)
- Removes unneeded complex logic from the plugin. - Less code.
1 parent c8b64c6 commit 9a8dbc5

File tree

10 files changed

+249
-354
lines changed

10 files changed

+249
-354
lines changed

.vscode/launch.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
{
22
"version": "0.2.0",
33
"configurations": [
4+
{
5+
"name": "Debug tests",
6+
"preLaunchTask": "Build",
7+
"request": "launch",
8+
"cwd": "${workspaceFolder}",
9+
"type": "node",
10+
"runtimeExecutable": "npm",
11+
"args": [
12+
"run",
13+
"test"
14+
],
15+
"env": {
16+
"NODE_OPTIONS": "--inspect",
17+
},
18+
},
419
{
520
"name": "Next.js: debug server-side",
621
"preLaunchTask": "Build",

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,13 @@ This improves readability and follows the same pattern as regular CSS.
8585
When the plugin finds `'.module.css'` import in the file, it will transform
8686
**all** CSS classnames to use the imported CSS module. However, you may want
8787
to use regular CSS classnames and prevent transformations on them. This
88-
can be done by adding `:g` at the end of the classname:
88+
can be done by adding `g:` at the start of the classname:
8989

9090
```jsx
9191
import "./style.module.css"
9292

9393
function Component() {
94-
return <div className="card-layout:g card-rnd-1"></div>
94+
return <div className="g:card-layout card-rnd-1"></div>
9595
}
9696
```
9797

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

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

125125
```jsx
126-
import "./layout.module.css:layout"
127-
import "./component.module.css:com"
126+
import "layout:./layout.module.css"
127+
import "com:./component.module.css"
128128
```
129129

130130
And use the same labels for writing your classnames:
131131

132132
```jsx
133133
function Component() {
134134
return (
135-
<ul className="food-items:layout">
136-
<li className="food-item:com"></li>
137-
<li className="food-item:com"></li>
135+
<ul className="layout:food-items">
136+
<li className="com:food-item"></li>
137+
<li className="com:food-item"></li>
138138
</ul>
139139
)
140140
}

src/plugin.ts

Lines changed: 49 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { types as t } from "@babel/core";
33
import type babel from "@babel/core";
44
import chalk from "chalk";
55

6-
import { getImportInfo, getTemplFromStrCls } from "./transforms.js";
6+
import { transformClassNames, transformImport } from "./transforms.js";
77
import { CSSModuleError } from "./utils.js";
88

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

18-
if (path.node.specifiers.length > 1 && !t.isImportDefaultSpecifier(path.node.specifiers[0])) {
19-
// Syntax: import { classA, classB } from "./m1.module.css"
20-
throw new CSSModuleError(`Import CSS-Module as a default import on '${chalk.cyan(path.node.source.value)}'`);
21-
}
22-
if (path.node.specifiers.length > 1) {
23-
// Syntax: import style, { classA, classB } from "./m1.module.css"
24-
throw new CSSModuleError(`More than one import found on '${chalk.cyan(path.node.source.value)}'`);
25-
}
26-
27-
let moduleInfo = getImportInfo(path.node);
28-
if (moduleInfo.hasSpecifier) {
29-
let importSpecifier = path.node.specifiers[0].local;
30-
if (importSpecifier.name in state.pluginState.modules.namedModules) {
31-
throw new CSSModuleError(`CSS-Module ${chalk.yellow(`'${importSpecifier.name}'`)} has already been declared`);
32-
}
18+
// 1. Transform import declaration
19+
const idGenerator = (hint: string) => path.scope.generateUidIdentifier(hint);
20+
const res = transformImport(path.node, idGenerator);
21+
path.replaceWith(res.transformedNode);
22+
path.skip();
3323

34-
// saving new module
35-
state.pluginState.modules.namedModules[importSpecifier.name] = importSpecifier.name;
36-
} else if (moduleInfo.default) {
37-
if (state.pluginState.modules.defaultModule) {
38-
throw new CSSModuleError(`Only one default css-module import is allowed. Provide names for all except the default module`);
24+
// 2. Add CSS module to the list
25+
const importSpecifier = res.transformedNode.specifiers[0].local.name;
26+
if (res.generatedSpecifier) {
27+
if (res.moduleLabel) {
28+
addCheckedModule(res.moduleLabel, importSpecifier, pluginState.modules);
29+
} else {
30+
// this is a default module
31+
addCheckedDefaultModule(importSpecifier, pluginState.modules);
3932
}
40-
41-
let importSpecifier = path.scope.generateUidIdentifier("style");
42-
let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)];
43-
let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value));
44-
path.replaceWith<t.ImportDeclaration>(newImportDeclaration);
45-
46-
// saving this module as the default module for the current translation unit.
47-
state.pluginState.modules.defaultModule = importSpecifier.name;
4833
} else {
49-
if (moduleInfo.moduleName in state.pluginState.modules.namedModules) {
50-
throw new CSSModuleError(`CSS-Module ${chalk.yellow(`'${moduleInfo.moduleName}'`)} has already been declared`);
34+
// Verify that the module label is unique.
35+
// Prevents scenarios where the same value is used as both a module
36+
// label and an import specifier in different import declarations.
37+
addCheckedModule(importSpecifier, importSpecifier, pluginState.modules);
38+
39+
if (res.moduleLabel && res.moduleLabel != importSpecifier) {
40+
// Make module label an alias to the provided specifier
41+
addCheckedModule(res.moduleLabel, importSpecifier, pluginState.modules);
5142
}
52-
53-
let importSpecifier = path.scope.generateUidIdentifier(moduleInfo.moduleName);
54-
let newSpecifiers = [t.importDefaultSpecifier(importSpecifier)];
55-
let newImportDeclaration = t.importDeclaration(newSpecifiers, t.stringLiteral(path.node.source.value));
56-
path.replaceWith<t.ImportDeclaration>(newImportDeclaration);
57-
58-
// saving new module
59-
state.pluginState.modules.namedModules[moduleInfo.moduleName] = importSpecifier.name;
6043
}
61-
62-
// strips away module name from the source
63-
path.node.source.value = moduleInfo.moduleSource; // this inplace replacment does not causes any problem with the ast
64-
path.skip();
6544
}
6645

67-
function JSXAttribute(path: NodePath<t.JSXAttribute>, state: PluginPass) {
68-
const firstNamedModule = getFirstNamedModule(state.pluginState.modules.namedModules);
46+
function JSXAttribute(path: NodePath<t.JSXAttribute>, { pluginState }: PluginPass) {
47+
const firstNamedModule = getFirstNamedModule(pluginState.modules.namedModules);
6948

7049
// we only support className attribute having a string value
71-
if (path.node.name.name != "className" || !t.isStringLiteral(path.node.value)) {
50+
if (path.node.name.name != "className" || !path.node.value || !t.isStringLiteral(path.node.value)) {
7251
return;
7352
}
7453
// className values should be transformed only if we ever found a css module.
7554
// FirstNamedModule signifies that we found at least one named css module.
76-
if (!state.pluginState.modules.defaultModule && !firstNamedModule) {
55+
if (!pluginState.modules.defaultModule && !firstNamedModule) {
7756
return;
7857
}
7958

8059
// saving path for error messages
8160
CSSModuleError.path = path;
8261

8362
// if no default modules is available, make the first modules as default
84-
if (!state.pluginState.modules.defaultModule) {
63+
if (!pluginState.modules.defaultModule) {
8564
if (firstNamedModule) {
86-
state.pluginState.modules.defaultModule = state.pluginState.modules.namedModules[firstNamedModule];
65+
pluginState.modules.defaultModule = pluginState.modules.namedModules[firstNamedModule];
8766
}
8867
}
8968

90-
let fileCSSModules = state.pluginState.modules;
91-
let templateLiteral = getTemplFromStrCls(path.node.value.value, fileCSSModules);
69+
let classNames = path.node.value.value;
70+
let templateLiteral = transformClassNames(classNames, pluginState.modules);
9271
let jsxExpressionContainer = t.jsxExpressionContainer(templateLiteral);
9372
let newJSXAttr = t.jsxAttribute(t.jsxIdentifier("className"), jsxExpressionContainer);
9473
path.replaceWith(newJSXAttr);
9574
path.skip();
9675
}
9776

9877
function API(): PluginObj<PluginPass> {
99-
/**
100-
* Sets up the initial state of the plugin
101-
*/
78+
// Set up the initial state for the plugin
10279
function pre(this: PluginPass): void {
10380
this.pluginState = {
10481
modules: {
@@ -116,16 +93,32 @@ function API(): PluginObj<PluginPass> {
11693
};
11794
}
11895

96+
function addCheckedModule(moduleLabel: string, module: string, modules: Modules) {
97+
if (moduleLabel in modules.namedModules) {
98+
throw new CSSModuleError(`Duplicate CSS module '${chalk.yellow(module)}' found`);
99+
}
100+
modules.namedModules[moduleLabel] = module;
101+
}
102+
103+
function addCheckedDefaultModule(module: string, modules: Modules) {
104+
if (modules.defaultModule) {
105+
throw new CSSModuleError(`Only one default css-module import is allowed. Provide names for all except the default module`);
106+
}
107+
modules.defaultModule = module;
108+
}
109+
119110
export default API;
120111

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

117+
type CSSModuleLabel = string;
118+
type CSSModuleIdentifier = string;
126119
export type Modules = {
127120
defaultModule?: string;
128-
namedModules: { [moduleName: string]: string };
121+
namedModules: { [moduleLabel: CSSModuleLabel]: CSSModuleIdentifier };
129122
};
130123

131124
type PluginState = {

0 commit comments

Comments
 (0)