@greenfinity/rescript-typed-css-modules 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/__tests__/.gitkeep +0 -0
- package/dist/css-to-rescript.js +26902 -0
- package/package.json +58 -0
- package/rescript.json +30 -0
- package/src/CssToRescript.bs.mjs +335 -0
- package/src/CssToRescript.res +381 -0
- package/src/require-shim.mjs +45 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@greenfinity/rescript-typed-css-modules",
|
|
3
|
+
"description": "Typed CSS Modules for ReScript",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"css-to-rescript": "./dist/css-to-rescript.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.12.0",
|
|
12
|
+
"yarn": ">=4.5.3"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"__tests__/.gitkeep",
|
|
18
|
+
"rescript.json"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"changelog": "yarn auto-changelog -p && git add HISTORY.md",
|
|
22
|
+
"rescript:clean": "yarn rescript clean",
|
|
23
|
+
"rescript:build": "yarn rescript build",
|
|
24
|
+
"rescript:dev": "RES_LOGGER=ReScriptLogger.Universal RES_LOG=info yarn rescript watch",
|
|
25
|
+
"esbuild:clean": "rm -rf dist",
|
|
26
|
+
"esbuild:build": "yarn esbuild src/CssToRescript.bs.mjs --bundle --platform=node --format=esm --outfile=dist/css-to-rescript.js --banner:js=\"#!/usr/bin/env node\" --inject:./src/require-shim.mjs && yarn esbuild:chmod",
|
|
27
|
+
"esbuild:chmod": "chmod +x dist/css-to-rescript.js",
|
|
28
|
+
"esbuild:dev": "yarn nodemon -e res -e CssModule.res -x \"yarn esbuild:build\"",
|
|
29
|
+
"fixtures:clean": "rm -rf __tests__/fixtures/**/*.res",
|
|
30
|
+
"fixtures:build": "dist/css-to-rescript.js __tests__/fixtures",
|
|
31
|
+
"fixtures:dev": "yarn nodemon -e res -e CssModule.res -x \"yarn fixtures:build\"",
|
|
32
|
+
"clean": "yarn rescript:clean && yarn esbuild:clean && yarn fixtures:clean",
|
|
33
|
+
"build": "yarn rescript:build && yarn esbuild:build && yarn fixtures:build",
|
|
34
|
+
"dev": "yarn build & yarn rescript:dev && yarn esbuild:dev && yarn fixtures:dev",
|
|
35
|
+
"yalc:dev": "yarn rescript:dev & yarn esbuild:dev & yarn fixtures:dev & yarn nodemon -x \"yalc push\"",
|
|
36
|
+
"yalc:push": "yarn nodemon -e res -x \"yalc push\""
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"auto-changelog": "^2.5.0",
|
|
40
|
+
"esbuild": "^0.27.2",
|
|
41
|
+
"nodemon": "^3.0.1",
|
|
42
|
+
"rescript": "^12.1.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"rescript": "^12.1.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@rescript/core": "^1.6.1",
|
|
49
|
+
"chokidar": "^5.0.0",
|
|
50
|
+
"meow": "^14.0.0",
|
|
51
|
+
"postcss": "^8.5.6",
|
|
52
|
+
"postcss-import": "^16.1.1",
|
|
53
|
+
"postcss-modules": "^6.0.1",
|
|
54
|
+
"postcss-scss": "^4.0.9",
|
|
55
|
+
"rescript-nodejs": "^16.1.0"
|
|
56
|
+
},
|
|
57
|
+
"packageManager": "yarn@4.12.0"
|
|
58
|
+
}
|
package/rescript.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@greenfinity/rescript-typed-css-modules",
|
|
3
|
+
"sources": [
|
|
4
|
+
{
|
|
5
|
+
"dir": "src",
|
|
6
|
+
"subdirs": true
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"dir": "__tests__",
|
|
10
|
+
"subdirs": true,
|
|
11
|
+
"type": "dev"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"warnings": {
|
|
15
|
+
"number": "-3",
|
|
16
|
+
"error": "+101"
|
|
17
|
+
},
|
|
18
|
+
"suffix": ".bs.mjs",
|
|
19
|
+
"namespace": true,
|
|
20
|
+
"compiler-flags": ["-bs-no-version-header", "-open RescriptCore"],
|
|
21
|
+
"package-specs": [
|
|
22
|
+
{
|
|
23
|
+
"module": "esmodule",
|
|
24
|
+
"in-source": true
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"dependencies": ["@rescript/core", "rescript-nodejs"],
|
|
28
|
+
"dev-dependencies": [],
|
|
29
|
+
"ppx-flags": []
|
|
30
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import Meow from "meow";
|
|
4
|
+
import * as Nodefs from "node:fs";
|
|
5
|
+
import Postcss from "postcss";
|
|
6
|
+
import * as Process from "process";
|
|
7
|
+
import * as Chokidar from "chokidar";
|
|
8
|
+
import * as Nodepath from "node:path";
|
|
9
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.bs.mjs";
|
|
10
|
+
import * as Core__Option from "@rescript/core/src/Core__Option.bs.mjs";
|
|
11
|
+
import PostcssScss from "postcss-scss";
|
|
12
|
+
import PostcssImport from "postcss-import";
|
|
13
|
+
import PostcssModules from "postcss-modules";
|
|
14
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
15
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
16
|
+
|
|
17
|
+
let Meow$1 = {};
|
|
18
|
+
|
|
19
|
+
let Chokidar$1 = {};
|
|
20
|
+
|
|
21
|
+
let PostCss = {};
|
|
22
|
+
|
|
23
|
+
let PostCssScss = {};
|
|
24
|
+
|
|
25
|
+
let PostCssImport = {};
|
|
26
|
+
|
|
27
|
+
let PostCssModules = {};
|
|
28
|
+
|
|
29
|
+
async function extractClassNames(cssContent, from) {
|
|
30
|
+
return (await new Promise((resolve, param) => {
|
|
31
|
+
let classNames = {
|
|
32
|
+
contents: []
|
|
33
|
+
};
|
|
34
|
+
Postcss([
|
|
35
|
+
PostcssImport(),
|
|
36
|
+
PostcssModules({
|
|
37
|
+
getJSON: (param, json) => {
|
|
38
|
+
classNames.contents = Object.keys(json);
|
|
39
|
+
},
|
|
40
|
+
exportGlobals: true
|
|
41
|
+
})
|
|
42
|
+
]).process(cssContent, {
|
|
43
|
+
from: from,
|
|
44
|
+
syntax: Primitive_option.some(PostcssScss)
|
|
45
|
+
}).then(param => resolve(classNames.contents));
|
|
46
|
+
})).toSorted(Primitive_string.compare);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generateReScriptBindings(baseName, importType, classNames) {
|
|
50
|
+
let recordFields = classNames.map(className => ` "` + className + `": string`);
|
|
51
|
+
let prelude = `// Generated from ` + baseName + `
|
|
52
|
+
// Do not edit manually
|
|
53
|
+
|
|
54
|
+
type t = {
|
|
55
|
+
` + recordFields.join(",\n") + `
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
if (importType === "Module") {
|
|
59
|
+
return prelude + (`@module("./` + baseName + `") external css: t = "default"
|
|
60
|
+
|
|
61
|
+
// Access class names from the fields of the css object.
|
|
62
|
+
// For scoped classses, the hashed class name is returned.
|
|
63
|
+
// For :global() classes, the class name is returned as-is: no scoping.
|
|
64
|
+
// Classes from @import are also available.
|
|
65
|
+
|
|
66
|
+
@module("./` + baseName + `") external _imported: t = "default"
|
|
67
|
+
@new external proxy: ('a, 'b) => 'c = "Proxy"
|
|
68
|
+
%%private(
|
|
69
|
+
external toDict: t => dict<string> = "%identity"
|
|
70
|
+
let withProxy = (obj: t): t =>
|
|
71
|
+
proxy(
|
|
72
|
+
obj,
|
|
73
|
+
{
|
|
74
|
+
// "get": (_b, _c): string => %raw("_b[_c] || _c"),
|
|
75
|
+
"get": (base, className) =>
|
|
76
|
+
switch base->toDict->Dict.get(className) {
|
|
77
|
+
| Some(className) => className
|
|
78
|
+
| None => className
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
let css = withProxy(css)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
`);
|
|
87
|
+
} else {
|
|
88
|
+
return prelude + `
|
|
89
|
+
// Access class names from the fields of the css object.
|
|
90
|
+
// Import is not done, the css has to be manually imported
|
|
91
|
+
// from the top of the component hierarchy.
|
|
92
|
+
// For all classes, the class name is returned as-is: no scoping.
|
|
93
|
+
// Classes from @import are also available.
|
|
94
|
+
|
|
95
|
+
@new external proxy: ('a, 'b) => 'c = "Proxy"
|
|
96
|
+
type empty = {}
|
|
97
|
+
%%private(
|
|
98
|
+
let withProxy = (obj: empty): t =>
|
|
99
|
+
proxy(
|
|
100
|
+
obj,
|
|
101
|
+
{
|
|
102
|
+
"get": (_: empty, className: string): string => className,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
let css = withProxy({})
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getBaseNameAndImportType(cssFilePath) {
|
|
112
|
+
let baseName = Nodepath.basename(cssFilePath);
|
|
113
|
+
return [
|
|
114
|
+
baseName,
|
|
115
|
+
/\.module\.(css|scss)$/.test(baseName) ? "Module" : "Global"
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getOutputFileName(baseName, importType) {
|
|
120
|
+
if (importType === "Module") {
|
|
121
|
+
return baseName.replace(/\.module\.(css|scss)$/, "_CssModule") + ".res";
|
|
122
|
+
} else {
|
|
123
|
+
return baseName.replace(/\.global\.(css|scss)$/, "_CssGlobal") + ".res";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function processFile(cssFilePath, outputDir) {
|
|
128
|
+
let content = Nodefs.readFileSync(cssFilePath).toString();
|
|
129
|
+
console.log(`Processing file: ` + cssFilePath);
|
|
130
|
+
let classNames = await extractClassNames(content, cssFilePath);
|
|
131
|
+
if (classNames.length === 0) {
|
|
132
|
+
console.log(`⚠️ No classes found in ` + cssFilePath);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let match = getBaseNameAndImportType(cssFilePath);
|
|
136
|
+
let importType = match[1];
|
|
137
|
+
let baseName = match[0];
|
|
138
|
+
let outputFileName = getOutputFileName(baseName, importType);
|
|
139
|
+
let bindings = generateReScriptBindings(baseName, importType, classNames);
|
|
140
|
+
let outputPath = Nodepath.join(Core__Option.getOr(outputDir, Nodepath.dirname(cssFilePath)), outputFileName);
|
|
141
|
+
Nodefs.writeFileSync(outputPath, Buffer.from(bindings));
|
|
142
|
+
console.log(`✅ Generated ` + outputPath + ` (` + classNames.length.toString() + ` classes)`);
|
|
143
|
+
return [
|
|
144
|
+
outputPath,
|
|
145
|
+
classNames
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findCssFiles(dir) {
|
|
150
|
+
let entries = Nodefs.readdirSync(dir);
|
|
151
|
+
return entries.flatMap(entry => {
|
|
152
|
+
let fullPath = Nodepath.join(dir, entry);
|
|
153
|
+
let stat = Nodefs.lstatSync(fullPath);
|
|
154
|
+
if (stat.isDirectory()) {
|
|
155
|
+
return findCssFiles(fullPath);
|
|
156
|
+
} else if (/\.(module|global)\.(css|scss)$/.test(entry)) {
|
|
157
|
+
return [fullPath];
|
|
158
|
+
} else {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function watchDirectories(dirs, outputDir, skipInitial) {
|
|
165
|
+
console.log(`👀 Watching ` + dirs.length.toString() + ` directories for CSS module/global changes...`);
|
|
166
|
+
dirs.forEach(dir => {
|
|
167
|
+
console.log(` ` + dir);
|
|
168
|
+
});
|
|
169
|
+
if (skipInitial) {
|
|
170
|
+
console.log(`Skipping initial compilation.`);
|
|
171
|
+
}
|
|
172
|
+
console.log(`Press Ctrl+C to stop.\n`);
|
|
173
|
+
let isIgnored = path => {
|
|
174
|
+
let isDotfile = /(^|[\/\\])\./.test(path);
|
|
175
|
+
let isCssFile = /\.(module|global)\.(css|scss)$/.test(path);
|
|
176
|
+
let isDir = Nodefs.existsSync(path) && Nodefs.lstatSync(path).isDirectory();
|
|
177
|
+
if (isDotfile) {
|
|
178
|
+
return true;
|
|
179
|
+
} else if (isCssFile) {
|
|
180
|
+
return false;
|
|
181
|
+
} else {
|
|
182
|
+
return !isDir;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
let ready = {
|
|
186
|
+
contents: false
|
|
187
|
+
};
|
|
188
|
+
Chokidar.watch(dirs, {
|
|
189
|
+
ignored: isIgnored,
|
|
190
|
+
persistent: true
|
|
191
|
+
}).on("ready", () => {
|
|
192
|
+
ready.contents = true;
|
|
193
|
+
console.log(`Ready for changes.`);
|
|
194
|
+
}).on("change", path => {
|
|
195
|
+
console.log(`\nChanged: ` + path);
|
|
196
|
+
processFile(path, outputDir);
|
|
197
|
+
}).on("add", path => {
|
|
198
|
+
if (skipInitial && !ready.contents) {
|
|
199
|
+
return;
|
|
200
|
+
} else {
|
|
201
|
+
console.log(`\nAdded: ` + path);
|
|
202
|
+
processFile(path, outputDir);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}).on("unlink", path => {
|
|
206
|
+
console.log(`\n🗑️ Deleted: ` + path);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let helpText = `
|
|
211
|
+
Usage
|
|
212
|
+
$ css-to-rescript <file.module.css|scss|global.css|scss...>
|
|
213
|
+
$ css-to-rescript <directory...>
|
|
214
|
+
|
|
215
|
+
Generates ReScript bindings from CSS module and global files:
|
|
216
|
+
*.module.css|scss -> *_CssModule.res
|
|
217
|
+
*.global.css|scss -> *_CssGlobal.res
|
|
218
|
+
|
|
219
|
+
Options
|
|
220
|
+
--watch, -w Watch for changes and regenerate bindings (directories only)
|
|
221
|
+
--skip-initial, -s Skip initial compilation in watch mode
|
|
222
|
+
--output-dir, -o Directory to write generated .res files
|
|
223
|
+
(multiple files or single directory only)
|
|
224
|
+
|
|
225
|
+
Examples
|
|
226
|
+
$ css-to-rescript src/Card.module.scss
|
|
227
|
+
$ css-to-rescript src/Theme.global.css
|
|
228
|
+
$ css-to-rescript src/Button.module.css src/Card.module.scss -o src/bindings
|
|
229
|
+
$ css-to-rescript src/components
|
|
230
|
+
$ css-to-rescript src/components src/pages --watch
|
|
231
|
+
`;
|
|
232
|
+
|
|
233
|
+
async function main() {
|
|
234
|
+
let cli = Meow(helpText, {
|
|
235
|
+
importMeta: import.meta,
|
|
236
|
+
flags: {
|
|
237
|
+
watch: {
|
|
238
|
+
type: "boolean",
|
|
239
|
+
shortFlag: "w",
|
|
240
|
+
default: false
|
|
241
|
+
},
|
|
242
|
+
outputDir: {
|
|
243
|
+
type: "string",
|
|
244
|
+
shortFlag: "o"
|
|
245
|
+
},
|
|
246
|
+
skipInitial: {
|
|
247
|
+
type: "boolean",
|
|
248
|
+
shortFlag: "s",
|
|
249
|
+
default: false
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
allowUnknownFlags: false
|
|
253
|
+
});
|
|
254
|
+
let inputPaths = cli.input;
|
|
255
|
+
if (inputPaths.length === 0) {
|
|
256
|
+
cli.showHelp();
|
|
257
|
+
Process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
let outputDir = cli.flags.outputDir;
|
|
260
|
+
let watchMode = cli.flags.watch;
|
|
261
|
+
let skipInitial = cli.flags.skipInitial;
|
|
262
|
+
let match = Core__Array.reduce(inputPaths, [
|
|
263
|
+
[],
|
|
264
|
+
[]
|
|
265
|
+
], (param, path) => {
|
|
266
|
+
let dirs = param[1];
|
|
267
|
+
let files = param[0];
|
|
268
|
+
let stat = Nodefs.lstatSync(path);
|
|
269
|
+
if (stat.isFile()) {
|
|
270
|
+
return [
|
|
271
|
+
files.concat([path]),
|
|
272
|
+
dirs
|
|
273
|
+
];
|
|
274
|
+
} else if (stat.isDirectory()) {
|
|
275
|
+
return [
|
|
276
|
+
files,
|
|
277
|
+
dirs.concat([path])
|
|
278
|
+
];
|
|
279
|
+
} else {
|
|
280
|
+
return [
|
|
281
|
+
files,
|
|
282
|
+
dirs
|
|
283
|
+
];
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
let dirs = match[1];
|
|
287
|
+
let files = match[0];
|
|
288
|
+
if (watchMode && files.length > 0) {
|
|
289
|
+
console.error(`Error: Watch mode only supports directories, not files.`);
|
|
290
|
+
Process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
if (Core__Option.isSome(outputDir) && (dirs.length > 1 || files.length > 0 && dirs.length > 0)) {
|
|
293
|
+
console.error(`Error: --output-dir only supports multiple files or a single directory, not mixed inputs or multiple directories.`);
|
|
294
|
+
Process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
if (files.length > 0) {
|
|
297
|
+
await Core__Array.reduce(files, Promise.resolve(), async (acc, file) => {
|
|
298
|
+
await acc;
|
|
299
|
+
await processFile(file, outputDir);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (dirs.length <= 0) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (watchMode) {
|
|
306
|
+
return await watchDirectories(dirs, outputDir, skipInitial);
|
|
307
|
+
}
|
|
308
|
+
let moduleFiles = dirs.flatMap(findCssFiles);
|
|
309
|
+
console.log(`Found ` + moduleFiles.length.toString() + ` CSS module files\n`);
|
|
310
|
+
return await Core__Array.reduce(moduleFiles, Promise.resolve(), async (acc, file) => {
|
|
311
|
+
await acc;
|
|
312
|
+
await processFile(file, outputDir);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main();
|
|
317
|
+
|
|
318
|
+
export {
|
|
319
|
+
Meow$1 as Meow,
|
|
320
|
+
Chokidar$1 as Chokidar,
|
|
321
|
+
PostCss,
|
|
322
|
+
PostCssScss,
|
|
323
|
+
PostCssImport,
|
|
324
|
+
PostCssModules,
|
|
325
|
+
extractClassNames,
|
|
326
|
+
generateReScriptBindings,
|
|
327
|
+
getBaseNameAndImportType,
|
|
328
|
+
getOutputFileName,
|
|
329
|
+
processFile,
|
|
330
|
+
findCssFiles,
|
|
331
|
+
watchDirectories,
|
|
332
|
+
helpText,
|
|
333
|
+
main,
|
|
334
|
+
}
|
|
335
|
+
/* Not a pure module */
|