@greenfinity/rescript-typed-css-modules 0.1.0 ā 0.2.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/README.md +13 -10
- package/dist/css-to-rescript.js +109 -32
- package/package.json +1 -1
- package/src/CssToRescript.bs.mjs +109 -28
- package/src/CssToRescript.res +138 -46
package/README.md
CHANGED
|
@@ -24,15 +24,13 @@ Only tested with Next.js and might not work with other frameworks out of the box
|
|
|
24
24
|
|
|
25
25
|
### Why global CSS support?
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Global CSS files (`.global.css` / `.global.scss`) also get type-safe bindings, but they are not imported, and their class names are **not hashed**. This is useful when working with third-party libraries (such as React Aria Components) that emit HTML markup with predefined class names that you need to style.
|
|
27
|
+
TL;DR: Global CSS files behave just like CSS Modules, except their class names are **not hashed** but kept as-is.
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
Support for global CSS is provided in addition to CSS Modules.
|
|
32
30
|
|
|
33
|
-
The
|
|
31
|
+
The use case is to access classes in a type safe way from third party css libraries, that emit HTML markup with predefined class names without the use of css modules (e.g. React Aria Components).
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
Even though global CSS classes aren't scoped, generating typed bindings provides the benefit of compile-time checking that class names exist. If the framework you are using supports bundling by routes (such as NextJS), the global css files will also be bundled together with JavaScript. Regarding duplicates, they should be handled the same way as CSS Modules, or ordinary JavaScript imports. (Checked with NextJS with both the Webpack and the Turbopack builder.)
|
|
36
34
|
|
|
37
35
|
## Installation
|
|
38
36
|
|
|
@@ -50,9 +48,15 @@ Usage
|
|
|
50
48
|
Options
|
|
51
49
|
--watch, -w Watch for changes and regenerate bindings (directories only)
|
|
52
50
|
--skip-initial, -s Skip initial compilation in watch mode
|
|
51
|
+
--force, -f Force compilation even if files are unchanged
|
|
52
|
+
--silent Only show "Generated" messages, suppress other output
|
|
53
|
+
--quiet, -q Suppress all output
|
|
53
54
|
--output-dir, -o Directory to write generated .res files
|
|
54
55
|
(multiple files or single directory only)
|
|
55
56
|
|
|
57
|
+
By default, files are skipped if the source CSS file has not been modified
|
|
58
|
+
since the last compilation. Use --force to always recompile.
|
|
59
|
+
|
|
56
60
|
Examples
|
|
57
61
|
$ css-to-rescript src/Card.module.scss
|
|
58
62
|
$ css-to-rescript src/Theme.global.css
|
|
@@ -60,6 +64,7 @@ Examples
|
|
|
60
64
|
$ css-to-rescript src/components
|
|
61
65
|
$ css-to-rescript src/components src/pages --watch
|
|
62
66
|
$ css-to-rescript src/components --watch --skip-initial
|
|
67
|
+
$ css-to-rescript src/components --force
|
|
63
68
|
```
|
|
64
69
|
|
|
65
70
|
## Example
|
|
@@ -168,6 +173,7 @@ type t = {
|
|
|
168
173
|
"light-mode": string,
|
|
169
174
|
"primary-color": string
|
|
170
175
|
}
|
|
176
|
+
@module("./Button.module.css") external css: t = "default"
|
|
171
177
|
|
|
172
178
|
// Class names are returned as-is (no hashing)
|
|
173
179
|
let css = ...
|
|
@@ -176,10 +182,7 @@ let css = ...
|
|
|
176
182
|
Usage:
|
|
177
183
|
|
|
178
184
|
```rescript
|
|
179
|
-
//
|
|
180
|
-
%%raw(`import "./Theme.global.css"`)
|
|
181
|
-
|
|
182
|
-
// Then use type-safe class names anywhere
|
|
185
|
+
// Use type-safe class names anywhere
|
|
183
186
|
<div className={Theme_CssGlobal.css["dark-mode"]}>
|
|
184
187
|
```
|
|
185
188
|
|
package/dist/css-to-rescript.js
CHANGED
|
@@ -26620,14 +26620,14 @@ type t = {
|
|
|
26620
26620
|
}
|
|
26621
26621
|
`;
|
|
26622
26622
|
if (importType === "Module") {
|
|
26623
|
-
return prelude + (
|
|
26623
|
+
return prelude + (`
|
|
26624
|
+
@module("./` + baseName + `") external css: t = "default"
|
|
26624
26625
|
|
|
26625
26626
|
// Access class names from the fields of the css object.
|
|
26626
26627
|
// For scoped classses, the hashed class name is returned.
|
|
26627
26628
|
// For :global() classes, the class name is returned as-is: no scoping.
|
|
26628
26629
|
// Classes from @import are also available.
|
|
26629
26630
|
|
|
26630
|
-
@module("./` + baseName + `") external _imported: t = "default"
|
|
26631
26631
|
@new external proxy: ('a, 'b) => 'c = "Proxy"
|
|
26632
26632
|
%%private(
|
|
26633
26633
|
external toDict: t => dict<string> = "%identity"
|
|
@@ -26649,10 +26649,10 @@ let css = withProxy(css)
|
|
|
26649
26649
|
|
|
26650
26650
|
`);
|
|
26651
26651
|
} else {
|
|
26652
|
-
return prelude + `
|
|
26652
|
+
return prelude + (`
|
|
26653
|
+
@module("./` + baseName + `") external _imported: t = "default"
|
|
26654
|
+
|
|
26653
26655
|
// Access class names from the fields of the css object.
|
|
26654
|
-
// Import is not done, the css has to be manually imported
|
|
26655
|
-
// from the top of the component hierarchy.
|
|
26656
26656
|
// For all classes, the class name is returned as-is: no scoping.
|
|
26657
26657
|
// Classes from @import are also available.
|
|
26658
26658
|
|
|
@@ -26668,7 +26668,7 @@ type empty = {}
|
|
|
26668
26668
|
)
|
|
26669
26669
|
)
|
|
26670
26670
|
let css = withProxy({})
|
|
26671
|
-
|
|
26671
|
+
`);
|
|
26672
26672
|
}
|
|
26673
26673
|
}
|
|
26674
26674
|
function getBaseNameAndImportType(cssFilePath) {
|
|
@@ -26685,12 +26685,36 @@ function getOutputFileName(baseName, importType) {
|
|
|
26685
26685
|
return baseName.replace(/\.global\.(css|scss)$/, "_CssGlobal") + ".res";
|
|
26686
26686
|
}
|
|
26687
26687
|
}
|
|
26688
|
-
|
|
26688
|
+
function shouldCompile(cssFilePath, outputDir) {
|
|
26689
|
+
let match = getBaseNameAndImportType(cssFilePath);
|
|
26690
|
+
let outputFileName = getOutputFileName(match[0], match[1]);
|
|
26691
|
+
let outputPath = Nodepath.join(getOr(outputDir, Nodepath.dirname(cssFilePath)), outputFileName);
|
|
26692
|
+
if (!Nodefs.existsSync(outputPath)) {
|
|
26693
|
+
return true;
|
|
26694
|
+
}
|
|
26695
|
+
let sourceStat = Nodefs.lstatSync(cssFilePath);
|
|
26696
|
+
let outputStat = Nodefs.lstatSync(outputPath);
|
|
26697
|
+
return sourceStat.mtimeMs > outputStat.mtimeMs;
|
|
26698
|
+
}
|
|
26699
|
+
async function processFile(cssFilePath, outputDir, forceOpt, silentOpt, quietOpt) {
|
|
26700
|
+
let force = forceOpt !== void 0 ? forceOpt : false;
|
|
26701
|
+
let silent = silentOpt !== void 0 ? silentOpt : false;
|
|
26702
|
+
let quiet = quietOpt !== void 0 ? quietOpt : false;
|
|
26703
|
+
if (!force && !shouldCompile(cssFilePath, outputDir)) {
|
|
26704
|
+
if (!silent && !quiet) {
|
|
26705
|
+
console.log(`\u23ED\uFE0F Skipped: ` + cssFilePath + ` (unchanged)`);
|
|
26706
|
+
}
|
|
26707
|
+
return;
|
|
26708
|
+
}
|
|
26689
26709
|
let content = Nodefs.readFileSync(cssFilePath).toString();
|
|
26690
|
-
|
|
26710
|
+
if (!silent && !quiet) {
|
|
26711
|
+
console.log(`Processing file: ` + cssFilePath);
|
|
26712
|
+
}
|
|
26691
26713
|
let classNames = await extractClassNames(content, cssFilePath);
|
|
26692
26714
|
if (classNames.length === 0) {
|
|
26693
|
-
|
|
26715
|
+
if (!silent && !quiet) {
|
|
26716
|
+
console.log(`\u26A0\uFE0F No classes found in ` + cssFilePath);
|
|
26717
|
+
}
|
|
26694
26718
|
return;
|
|
26695
26719
|
}
|
|
26696
26720
|
let match = getBaseNameAndImportType(cssFilePath);
|
|
@@ -26700,7 +26724,9 @@ async function processFile(cssFilePath, outputDir) {
|
|
|
26700
26724
|
let bindings = generateReScriptBindings(baseName, importType, classNames);
|
|
26701
26725
|
let outputPath = Nodepath.join(getOr(outputDir, Nodepath.dirname(cssFilePath)), outputFileName);
|
|
26702
26726
|
Nodefs.writeFileSync(outputPath, Buffer.from(bindings));
|
|
26703
|
-
|
|
26727
|
+
if (!quiet) {
|
|
26728
|
+
console.log(`\u2705 Generated ` + outputPath + ` (` + classNames.length.toString() + ` classes)`);
|
|
26729
|
+
}
|
|
26704
26730
|
return [
|
|
26705
26731
|
outputPath,
|
|
26706
26732
|
classNames
|
|
@@ -26720,16 +26746,18 @@ function findCssFiles(dir) {
|
|
|
26720
26746
|
}
|
|
26721
26747
|
});
|
|
26722
26748
|
}
|
|
26723
|
-
async function watchDirectories(dirs, outputDir, skipInitial) {
|
|
26724
|
-
|
|
26725
|
-
|
|
26726
|
-
|
|
26727
|
-
|
|
26728
|
-
|
|
26729
|
-
|
|
26730
|
-
|
|
26731
|
-
|
|
26749
|
+
async function watchDirectories(dirs, outputDir, skipInitial, force, silent, quiet) {
|
|
26750
|
+
if (!silent && !quiet) {
|
|
26751
|
+
console.log(`\u{1F440} Watching ` + dirs.length.toString() + ` directories for CSS module/global changes...`);
|
|
26752
|
+
dirs.forEach((dir) => {
|
|
26753
|
+
console.log(` ` + dir);
|
|
26754
|
+
});
|
|
26755
|
+
if (skipInitial) {
|
|
26756
|
+
console.log(`Skipping initial compilation.`);
|
|
26757
|
+
}
|
|
26758
|
+
console.log(`Press Ctrl+C to stop.
|
|
26732
26759
|
`);
|
|
26760
|
+
}
|
|
26733
26761
|
let isIgnored = (path3) => {
|
|
26734
26762
|
let isDotfile = /(^|[\/\\])\./.test(path3);
|
|
26735
26763
|
let isCssFile = /\.(module|global)\.(css|scss)$/.test(path3);
|
|
@@ -26750,23 +26778,45 @@ async function watchDirectories(dirs, outputDir, skipInitial) {
|
|
|
26750
26778
|
persistent: true
|
|
26751
26779
|
}).on("ready", () => {
|
|
26752
26780
|
ready.contents = true;
|
|
26753
|
-
|
|
26781
|
+
if (!silent && !quiet) {
|
|
26782
|
+
console.log(`Ready for changes.`);
|
|
26783
|
+
return;
|
|
26784
|
+
}
|
|
26754
26785
|
}).on("change", (path3) => {
|
|
26755
|
-
|
|
26756
|
-
Changed: ` + path3);
|
|
26757
|
-
|
|
26786
|
+
if (!silent && !quiet) {
|
|
26787
|
+
console.log(`Changed: ` + path3);
|
|
26788
|
+
}
|
|
26789
|
+
processFile(path3, outputDir, true, silent, quiet);
|
|
26758
26790
|
}).on("add", (path3) => {
|
|
26759
26791
|
if (skipInitial && !ready.contents) {
|
|
26760
26792
|
return;
|
|
26761
26793
|
} else {
|
|
26762
|
-
|
|
26763
|
-
|
|
26764
|
-
|
|
26794
|
+
if (ready.contents) {
|
|
26795
|
+
if (!silent && !quiet) {
|
|
26796
|
+
console.log(`Added: ` + path3);
|
|
26797
|
+
}
|
|
26798
|
+
processFile(path3, outputDir, true, silent, quiet);
|
|
26799
|
+
} else {
|
|
26800
|
+
processFile(path3, outputDir, force, silent, quiet);
|
|
26801
|
+
}
|
|
26765
26802
|
return;
|
|
26766
26803
|
}
|
|
26767
26804
|
}).on("unlink", (path3) => {
|
|
26768
|
-
|
|
26769
|
-
|
|
26805
|
+
if (!silent && !quiet) {
|
|
26806
|
+
console.log(`\u{1F5D1}\uFE0F Deleted: ` + path3);
|
|
26807
|
+
}
|
|
26808
|
+
let match = getBaseNameAndImportType(path3);
|
|
26809
|
+
let outputFileName = getOutputFileName(match[0], match[1]);
|
|
26810
|
+
let outputPath = Nodepath.join(getOr(outputDir, Nodepath.dirname(path3)), outputFileName);
|
|
26811
|
+
if (Nodefs.existsSync(outputPath)) {
|
|
26812
|
+
Nodefs.unlinkSync(outputPath);
|
|
26813
|
+
if (!silent && !quiet) {
|
|
26814
|
+
console.log(`\u{1F5D1}\uFE0F Deleted compiled: ` + outputPath);
|
|
26815
|
+
return;
|
|
26816
|
+
} else {
|
|
26817
|
+
return;
|
|
26818
|
+
}
|
|
26819
|
+
}
|
|
26770
26820
|
});
|
|
26771
26821
|
}
|
|
26772
26822
|
var helpText = `
|
|
@@ -26781,15 +26831,22 @@ var helpText = `
|
|
|
26781
26831
|
Options
|
|
26782
26832
|
--watch, -w Watch for changes and regenerate bindings (directories only)
|
|
26783
26833
|
--skip-initial, -s Skip initial compilation in watch mode
|
|
26834
|
+
--force, -f Force compilation even if files are unchanged
|
|
26835
|
+
--silent Only show "Generated" messages, suppress other output
|
|
26836
|
+
--quiet, -q Suppress all output
|
|
26784
26837
|
--output-dir, -o Directory to write generated .res files
|
|
26785
26838
|
(multiple files or single directory only)
|
|
26786
26839
|
|
|
26840
|
+
By default, files are skipped if the source CSS file has not been modified
|
|
26841
|
+
since the last compilation. Use --force to always recompile.
|
|
26842
|
+
|
|
26787
26843
|
Examples
|
|
26788
26844
|
$ css-to-rescript src/Card.module.scss
|
|
26789
26845
|
$ css-to-rescript src/Theme.global.css
|
|
26790
26846
|
$ css-to-rescript src/Button.module.css src/Card.module.scss -o src/bindings
|
|
26791
26847
|
$ css-to-rescript src/components
|
|
26792
26848
|
$ css-to-rescript src/components src/pages --watch
|
|
26849
|
+
$ css-to-rescript src/components --force
|
|
26793
26850
|
`;
|
|
26794
26851
|
async function main() {
|
|
26795
26852
|
let cli = meow(helpText, {
|
|
@@ -26808,6 +26865,20 @@ async function main() {
|
|
|
26808
26865
|
type: "boolean",
|
|
26809
26866
|
shortFlag: "s",
|
|
26810
26867
|
default: false
|
|
26868
|
+
},
|
|
26869
|
+
force: {
|
|
26870
|
+
type: "boolean",
|
|
26871
|
+
shortFlag: "f",
|
|
26872
|
+
default: false
|
|
26873
|
+
},
|
|
26874
|
+
silent: {
|
|
26875
|
+
type: "boolean",
|
|
26876
|
+
default: false
|
|
26877
|
+
},
|
|
26878
|
+
quiet: {
|
|
26879
|
+
type: "boolean",
|
|
26880
|
+
shortFlag: "q",
|
|
26881
|
+
default: false
|
|
26811
26882
|
}
|
|
26812
26883
|
},
|
|
26813
26884
|
allowUnknownFlags: false
|
|
@@ -26820,6 +26891,9 @@ async function main() {
|
|
|
26820
26891
|
let outputDir = cli.flags.outputDir;
|
|
26821
26892
|
let watchMode = cli.flags.watch;
|
|
26822
26893
|
let skipInitial = cli.flags.skipInitial;
|
|
26894
|
+
let force = cli.flags.force;
|
|
26895
|
+
let silent = cli.flags.silent;
|
|
26896
|
+
let quiet = cli.flags.quiet;
|
|
26823
26897
|
let match = reduce(inputPaths, [
|
|
26824
26898
|
[],
|
|
26825
26899
|
[]
|
|
@@ -26857,21 +26931,23 @@ async function main() {
|
|
|
26857
26931
|
if (files.length > 0) {
|
|
26858
26932
|
await reduce(files, Promise.resolve(), async (acc, file) => {
|
|
26859
26933
|
await acc;
|
|
26860
|
-
await processFile(file, outputDir);
|
|
26934
|
+
await processFile(file, outputDir, force, silent, quiet);
|
|
26861
26935
|
});
|
|
26862
26936
|
}
|
|
26863
26937
|
if (dirs.length <= 0) {
|
|
26864
26938
|
return;
|
|
26865
26939
|
}
|
|
26866
26940
|
if (watchMode) {
|
|
26867
|
-
return await watchDirectories(dirs, outputDir, skipInitial);
|
|
26941
|
+
return await watchDirectories(dirs, outputDir, skipInitial, force, silent, quiet);
|
|
26868
26942
|
}
|
|
26869
26943
|
let moduleFiles = dirs.flatMap(findCssFiles);
|
|
26870
|
-
|
|
26944
|
+
if (!silent && !quiet) {
|
|
26945
|
+
console.log(`Found ` + moduleFiles.length.toString() + ` CSS module files
|
|
26871
26946
|
`);
|
|
26947
|
+
}
|
|
26872
26948
|
return await reduce(moduleFiles, Promise.resolve(), async (acc, file) => {
|
|
26873
26949
|
await acc;
|
|
26874
|
-
await processFile(file, outputDir);
|
|
26950
|
+
await processFile(file, outputDir, force, silent, quiet);
|
|
26875
26951
|
});
|
|
26876
26952
|
}
|
|
26877
26953
|
main();
|
|
@@ -26890,6 +26966,7 @@ export {
|
|
|
26890
26966
|
helpText,
|
|
26891
26967
|
main,
|
|
26892
26968
|
processFile,
|
|
26969
|
+
shouldCompile,
|
|
26893
26970
|
watchDirectories
|
|
26894
26971
|
};
|
|
26895
26972
|
/*! Bundled license information:
|
package/package.json
CHANGED
package/src/CssToRescript.bs.mjs
CHANGED
|
@@ -56,14 +56,14 @@ type t = {
|
|
|
56
56
|
}
|
|
57
57
|
`;
|
|
58
58
|
if (importType === "Module") {
|
|
59
|
-
return prelude + (
|
|
59
|
+
return prelude + (`
|
|
60
|
+
@module("./` + baseName + `") external css: t = "default"
|
|
60
61
|
|
|
61
62
|
// Access class names from the fields of the css object.
|
|
62
63
|
// For scoped classses, the hashed class name is returned.
|
|
63
64
|
// For :global() classes, the class name is returned as-is: no scoping.
|
|
64
65
|
// Classes from @import are also available.
|
|
65
66
|
|
|
66
|
-
@module("./` + baseName + `") external _imported: t = "default"
|
|
67
67
|
@new external proxy: ('a, 'b) => 'c = "Proxy"
|
|
68
68
|
%%private(
|
|
69
69
|
external toDict: t => dict<string> = "%identity"
|
|
@@ -85,10 +85,10 @@ let css = withProxy(css)
|
|
|
85
85
|
|
|
86
86
|
`);
|
|
87
87
|
} else {
|
|
88
|
-
return prelude + `
|
|
88
|
+
return prelude + (`
|
|
89
|
+
@module("./` + baseName + `") external _imported: t = "default"
|
|
90
|
+
|
|
89
91
|
// 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
92
|
// For all classes, the class name is returned as-is: no scoping.
|
|
93
93
|
// Classes from @import are also available.
|
|
94
94
|
|
|
@@ -104,7 +104,7 @@ type empty = {}
|
|
|
104
104
|
)
|
|
105
105
|
)
|
|
106
106
|
let css = withProxy({})
|
|
107
|
-
|
|
107
|
+
`);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -124,12 +124,37 @@ function getOutputFileName(baseName, importType) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
function shouldCompile(cssFilePath, outputDir) {
|
|
128
|
+
let match = getBaseNameAndImportType(cssFilePath);
|
|
129
|
+
let outputFileName = getOutputFileName(match[0], match[1]);
|
|
130
|
+
let outputPath = Nodepath.join(Core__Option.getOr(outputDir, Nodepath.dirname(cssFilePath)), outputFileName);
|
|
131
|
+
if (!Nodefs.existsSync(outputPath)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
let sourceStat = Nodefs.lstatSync(cssFilePath);
|
|
135
|
+
let outputStat = Nodefs.lstatSync(outputPath);
|
|
136
|
+
return sourceStat.mtimeMs > outputStat.mtimeMs;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function processFile(cssFilePath, outputDir, forceOpt, silentOpt, quietOpt) {
|
|
140
|
+
let force = forceOpt !== undefined ? forceOpt : false;
|
|
141
|
+
let silent = silentOpt !== undefined ? silentOpt : false;
|
|
142
|
+
let quiet = quietOpt !== undefined ? quietOpt : false;
|
|
143
|
+
if (!force && !shouldCompile(cssFilePath, outputDir)) {
|
|
144
|
+
if (!silent && !quiet) {
|
|
145
|
+
console.log(`āļø Skipped: ` + cssFilePath + ` (unchanged)`);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
128
149
|
let content = Nodefs.readFileSync(cssFilePath).toString();
|
|
129
|
-
|
|
150
|
+
if (!silent && !quiet) {
|
|
151
|
+
console.log(`Processing file: ` + cssFilePath);
|
|
152
|
+
}
|
|
130
153
|
let classNames = await extractClassNames(content, cssFilePath);
|
|
131
154
|
if (classNames.length === 0) {
|
|
132
|
-
|
|
155
|
+
if (!silent && !quiet) {
|
|
156
|
+
console.log(`ā ļø No classes found in ` + cssFilePath);
|
|
157
|
+
}
|
|
133
158
|
return;
|
|
134
159
|
}
|
|
135
160
|
let match = getBaseNameAndImportType(cssFilePath);
|
|
@@ -139,7 +164,9 @@ async function processFile(cssFilePath, outputDir) {
|
|
|
139
164
|
let bindings = generateReScriptBindings(baseName, importType, classNames);
|
|
140
165
|
let outputPath = Nodepath.join(Core__Option.getOr(outputDir, Nodepath.dirname(cssFilePath)), outputFileName);
|
|
141
166
|
Nodefs.writeFileSync(outputPath, Buffer.from(bindings));
|
|
142
|
-
|
|
167
|
+
if (!quiet) {
|
|
168
|
+
console.log(`ā
Generated ` + outputPath + ` (` + classNames.length.toString() + ` classes)`);
|
|
169
|
+
}
|
|
143
170
|
return [
|
|
144
171
|
outputPath,
|
|
145
172
|
classNames
|
|
@@ -161,15 +188,17 @@ function findCssFiles(dir) {
|
|
|
161
188
|
});
|
|
162
189
|
}
|
|
163
190
|
|
|
164
|
-
async function watchDirectories(dirs, outputDir, skipInitial) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
191
|
+
async function watchDirectories(dirs, outputDir, skipInitial, force, silent, quiet) {
|
|
192
|
+
if (!silent && !quiet) {
|
|
193
|
+
console.log(`š Watching ` + dirs.length.toString() + ` directories for CSS module/global changes...`);
|
|
194
|
+
dirs.forEach(dir => {
|
|
195
|
+
console.log(` ` + dir);
|
|
196
|
+
});
|
|
197
|
+
if (skipInitial) {
|
|
198
|
+
console.log(`Skipping initial compilation.`);
|
|
199
|
+
}
|
|
200
|
+
console.log(`Press Ctrl+C to stop.\n`);
|
|
171
201
|
}
|
|
172
|
-
console.log(`Press Ctrl+C to stop.\n`);
|
|
173
202
|
let isIgnored = path => {
|
|
174
203
|
let isDotfile = /(^|[\/\\])\./.test(path);
|
|
175
204
|
let isCssFile = /\.(module|global)\.(css|scss)$/.test(path);
|
|
@@ -190,20 +219,45 @@ async function watchDirectories(dirs, outputDir, skipInitial) {
|
|
|
190
219
|
persistent: true
|
|
191
220
|
}).on("ready", () => {
|
|
192
221
|
ready.contents = true;
|
|
193
|
-
|
|
222
|
+
if (!silent && !quiet) {
|
|
223
|
+
console.log(`Ready for changes.`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
194
226
|
}).on("change", path => {
|
|
195
|
-
|
|
196
|
-
|
|
227
|
+
if (!silent && !quiet) {
|
|
228
|
+
console.log(`Changed: ` + path);
|
|
229
|
+
}
|
|
230
|
+
processFile(path, outputDir, true, silent, quiet);
|
|
197
231
|
}).on("add", path => {
|
|
198
232
|
if (skipInitial && !ready.contents) {
|
|
199
233
|
return;
|
|
200
234
|
} else {
|
|
201
|
-
|
|
202
|
-
|
|
235
|
+
if (ready.contents) {
|
|
236
|
+
if (!silent && !quiet) {
|
|
237
|
+
console.log(`Added: ` + path);
|
|
238
|
+
}
|
|
239
|
+
processFile(path, outputDir, true, silent, quiet);
|
|
240
|
+
} else {
|
|
241
|
+
processFile(path, outputDir, force, silent, quiet);
|
|
242
|
+
}
|
|
203
243
|
return;
|
|
204
244
|
}
|
|
205
245
|
}).on("unlink", path => {
|
|
206
|
-
|
|
246
|
+
if (!silent && !quiet) {
|
|
247
|
+
console.log(`šļø Deleted: ` + path);
|
|
248
|
+
}
|
|
249
|
+
let match = getBaseNameAndImportType(path);
|
|
250
|
+
let outputFileName = getOutputFileName(match[0], match[1]);
|
|
251
|
+
let outputPath = Nodepath.join(Core__Option.getOr(outputDir, Nodepath.dirname(path)), outputFileName);
|
|
252
|
+
if (Nodefs.existsSync(outputPath)) {
|
|
253
|
+
Nodefs.unlinkSync(outputPath);
|
|
254
|
+
if (!silent && !quiet) {
|
|
255
|
+
console.log(`šļø Deleted compiled: ` + outputPath);
|
|
256
|
+
return;
|
|
257
|
+
} else {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
207
261
|
});
|
|
208
262
|
}
|
|
209
263
|
|
|
@@ -219,15 +273,22 @@ let helpText = `
|
|
|
219
273
|
Options
|
|
220
274
|
--watch, -w Watch for changes and regenerate bindings (directories only)
|
|
221
275
|
--skip-initial, -s Skip initial compilation in watch mode
|
|
276
|
+
--force, -f Force compilation even if files are unchanged
|
|
277
|
+
--silent Only show "Generated" messages, suppress other output
|
|
278
|
+
--quiet, -q Suppress all output
|
|
222
279
|
--output-dir, -o Directory to write generated .res files
|
|
223
280
|
(multiple files or single directory only)
|
|
224
281
|
|
|
282
|
+
By default, files are skipped if the source CSS file has not been modified
|
|
283
|
+
since the last compilation. Use --force to always recompile.
|
|
284
|
+
|
|
225
285
|
Examples
|
|
226
286
|
$ css-to-rescript src/Card.module.scss
|
|
227
287
|
$ css-to-rescript src/Theme.global.css
|
|
228
288
|
$ css-to-rescript src/Button.module.css src/Card.module.scss -o src/bindings
|
|
229
289
|
$ css-to-rescript src/components
|
|
230
290
|
$ css-to-rescript src/components src/pages --watch
|
|
291
|
+
$ css-to-rescript src/components --force
|
|
231
292
|
`;
|
|
232
293
|
|
|
233
294
|
async function main() {
|
|
@@ -247,6 +308,20 @@ async function main() {
|
|
|
247
308
|
type: "boolean",
|
|
248
309
|
shortFlag: "s",
|
|
249
310
|
default: false
|
|
311
|
+
},
|
|
312
|
+
force: {
|
|
313
|
+
type: "boolean",
|
|
314
|
+
shortFlag: "f",
|
|
315
|
+
default: false
|
|
316
|
+
},
|
|
317
|
+
silent: {
|
|
318
|
+
type: "boolean",
|
|
319
|
+
default: false
|
|
320
|
+
},
|
|
321
|
+
quiet: {
|
|
322
|
+
type: "boolean",
|
|
323
|
+
shortFlag: "q",
|
|
324
|
+
default: false
|
|
250
325
|
}
|
|
251
326
|
},
|
|
252
327
|
allowUnknownFlags: false
|
|
@@ -259,6 +334,9 @@ async function main() {
|
|
|
259
334
|
let outputDir = cli.flags.outputDir;
|
|
260
335
|
let watchMode = cli.flags.watch;
|
|
261
336
|
let skipInitial = cli.flags.skipInitial;
|
|
337
|
+
let force = cli.flags.force;
|
|
338
|
+
let silent = cli.flags.silent;
|
|
339
|
+
let quiet = cli.flags.quiet;
|
|
262
340
|
let match = Core__Array.reduce(inputPaths, [
|
|
263
341
|
[],
|
|
264
342
|
[]
|
|
@@ -296,20 +374,22 @@ async function main() {
|
|
|
296
374
|
if (files.length > 0) {
|
|
297
375
|
await Core__Array.reduce(files, Promise.resolve(), async (acc, file) => {
|
|
298
376
|
await acc;
|
|
299
|
-
await processFile(file, outputDir);
|
|
377
|
+
await processFile(file, outputDir, force, silent, quiet);
|
|
300
378
|
});
|
|
301
379
|
}
|
|
302
380
|
if (dirs.length <= 0) {
|
|
303
381
|
return;
|
|
304
382
|
}
|
|
305
383
|
if (watchMode) {
|
|
306
|
-
return await watchDirectories(dirs, outputDir, skipInitial);
|
|
384
|
+
return await watchDirectories(dirs, outputDir, skipInitial, force, silent, quiet);
|
|
307
385
|
}
|
|
308
386
|
let moduleFiles = dirs.flatMap(findCssFiles);
|
|
309
|
-
|
|
387
|
+
if (!silent && !quiet) {
|
|
388
|
+
console.log(`Found ` + moduleFiles.length.toString() + ` CSS module files\n`);
|
|
389
|
+
}
|
|
310
390
|
return await Core__Array.reduce(moduleFiles, Promise.resolve(), async (acc, file) => {
|
|
311
391
|
await acc;
|
|
312
|
-
await processFile(file, outputDir);
|
|
392
|
+
await processFile(file, outputDir, force, silent, quiet);
|
|
313
393
|
});
|
|
314
394
|
}
|
|
315
395
|
|
|
@@ -326,6 +406,7 @@ export {
|
|
|
326
406
|
generateReScriptBindings,
|
|
327
407
|
getBaseNameAndImportType,
|
|
328
408
|
getOutputFileName,
|
|
409
|
+
shouldCompile,
|
|
329
410
|
processFile,
|
|
330
411
|
findCssFiles,
|
|
331
412
|
watchDirectories,
|
package/src/CssToRescript.res
CHANGED
|
@@ -9,7 +9,14 @@ module Meow = {
|
|
|
9
9
|
default?: bool,
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
type flags = {
|
|
12
|
+
type flags = {
|
|
13
|
+
"watch": flag,
|
|
14
|
+
"outputDir": flag,
|
|
15
|
+
"skipInitial": flag,
|
|
16
|
+
"force": flag,
|
|
17
|
+
"silent": flag,
|
|
18
|
+
"quiet": flag,
|
|
19
|
+
}
|
|
13
20
|
|
|
14
21
|
type importMeta
|
|
15
22
|
|
|
@@ -21,7 +28,14 @@ module Meow = {
|
|
|
21
28
|
|
|
22
29
|
type result = {
|
|
23
30
|
input: array<string>,
|
|
24
|
-
flags: {
|
|
31
|
+
flags: {
|
|
32
|
+
"watch": bool,
|
|
33
|
+
"outputDir": option<string>,
|
|
34
|
+
"skipInitial": bool,
|
|
35
|
+
"force": bool,
|
|
36
|
+
"silent": bool,
|
|
37
|
+
"quiet": bool,
|
|
38
|
+
},
|
|
25
39
|
showHelp: unit => unit,
|
|
26
40
|
}
|
|
27
41
|
|
|
@@ -109,14 +123,14 @@ ${recordFields->Array.join(",\n")}
|
|
|
109
123
|
// CSS Module import will get access to the object mapping returned
|
|
110
124
|
// by the import. Hashing will happen automatically.
|
|
111
125
|
prelude +
|
|
112
|
-
|
|
126
|
+
`
|
|
127
|
+
@module("./${baseName}") external css: t = "default"
|
|
113
128
|
|
|
114
129
|
// Access class names from the fields of the css object.
|
|
115
130
|
// For scoped classses, the hashed class name is returned.
|
|
116
131
|
// For :global() classes, the class name is returned as-is: no scoping.
|
|
117
132
|
// Classes from @import are also available.
|
|
118
133
|
|
|
119
|
-
@module("./${baseName}") external _imported: t = "default"
|
|
120
134
|
@new external proxy: ('a, 'b) => 'c = "Proxy"
|
|
121
135
|
%%private(
|
|
122
136
|
external toDict: t => dict<string> = "%identity"
|
|
@@ -139,11 +153,11 @@ let css = withProxy(css)
|
|
|
139
153
|
`
|
|
140
154
|
| Global =>
|
|
141
155
|
// Global css will return the css class name as-is: no scoping.
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
prelude +
|
|
157
|
+
`
|
|
158
|
+
@module("./${baseName}") external _imported: t = "default"
|
|
159
|
+
|
|
144
160
|
// Access class names from the fields of the css object.
|
|
145
|
-
// Import is not done, the css has to be manually imported
|
|
146
|
-
// from the top of the component hierarchy.
|
|
147
161
|
// For all classes, the class name is returned as-is: no scoping.
|
|
148
162
|
// Classes from @import are also available.
|
|
149
163
|
|
|
@@ -188,28 +202,63 @@ let getOutputFileName = (baseName, importType) => {
|
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
204
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let
|
|
205
|
+
// Check if source file is newer than output file
|
|
206
|
+
// Returns true if compilation should proceed (source is newer or output doesn't exist)
|
|
207
|
+
let shouldCompile = (cssFilePath, outputDir) => {
|
|
208
|
+
let (baseName, importType) = cssFilePath->getBaseNameAndImportType
|
|
209
|
+
let outputFileName = getOutputFileName(baseName, importType)
|
|
210
|
+
let outputPath = NodeJs.Path.join2(
|
|
211
|
+
outputDir->Option.getOr(cssFilePath->NodeJs.Path.dirname),
|
|
212
|
+
outputFileName,
|
|
213
|
+
)
|
|
196
214
|
|
|
197
|
-
if
|
|
198
|
-
|
|
199
|
-
None
|
|
215
|
+
if !NodeJs.Fs.existsSync(outputPath) {
|
|
216
|
+
true
|
|
200
217
|
} else {
|
|
201
|
-
let
|
|
202
|
-
let
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
outputFileName,
|
|
207
|
-
)
|
|
218
|
+
let sourceStat = NodeJs.Fs.lstatSync(#String(cssFilePath))
|
|
219
|
+
let outputStat = NodeJs.Fs.lstatSync(#String(outputPath))
|
|
220
|
+
sourceStat.mtimeMs > outputStat.mtimeMs
|
|
221
|
+
}
|
|
222
|
+
}
|
|
208
223
|
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
// Process a single CSS module file
|
|
225
|
+
let processFile = async (cssFilePath, outputDir, ~force=false, ~silent=false, ~quiet=false) => {
|
|
226
|
+
// Skip if source file is not newer than output (unless force is set)
|
|
227
|
+
if !force && !shouldCompile(cssFilePath, outputDir) {
|
|
228
|
+
if !silent && !quiet {
|
|
229
|
+
Console.log(`āļø Skipped: ${cssFilePath} (unchanged)`)
|
|
230
|
+
}
|
|
231
|
+
None
|
|
232
|
+
} else {
|
|
233
|
+
let content = NodeJs.Fs.readFileSync(cssFilePath)->NodeJs.Buffer.toString
|
|
234
|
+
if !silent && !quiet {
|
|
235
|
+
Console.log(`Processing file: ${cssFilePath}`)
|
|
236
|
+
}
|
|
237
|
+
let classNames = await extractClassNames(content, ~from=cssFilePath)
|
|
211
238
|
|
|
212
|
-
|
|
239
|
+
if classNames->Array.length == 0 {
|
|
240
|
+
if !silent && !quiet {
|
|
241
|
+
Console.log(`ā ļø No classes found in ${cssFilePath}`)
|
|
242
|
+
}
|
|
243
|
+
None
|
|
244
|
+
} else {
|
|
245
|
+
let (baseName, importType) = cssFilePath->getBaseNameAndImportType
|
|
246
|
+
let outputFileName = getOutputFileName(baseName, importType)
|
|
247
|
+
let bindings = generateReScriptBindings(baseName, importType, classNames)
|
|
248
|
+
let outputPath = NodeJs.Path.join2(
|
|
249
|
+
outputDir->Option.getOr(cssFilePath->NodeJs.Path.dirname),
|
|
250
|
+
outputFileName,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
NodeJs.Fs.writeFileSync(outputPath, NodeJs.Buffer.fromString(bindings))
|
|
254
|
+
if !quiet {
|
|
255
|
+
Console.log(
|
|
256
|
+
`ā
Generated ${outputPath} (${classNames->Array.length->Int.toString} classes)`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
(outputPath, classNames)->Some
|
|
261
|
+
}
|
|
213
262
|
}
|
|
214
263
|
}
|
|
215
264
|
|
|
@@ -231,17 +280,19 @@ let rec findCssFiles = dir => {
|
|
|
231
280
|
}
|
|
232
281
|
|
|
233
282
|
// Watch directories for CSS module and global file changes
|
|
234
|
-
let watchDirectories = async (dirs, outputDir, ~skipInitial) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
283
|
+
let watchDirectories = async (dirs, outputDir, ~skipInitial, ~force, ~silent, ~quiet) => {
|
|
284
|
+
if !silent && !quiet {
|
|
285
|
+
Console.log(
|
|
286
|
+
`š Watching ${dirs
|
|
287
|
+
->Array.length
|
|
288
|
+
->Int.toString} directories for CSS module/global changes...`,
|
|
289
|
+
)
|
|
290
|
+
dirs->Array.forEach(dir => Console.log(` ${dir}`))
|
|
291
|
+
if skipInitial {
|
|
292
|
+
Console.log(`Skipping initial compilation.`)
|
|
293
|
+
}
|
|
294
|
+
Console.log(`Press Ctrl+C to stop.\n`)
|
|
243
295
|
}
|
|
244
|
-
Console.log(`Press Ctrl+C to stop.\n`)
|
|
245
296
|
|
|
246
297
|
// Set up chokidar watcher for CSS module and global files
|
|
247
298
|
let isIgnored = path => {
|
|
@@ -258,23 +309,49 @@ let watchDirectories = async (dirs, outputDir, ~skipInitial) => {
|
|
|
258
309
|
Chokidar.watch(dirs, {"ignored": isIgnored, "persistent": true})
|
|
259
310
|
->Chokidar.onReady(() => {
|
|
260
311
|
ready := true
|
|
261
|
-
|
|
312
|
+
if !silent && !quiet {
|
|
313
|
+
Console.log(`Ready for changes.`)
|
|
314
|
+
}
|
|
262
315
|
})
|
|
263
316
|
->Chokidar.on("change", path => {
|
|
264
|
-
|
|
265
|
-
|
|
317
|
+
// File was explicitly changed, always compile
|
|
318
|
+
if !silent && !quiet {
|
|
319
|
+
Console.log(`Changed: ${path}`)
|
|
320
|
+
}
|
|
321
|
+
processFile(path, outputDir, ~force=true, ~silent, ~quiet)->ignore
|
|
266
322
|
})
|
|
267
323
|
->Chokidar.on("add", path => {
|
|
268
324
|
// Skip initial files if skipInitial is set
|
|
269
325
|
if skipInitial && !ready.contents {
|
|
270
326
|
()
|
|
327
|
+
} else if ready.contents {
|
|
328
|
+
// After ready, new files should always be compiled
|
|
329
|
+
if !silent && !quiet {
|
|
330
|
+
Console.log(`Added: ${path}`)
|
|
331
|
+
}
|
|
332
|
+
processFile(path, outputDir, ~force=true, ~silent, ~quiet)->ignore
|
|
271
333
|
} else {
|
|
272
|
-
|
|
273
|
-
processFile(path, outputDir)->ignore
|
|
334
|
+
// Initial compilation: use skip-unchanged logic (unless force is set)
|
|
335
|
+
processFile(path, outputDir, ~force, ~silent, ~quiet)->ignore
|
|
274
336
|
}
|
|
275
337
|
})
|
|
276
338
|
->Chokidar.on("unlink", path => {
|
|
277
|
-
|
|
339
|
+
if !silent && !quiet {
|
|
340
|
+
Console.log(`šļø Deleted: ${path}`)
|
|
341
|
+
}
|
|
342
|
+
// Delete the corresponding compiled .res file if it exists
|
|
343
|
+
let (baseName, importType) = path->getBaseNameAndImportType
|
|
344
|
+
let outputFileName = getOutputFileName(baseName, importType)
|
|
345
|
+
let outputPath = NodeJs.Path.join2(
|
|
346
|
+
outputDir->Option.getOr(path->NodeJs.Path.dirname),
|
|
347
|
+
outputFileName,
|
|
348
|
+
)
|
|
349
|
+
if NodeJs.Fs.existsSync(outputPath) {
|
|
350
|
+
NodeJs.Fs.unlinkSync(outputPath)
|
|
351
|
+
if !silent && !quiet {
|
|
352
|
+
Console.log(`šļø Deleted compiled: ${outputPath}`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
278
355
|
})
|
|
279
356
|
->ignore
|
|
280
357
|
}
|
|
@@ -291,15 +368,22 @@ let helpText = `
|
|
|
291
368
|
Options
|
|
292
369
|
--watch, -w Watch for changes and regenerate bindings (directories only)
|
|
293
370
|
--skip-initial, -s Skip initial compilation in watch mode
|
|
371
|
+
--force, -f Force compilation even if files are unchanged
|
|
372
|
+
--silent Only show "Generated" messages, suppress other output
|
|
373
|
+
--quiet, -q Suppress all output
|
|
294
374
|
--output-dir, -o Directory to write generated .res files
|
|
295
375
|
(multiple files or single directory only)
|
|
296
376
|
|
|
377
|
+
By default, files are skipped if the source CSS file has not been modified
|
|
378
|
+
since the last compilation. Use --force to always recompile.
|
|
379
|
+
|
|
297
380
|
Examples
|
|
298
381
|
$ css-to-rescript src/Card.module.scss
|
|
299
382
|
$ css-to-rescript src/Theme.global.css
|
|
300
383
|
$ css-to-rescript src/Button.module.css src/Card.module.scss -o src/bindings
|
|
301
384
|
$ css-to-rescript src/components
|
|
302
385
|
$ css-to-rescript src/components src/pages --watch
|
|
386
|
+
$ css-to-rescript src/components --force
|
|
303
387
|
`
|
|
304
388
|
|
|
305
389
|
let main = async () => {
|
|
@@ -311,6 +395,9 @@ let main = async () => {
|
|
|
311
395
|
"watch": {Meow.type_: "boolean", shortFlag: "w", default: false},
|
|
312
396
|
"outputDir": {Meow.type_: "string", shortFlag: "o"},
|
|
313
397
|
"skipInitial": {Meow.type_: "boolean", shortFlag: "s", default: false},
|
|
398
|
+
"force": {Meow.type_: "boolean", shortFlag: "f", default: false},
|
|
399
|
+
"silent": {Meow.type_: "boolean", default: false},
|
|
400
|
+
"quiet": {Meow.type_: "boolean", shortFlag: "q", default: false},
|
|
314
401
|
},
|
|
315
402
|
allowUnknownFlags: false,
|
|
316
403
|
},
|
|
@@ -326,6 +413,9 @@ let main = async () => {
|
|
|
326
413
|
let outputDir = cli.flags["outputDir"]
|
|
327
414
|
let watchMode = cli.flags["watch"]
|
|
328
415
|
let skipInitial = cli.flags["skipInitial"]
|
|
416
|
+
let force = cli.flags["force"]
|
|
417
|
+
let silent = cli.flags["silent"]
|
|
418
|
+
let quiet = cli.flags["quiet"]
|
|
329
419
|
|
|
330
420
|
// Classify inputs as files or directories
|
|
331
421
|
let (files, dirs) = inputPaths->Array.reduce(([], []), ((files, dirs), path) => {
|
|
@@ -358,21 +448,23 @@ let main = async () => {
|
|
|
358
448
|
if files->Array.length > 0 {
|
|
359
449
|
await files->Array.reduce(Promise.resolve(), async (acc, file) => {
|
|
360
450
|
await acc
|
|
361
|
-
(await processFile(file, outputDir))->ignore
|
|
451
|
+
(await processFile(file, outputDir, ~force, ~silent, ~quiet))->ignore
|
|
362
452
|
})
|
|
363
453
|
}
|
|
364
454
|
|
|
365
455
|
// Process directories
|
|
366
456
|
if dirs->Array.length > 0 {
|
|
367
457
|
if watchMode {
|
|
368
|
-
await watchDirectories(dirs, outputDir, ~skipInitial)
|
|
458
|
+
await watchDirectories(dirs, outputDir, ~skipInitial, ~force, ~silent, ~quiet)
|
|
369
459
|
} else {
|
|
370
460
|
let moduleFiles = dirs->Array.flatMap(findCssFiles)
|
|
371
|
-
|
|
461
|
+
if !silent && !quiet {
|
|
462
|
+
Console.log(`Found ${moduleFiles->Array.length->Int.toString} CSS module files\n`)
|
|
463
|
+
}
|
|
372
464
|
|
|
373
465
|
await moduleFiles->Array.reduce(Promise.resolve(), async (acc, file) => {
|
|
374
466
|
await acc
|
|
375
|
-
(await processFile(file, outputDir))->ignore
|
|
467
|
+
(await processFile(file, outputDir, ~force, ~silent, ~quiet))->ignore
|
|
376
468
|
})
|
|
377
469
|
}
|
|
378
470
|
}
|