@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 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
- 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).
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
- Even though global CSS classes aren't scoped, generating typed bindings provides the benefit of compile-time checking that class names exist.
29
+ Support for global CSS is provided in addition to CSS Modules.
32
30
 
33
- The imports are not done because these css typically has to be imported from the top of the component hierarchy. So the import has to be done manually. To get type safe access, simply rename the file to `.global.css` (or create one and import the original css from it).
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
- (Remark: we could also provide a way to import the css automatically, but this would open issues with removing duplicates during bundling, and handling the css on route changes. NextJs does not support these use cases out of the box, so we revert to the manual import for global css.)
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
- // Import the CSS manually at your app root
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
 
@@ -26620,14 +26620,14 @@ type t = {
26620
26620
  }
26621
26621
  `;
26622
26622
  if (importType === "Module") {
26623
- return prelude + (`@module("./` + baseName + `") external css: t = "default"
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
- async function processFile(cssFilePath, outputDir) {
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
- console.log(`Processing file: ` + cssFilePath);
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
- console.log(`\u26A0\uFE0F No classes found in ` + cssFilePath);
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
- console.log(`\u2705 Generated ` + outputPath + ` (` + classNames.length.toString() + ` classes)`);
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
- console.log(`\u{1F440} Watching ` + dirs.length.toString() + ` directories for CSS module/global changes...`);
26725
- dirs.forEach((dir) => {
26726
- console.log(` ` + dir);
26727
- });
26728
- if (skipInitial) {
26729
- console.log(`Skipping initial compilation.`);
26730
- }
26731
- console.log(`Press Ctrl+C to stop.
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
- console.log(`Ready for changes.`);
26781
+ if (!silent && !quiet) {
26782
+ console.log(`Ready for changes.`);
26783
+ return;
26784
+ }
26754
26785
  }).on("change", (path3) => {
26755
- console.log(`
26756
- Changed: ` + path3);
26757
- processFile(path3, outputDir);
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
- console.log(`
26763
- Added: ` + path3);
26764
- processFile(path3, outputDir);
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
- console.log(`
26769
- \u{1F5D1}\uFE0F Deleted: ` + path3);
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
- console.log(`Found ` + moduleFiles.length.toString() + ` CSS module files
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@greenfinity/rescript-typed-css-modules",
3
3
  "description": "Typed CSS Modules for ReScript",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -56,14 +56,14 @@ type t = {
56
56
  }
57
57
  `;
58
58
  if (importType === "Module") {
59
- return prelude + (`@module("./` + baseName + `") external css: t = "default"
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
- async function processFile(cssFilePath, outputDir) {
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
- console.log(`Processing file: ` + cssFilePath);
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
- console.log(`āš ļø No classes found in ` + cssFilePath);
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
- console.log(`āœ… Generated ` + outputPath + ` (` + classNames.length.toString() + ` classes)`);
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
- 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.`);
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
- console.log(`Ready for changes.`);
222
+ if (!silent && !quiet) {
223
+ console.log(`Ready for changes.`);
224
+ return;
225
+ }
194
226
  }).on("change", path => {
195
- console.log(`\nChanged: ` + path);
196
- processFile(path, outputDir);
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
- console.log(`\nAdded: ` + path);
202
- processFile(path, outputDir);
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
- console.log(`\nšŸ—‘ļø Deleted: ` + path);
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
- console.log(`Found ` + moduleFiles.length.toString() + ` CSS module files\n`);
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,
@@ -9,7 +9,14 @@ module Meow = {
9
9
  default?: bool,
10
10
  }
11
11
 
12
- type flags = {"watch": flag, "outputDir": flag, "skipInitial": flag}
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: {"watch": bool, "outputDir": option<string>, "skipInitial": bool},
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
- `@module("./${baseName}") external css: t = "default"
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
- // Import is not done.
143
- prelude + `
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
- // Process a single CSS module file
192
- let processFile = async (cssFilePath, outputDir) => {
193
- let content = NodeJs.Fs.readFileSync(cssFilePath)->NodeJs.Buffer.toString
194
- Console.log(`Processing file: ${cssFilePath}`)
195
- let classNames = await extractClassNames(content, ~from=cssFilePath)
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 classNames->Array.length == 0 {
198
- Console.log(`āš ļø No classes found in ${cssFilePath}`)
199
- None
215
+ if !NodeJs.Fs.existsSync(outputPath) {
216
+ true
200
217
  } else {
201
- let (baseName, importType) = cssFilePath->getBaseNameAndImportType
202
- let outputFileName = getOutputFileName(baseName, importType)
203
- let bindings = generateReScriptBindings(baseName, importType, classNames)
204
- let outputPath = NodeJs.Path.join2(
205
- outputDir->Option.getOr(cssFilePath->NodeJs.Path.dirname),
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
- NodeJs.Fs.writeFileSync(outputPath, NodeJs.Buffer.fromString(bindings))
210
- Console.log(`āœ… Generated ${outputPath} (${classNames->Array.length->Int.toString} classes)`)
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
- (outputPath, classNames)->Some
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
- Console.log(
236
- `šŸ‘€ Watching ${dirs
237
- ->Array.length
238
- ->Int.toString} directories for CSS module/global changes...`,
239
- )
240
- dirs->Array.forEach(dir => Console.log(` ${dir}`))
241
- if skipInitial {
242
- Console.log(`Skipping initial compilation.`)
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
- Console.log(`Ready for changes.`)
312
+ if !silent && !quiet {
313
+ Console.log(`Ready for changes.`)
314
+ }
262
315
  })
263
316
  ->Chokidar.on("change", path => {
264
- Console.log(`\nChanged: ${path}`)
265
- processFile(path, outputDir)->ignore
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
- Console.log(`\nAdded: ${path}`)
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
- Console.log(`\nšŸ—‘ļø Deleted: ${path}`)
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
- Console.log(`Found ${moduleFiles->Array.length->Int.toString} CSS module files\n`)
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
  }