-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
148 lines (129 loc) · 4.15 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { PluginObj, types as t, Visitor } from "@babel/core";
import { NodePath } from "@babel/traverse";
import { defaults } from "lodash";
import { dirname, resolve } from "path";
import { getModuleClassNames } from "./utils/getModuleClassNames";
import { isCssImportDeclaration } from "./utils/isCssImportDeclaration";
import { replaceClassNamesInStringLiteral } from "./utils/replaceClassNamesInStringLiteral";
type PluginOptions = {
importIdentifier: "styles";
};
type State = {
opts: PluginOptions;
classNames: Set<string>;
};
const PLUGIN_DEFAULTS = {
importIdentifier: "styles"
};
/**
* These visitors take a string literal, checks to see if it's a valid CSS module
* class name (from `State.classNames`), and if so replaces them with the corresponding
* CSS module lookup e.g. `style["className"]`.
*/
const stringLiteralReplacementVisitors: Visitor<State> = {
StringLiteral: (path, { classNames, opts }) => {
// Ignore strings inside object lookups i.e. obj["className"]
if (path.parentPath.isMemberExpression()) {
return;
}
// Generate a new string
const newPath = replaceClassNamesInStringLiteral(
classNames,
opts.importIdentifier,
path.node
);
// If the literal is inside an object property definition, we need to change
// it to be a computed value instead.
const isProperty = path.parentPath.isObjectProperty();
if (isProperty) {
const parentPath = path.parentPath as NodePath<t.ObjectProperty>;
parentPath.replaceWith(
t.objectProperty(newPath, parentPath.node.value, true, false)
);
}
// Otherwise just replace the literal completely
else {
path.replaceWith(newPath as any);
}
}
};
/**
* These visitors look within JSX `className` attributes, looking for string literals
* which we can process into `style["className"]` lookups, as well as variable references
* in the containing scope which also contain string literals.
*/
const classNameReplacementVisitors: Visitor<State> = {
...stringLiteralReplacementVisitors,
Identifier: ({ node, scope }, state) => {
const binding = scope.getBinding(node.name);
if (binding == null) {
return;
}
binding.path.traverse(stringLiteralReplacementVisitors, state);
}
};
/**
* Theese visitors process all JSX `className` attributes and traverses them
* them using the `replacementVisitors`.
*/
const classNameAttributeVisitors: Visitor<State> = {
JSXAttribute: (path, state) => {
const { name } = path.node.name;
if (name !== "className") {
return;
}
path.traverse(classNameReplacementVisitors, state);
}
};
/**
* These visitors process top-level import statements, looking for all CSS imports.
* When found, we'll retrieve and parse the CSS using PostCSS, to return the top
* level CSS module class names that can be used in the code. We'll then store this
* information in `State.classNames`, to be used in the above processing steps
*/
const importVisitors: Visitor<State> = {
ImportDeclaration: (path, state) => {
const { hub, node } = path;
// Ignore imports of non-CSS files
if (!isCssImportDeclaration(node)) {
return;
}
// Calculate the file path relative to the current file
const scssFilename = resolve(
dirname(hub.file.opts.filename),
node.source.value
);
// Get all relevant class names from the file
const classNames = getModuleClassNames(scssFilename);
// Replace the existing import with a wildcard import, namespaced under
// a module-scope identifier we can reference the keys of. This will be
// the CSS module classname map provided by webpack css-loader with modules
// enabled usually, or possibly PostCSS modules plugin like we use here.
path.replaceWith(
t.importDeclaration(
[
t.importNamespaceSpecifier(
t.identifier(state.opts.importIdentifier)
)
],
node.source
)
);
// Traverse the top-level program path for JSX className attributes
hub.file.path.traverse(classNameAttributeVisitors, {
...state,
classNames
});
}
};
export default (): PluginObj<State> => ({
name: "react-css-modules",
visitor: {
Program: (programPath, state) => {
programPath.traverse(importVisitors, {
...state,
opts: defaults({}, state.opts, PLUGIN_DEFAULTS)
});
}
}
});