-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.ts
139 lines (123 loc) · 4.04 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
import fs from "fs-extra";
import util from "util";
import tmp from "tmp";
import path from "path";
import csstree from "css-tree";
import { Plugin } from "esbuild";
import {
escapeClassName,
interpolatePattern,
isSupportedHashDigest,
isSupportedHashType,
makeNameHash,
SUPPORTED_DIGESTS,
SUPPORTED_HASHES,
} from "./helpers";
import { PathLike } from "fs";
import { Buffer } from "buffer";
const writeFile = util.promisify<PathLike, any>(fs.writeFile);
const readFile = util.promisify(fs.readFile);
const ensureDir = util.promisify(fs.ensureDir);
interface Options {
localIdentName?: string;
extension?: string;
}
export = (options: Options = {}): Plugin => ({
name: "css-modules",
setup: function (build) {
const { extension = ".module.css", localIdentName = "[hash]" } = options;
const rootDir = process.cwd();
const filter = new RegExp(`.\/.+${extension.replace(/\./g, "\\.")}$`);
const tmpDirPath = tmp.dirSync().name;
build.onLoad({ filter }, async (args) => {
const relativeDir = path.relative(rootDir, path.dirname(args.path));
const fileContent = (await readFile(args.path)) as Buffer;
const baseName = path.basename(args.path, extension);
const folderName = path.basename(path.dirname(args.path));
const extName = extension;
const preparedLocalIdentName = interpolatePattern(
localIdentName,
(name: string) => {
switch (name) {
case "ext":
return escapeClassName(extName);
case "name":
return escapeClassName(baseName);
case "path":
return escapeClassName(args.path);
case "folder":
return escapeClassName(folderName);
}
return null;
}
);
const ast = csstree.parse(fileContent.toString());
const classMap: Record<string, string> = {};
csstree.walk(ast, {
visit: "ClassSelector",
enter(node) {
const newClassname = interpolatePattern(
preparedLocalIdentName,
(name: string, params: string[]) => {
switch (name) {
case "local":
return node.name;
case "hash": {
const [lengthRaw, hashType, digestType] = params;
if (
hashType !== undefined &&
!isSupportedHashType(hashType)
) {
throw new Error(
`Hash algorithm is not supported: ${hashType}. Supported algorithms: ${SUPPORTED_HASHES.join(
", "
)}`
);
}
if (
digestType !== undefined &&
!isSupportedHashDigest(digestType)
) {
throw new Error(
`Digest type is not supported: ${digestType}. Supported digests: ${SUPPORTED_DIGESTS.join(
", "
)}`
);
}
const length: number | undefined =
parseInt(lengthRaw) || undefined;
return makeNameHash(
args.path + ":" + node.name,
length,
hashType,
digestType
);
}
}
return null;
}
);
classMap[node.name] = newClassname;
node.name = newClassname;
},
});
const baseFileName = path.basename(args.path, extension);
const tmpFilePath = path.resolve(
tmpDirPath,
relativeDir,
`${baseFileName}.css`
);
await ensureDir(path.dirname(tmpFilePath));
// @ts-ignore
await writeFile(tmpFilePath, csstree.generate(ast));
let contents = `
import "${tmpFilePath}";
const result = ${JSON.stringify(classMap)};
export default result;
`;
return {
contents: contents,
};
});
},
});