@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/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 */