css-module-sync 0.1.32 → 0.1.34

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
@@ -1,6 +1,6 @@
1
1
 
2
- # CSS-MODULE-SYNC
3
- Speed up react development by auto sync css module classes with react components.
2
+ # css-module-sync
3
+ Auto sync css module classes with react components.
4
4
 
5
5
 
6
6
  ### Install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "css-module-sync",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Auto sync css module classes with react components",
5
5
  "author": "Max Matinpalo",
6
6
  "type": "module",
@@ -15,6 +15,5 @@
15
15
  },
16
16
  "bin": {
17
17
  "css-sync": "./src/css_sync.js"
18
- },
19
- "files": "src"
18
+ }
20
19
  }
@@ -0,0 +1,128 @@
1
+ const BLOCK_MIN_DECLS = 7;
2
+
3
+ /**
4
+ * css_formatter.js
5
+ * Converts AST back to CSS string with category-based sorting.
6
+ */
7
+
8
+ export function format(root, sort_spec = []) {
9
+ const category_set = new Set(sort_spec.map(s => s.category.toUpperCase()));
10
+ category_set.add("ELSE");
11
+
12
+ const is_cat = (str) => {
13
+ if (!str) return false;
14
+ const clean = str.replace(/^\/\*+|\*+\/$/g, "").trim();
15
+ if (category_set.has(clean.toUpperCase())) return true;
16
+ return /^[A-Z_\s]+$/.test(clean);
17
+ };
18
+
19
+ function process(node, depth, is_last = false) {
20
+ const indent = "\t".repeat(depth);
21
+ let out = "";
22
+
23
+ if (node.comments?.length) {
24
+ for (const c of node.comments) {
25
+ if (c.trim() === "/* Auto-generated */" || is_cat(c)) continue;
26
+ out += `${indent}${c}\n`;
27
+ }
28
+ }
29
+
30
+ if (node.type === "leaf" && node.content) {
31
+ out += `${indent}${node.content.replace(/;+$/, "").trim()};\n`;
32
+ } else if (node.type === "block") {
33
+ const head = node.selector ? `${indent}${node.selector} {\n` : "";
34
+ const tail = node.selector ? `${indent}}\n` : "";
35
+ out += head;
36
+
37
+ if (node.children) {
38
+ const decls = [];
39
+ const others = [];
40
+
41
+ for (const child of node.children) {
42
+ if (child.type === "comment" && is_cat(child.content)) continue;
43
+ if (child.type === "header") continue;
44
+
45
+ const is_decl = child.type === "leaf" &&
46
+ child.content &&
47
+ child.content.includes(":") &&
48
+ !child.content.trim().startsWith("@");
49
+
50
+ (is_decl ? decls : others).push(child);
51
+ }
52
+
53
+ if (node.selector && decls.length === 0 && others.length > 0) out += "\n";
54
+
55
+ if (decls.length) {
56
+ const grouped = {};
57
+ const else_idx = sort_spec.length;
58
+
59
+ for (const decl of decls) {
60
+ const prop = decl.content.split(":")[0].trim();
61
+ const cat_idx = sort_spec.findIndex(s => s.keywords.some(k => {
62
+ const clean_k = k.replace(/\.+$/, "");
63
+ return k.includes("...") ? (prop === clean_k || prop.startsWith(clean_k + "-")) : prop === k;
64
+ }));
65
+ const key = cat_idx === -1 ? else_idx : cat_idx;
66
+ (grouped[key] ||= []).push(decl);
67
+ }
68
+
69
+ Object.keys(grouped).forEach(idx => {
70
+ const cat = sort_spec[idx];
71
+ grouped[idx].sort((a, b) => {
72
+ const prop_a = a.content.split(":")[0].trim();
73
+ const prop_b = b.content.split(":")[0].trim();
74
+ if (!cat) return prop_a.localeCompare(prop_b);
75
+
76
+ const find = (p) => cat.keywords.findIndex(k => {
77
+ const clean_k = k.replace(/\.+$/, "");
78
+ return k.includes("...") ? (p === clean_k || p.startsWith(clean_k + "-")) : p === k;
79
+ });
80
+ const i_a = find(prop_a);
81
+ const i_b = find(prop_b);
82
+
83
+ if (i_a !== i_b) return i_a - i_b;
84
+ return prop_a.localeCompare(prop_b);
85
+ });
86
+ });
87
+
88
+ const processed = [];
89
+ const sorted_keys = Object.keys(grouped).sort((a, b) => Number(a) - Number(b));
90
+ const skip_all_spacers = decls.length < BLOCK_MIN_DECLS;
91
+ let accumulated_decls = 0;
92
+
93
+ sorted_keys.forEach((idx, k_idx) => {
94
+ const items = grouped[idx];
95
+ if (k_idx > 0 && !skip_all_spacers && items.length >= 2 && accumulated_decls >= 2) {
96
+ processed.push({ type: "header", content: "" });
97
+ }
98
+ processed.push(...items);
99
+ accumulated_decls += items.length;
100
+ });
101
+
102
+ const all_children = [...processed, ...others];
103
+ all_children.forEach((child, i) => {
104
+ const is_child_last = i === all_children.length - 1;
105
+ if (child.type === "header") out += `${child.content}\n`;
106
+ else out += process(child, node.selector ? depth + 1 : depth, is_child_last);
107
+ });
108
+ } else {
109
+ others.forEach((child, i) => {
110
+ const is_child_last = i === others.length - 1;
111
+ out += process(child, node.selector ? depth + 1 : depth, is_child_last);
112
+ });
113
+ }
114
+ }
115
+
116
+ out += tail;
117
+ // Only add empty line between top-level blocks
118
+ if (node.selector && depth === 0 && !is_last) out += "\n";
119
+ }
120
+
121
+ return out;
122
+ }
123
+
124
+ const root_children = root.children || [];
125
+ return root_children.map((child, i) =>
126
+ process(child, 0, i === root_children.length - 1)
127
+ ).join("");
128
+ }
@@ -0,0 +1,77 @@
1
+ // css_parser.js
2
+ export function parse(input) {
3
+ let i = 0;
4
+ const n = input.length;
5
+
6
+ const root = { type: "block", selector: null, children: [], comments: [] };
7
+ let pending = [];
8
+
9
+ const is_ws = c => c === " " || c === "\n" || c === "\t" || c === "\r";
10
+ const skip_ws = () => { while (i < n && is_ws(input[i])) i++; };
11
+
12
+ const read_comment = () => {
13
+ if (input[i] !== "/" || input[i + 1] !== "*") return null;
14
+ const start = i;
15
+ i += 2;
16
+ while (i < n && !(input[i] === "*" && input[i + 1] === "/")) i++;
17
+ i = Math.min(n, i + 2);
18
+ return input.slice(start, i);
19
+ };
20
+
21
+ const read_until = stops => {
22
+ let out = "", q = null, esc = false;
23
+ while (i < n) {
24
+ const c = input[i];
25
+ if (esc) { out += c; esc = false; i++; continue; }
26
+ if (c === "\\") { out += c; esc = true; i++; continue; }
27
+ if (q) { if (c === q) q = null; out += c; i++; continue; }
28
+ if (c === "'" || c === `"`) { q = c; out += c; i++; continue; }
29
+ if (stops.includes(c)) break;
30
+ out += c; i++;
31
+ }
32
+ return out;
33
+ };
34
+
35
+ const parse_block = selector => {
36
+ const node = { type: "block", selector, children: [], comments: pending };
37
+ pending = [];
38
+ i++; // skip '{'
39
+ while (i < n) {
40
+ skip_ws();
41
+ const cmt = read_comment();
42
+ if (cmt) { pending.push(cmt); continue; }
43
+ if (input[i] === "}") { i++; break; }
44
+
45
+ const head = read_until(["{", ";", "}"]).trim();
46
+ if (!head) { if (input[i] === ";") i++; continue; }
47
+
48
+ if (input[i] === "{") node.children.push(parse_block(head));
49
+ else {
50
+ if (input[i] === ";") i++;
51
+ node.children.push({ type: "leaf", content: head, comments: pending });
52
+ pending = [];
53
+ }
54
+ }
55
+ return node;
56
+ };
57
+
58
+ while (i < n) {
59
+ skip_ws();
60
+ const cmt = read_comment();
61
+ if (cmt) { pending.push(cmt); continue; }
62
+ if (i >= n) break;
63
+
64
+ const head = read_until(["{", ";"]).trim();
65
+ if (!head) { i++; continue; }
66
+
67
+ if (input[i] === "{") root.children.push(parse_block(head));
68
+ else {
69
+ if (input[i] === ";") i++;
70
+ root.children.push({ type: "leaf", content: head, comments: pending });
71
+ pending = [];
72
+ }
73
+ }
74
+
75
+ if (pending.length) root.comments = pending;
76
+ return root;
77
+ }
package/src/css_sync.js CHANGED
@@ -214,14 +214,51 @@ async function main() {
214
214
  await run_scan();
215
215
  if (FLAGS.watch) {
216
216
  const debouncers = new Map();
217
+ let cache = new Set();
218
+ const refresh_cache = async () => {
219
+ const files = await fs.readdir(TARGET_DIR, { recursive: true });
220
+ cache = new Set(files.map(f => path.resolve(TARGET_DIR, f)));
221
+ };
222
+ await refresh_cache();
223
+
217
224
  console.log("Watching for changes...");
218
- watch(TARGET_DIR, { recursive: true }, (_, filename) => {
225
+ watch(TARGET_DIR, { recursive: true }, async (event, filename) => {
219
226
  if (!filename) return;
220
- const name = filename.toString();
221
- const is_tsx = /\.(jsx|tsx)$/.test(name);
222
- const is_css = /\.css$/.test(name);
223
- if (!is_tsx && !is_css) return;
224
- const full_path = path.resolve(TARGET_DIR, name);
227
+ const full_path = path.resolve(TARGET_DIR, filename.toString());
228
+ const exists = existsSync(full_path);
229
+ const is_tsx = /\.(jsx|tsx)$/.test(full_path);
230
+
231
+ if (event === "rename") {
232
+ const was_cached = cache.has(full_path);
233
+ if (!exists) setTimeout(() => { if (!existsSync(full_path)) cache.delete(full_path); }, 100);
234
+ else {
235
+ cache.add(full_path);
236
+ if (is_tsx && FLAGS.gen && !was_cached) setTimeout(async () => {
237
+ const dir = path.dirname(full_path);
238
+ const name = path.basename(full_path, path.extname(full_path));
239
+ const orphans = Array.from(cache).filter(p => {
240
+ if (!p.endsWith(".module.css") || path.dirname(p) !== dir || !existsSync(p)) return false;
241
+ const base = path.basename(p, ".module.css");
242
+ return !existsSync(path.join(dir, `${base}.tsx`)) && !existsSync(path.join(dir, `${base}.jsx`));
243
+ });
244
+ if (orphans.length === 1) {
245
+ const orphan = orphans[0];
246
+ const next_css = path.join(dir, `${name}.module.css`);
247
+ if (!existsSync(next_css)) {
248
+ await fs.rename(orphan, next_css);
249
+ cache.delete(orphan);
250
+ cache.add(next_css);
251
+ console.log(`Renamed: ${path.basename(orphan)} -> ${path.basename(next_css)}`);
252
+ }
253
+ } else if (orphans.length > 1) {
254
+ console.warn(`[RENAME] Multiple orphan .module.css in ${dir}; skipping:\n` +
255
+ orphans.map(p => `- ${path.basename(p)}`).join("\n"));
256
+ }
257
+ }, 150);
258
+ }
259
+ }
260
+
261
+ if (!is_tsx && !/\.css$/.test(full_path)) return;
225
262
  if (debouncers.has(full_path)) clearTimeout(debouncers.get(full_path));
226
263
  debouncers.set(full_path, setTimeout(() => {
227
264
  debouncers.delete(full_path);
@@ -0,0 +1,85 @@
1
+ [
2
+ {
3
+ "category": "POSITION",
4
+ "keywords": [
5
+ "position",
6
+ "inset",
7
+ "top",
8
+ "right",
9
+ "bottom",
10
+ "left",
11
+ "z-index"
12
+ ]
13
+ },
14
+ {
15
+ "category": "LAYOUT",
16
+ "keywords": [
17
+ "display",
18
+ "visibility",
19
+ "overflow...",
20
+ "flex...",
21
+ "justify-content",
22
+ "align-items",
23
+ "align-content",
24
+ "place-content",
25
+ "align-self",
26
+ "order",
27
+ "gap",
28
+ "row-gap",
29
+ "column-gap",
30
+ "grid..."
31
+ ]
32
+ },
33
+ {
34
+ "category": "BOX",
35
+ "keywords": [
36
+ "width",
37
+ "min-width",
38
+ "max-width",
39
+ "height",
40
+ "min-height",
41
+ "max-height",
42
+ "aspect-ratio",
43
+ "margin...",
44
+ "padding...",
45
+ "border..."
46
+ ]
47
+ },
48
+ {
49
+ "category": "COLOR",
50
+ "keywords": [
51
+ "color",
52
+ "background...",
53
+ "opacity",
54
+ "box-shadow"
55
+ ]
56
+ },
57
+ {
58
+ "category": "TYPO",
59
+ "keywords": [
60
+ "font",
61
+ "font-family",
62
+ "font-size",
63
+ "font-weight",
64
+ "font-style",
65
+ "font...",
66
+ "font-stretch",
67
+ "line-height",
68
+ "letter-spacing",
69
+ "word-spacing",
70
+ "text-align",
71
+ "text-transform",
72
+ "text-decoration....",
73
+ "text-underline-offset",
74
+ "white-space"
75
+ ]
76
+ },
77
+ {
78
+ "category": "ANIMATION",
79
+ "keywords": [
80
+ "transform...",
81
+ "transition...",
82
+ "animation..."
83
+ ]
84
+ }
85
+ ]