css-module-sync 0.1.31
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 +22 -0
- package/bin/css_sync.js +235 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
# CSS-MODULE-SYNC
|
|
3
|
+
Speed up react development by auto sync css module classes with react components.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Install
|
|
7
|
+
```bash
|
|
8
|
+
npm install css-module-sync
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Usage
|
|
12
|
+
```bash
|
|
13
|
+
npx css-sync
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Options
|
|
17
|
+
|
|
18
|
+
| Option | Description |
|
|
19
|
+
|---------|----------------------------------------------------------|
|
|
20
|
+
| --watch | Watches components all time and keeps styles in sync |
|
|
21
|
+
| --dir | Directory to watch (default is `src`) |
|
|
22
|
+
| --gen | Auto generates `.module.css` files for `.jsx` / `.tsx` files. Filename must start with uppercase letter and contain no spaces |
|
package/bin/css_sync.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { watch, existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parse } from "./css_parser.js";
|
|
7
|
+
import { format } from "./css_formatter.js";
|
|
8
|
+
|
|
9
|
+
const CWD = process.cwd();
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
// ------------------------------------------------------------------
|
|
13
|
+
// 0. CLI Arguments
|
|
14
|
+
// ------------------------------------------------------------------
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const dir_idx = args.indexOf("--dir");
|
|
17
|
+
const sort_idx = args.indexOf("--sort");
|
|
18
|
+
const has_a = args.includes("--a");
|
|
19
|
+
|
|
20
|
+
const FLAGS = {
|
|
21
|
+
watch: has_a || args.includes("--watch"),
|
|
22
|
+
gen: has_a || args.includes("--gen"),
|
|
23
|
+
sort: has_a || args.includes("--sort"),
|
|
24
|
+
dir: dir_idx > -1 && args[dir_idx + 1] ? args[dir_idx + 1] : "src"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function find_root(start) {
|
|
28
|
+
let curr = start;
|
|
29
|
+
while (curr !== path.parse(curr).root) {
|
|
30
|
+
if (existsSync(path.join(curr, "package.json"))) return curr;
|
|
31
|
+
curr = path.dirname(curr);
|
|
32
|
+
}
|
|
33
|
+
return start;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ROOT_DIR = find_root(CWD);
|
|
37
|
+
const TARGET_DIR = path.basename(CWD) === FLAGS.dir ? CWD : path.resolve(ROOT_DIR, FLAGS.dir);
|
|
38
|
+
let SORT_SPEC = [];
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------------
|
|
41
|
+
// 1. Helpers
|
|
42
|
+
// ------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function extract_classes(code) {
|
|
45
|
+
code = code.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/.*$/gm, " ");
|
|
46
|
+
const out = [], seen = new Set();
|
|
47
|
+
code = code.replace(/\bstyles\[(["'])([\w-]+)\1\]/g, (_, __, cls) => ` __SB_${cls}__ `);
|
|
48
|
+
code = code.replace(/(["'])(?:\\.|(?!\1)[\s\S])*\1/g, " ");
|
|
49
|
+
for (const m of code.matchAll(/__SB_([\w-]+)__/g)) if (!seen.has(m[1])) {
|
|
50
|
+
seen.add(m[1]);
|
|
51
|
+
out.push(m[1]);
|
|
52
|
+
}
|
|
53
|
+
for (const m of code.matchAll(/\bstyles\.([A-Za-z_]\w*)\b/g)) if (!seen.has(m[1])) {
|
|
54
|
+
seen.add(m[1]);
|
|
55
|
+
out.push(m[1]);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function get_classes(node) {
|
|
61
|
+
const set = new Set();
|
|
62
|
+
if (node.type === "block" && node.selector) {
|
|
63
|
+
const m = node.selector.match(/\.[A-Za-z_][\w-]*/g);
|
|
64
|
+
if (m) m.forEach(c => set.add(c.slice(1)));
|
|
65
|
+
}
|
|
66
|
+
return set;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function has_content(node) {
|
|
70
|
+
if (node.type === "leaf") return !!node.content;
|
|
71
|
+
return node.children ? node.children.some(has_content) : false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function update_marker(node, is_unused) {
|
|
75
|
+
node.comments = (node.comments || []).filter(c => !c.includes("UNUSED") && c !== "/* unused class */");
|
|
76
|
+
if (is_unused) node.comments.unshift("/* unused class */");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function resolve_css_path(tsx_path) {
|
|
80
|
+
const dir = path.dirname(tsx_path);
|
|
81
|
+
const name = path.basename(tsx_path, path.extname(tsx_path));
|
|
82
|
+
const module_path = path.join(dir, `${name}.module.css`);
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(module_path);
|
|
85
|
+
return module_path;
|
|
86
|
+
} catch { }
|
|
87
|
+
if (FLAGS.gen && /^[A-Z][^ ]*$/.test(name)) {
|
|
88
|
+
await fs.writeFile(module_path, "/* Auto-generated */\n");
|
|
89
|
+
console.log(`Generated: ${path.relative(CWD, module_path)}`);
|
|
90
|
+
return module_path;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ------------------------------------------------------------------
|
|
96
|
+
// 2. Sync Logic
|
|
97
|
+
// ------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
async function format_css_only(css_path) {
|
|
100
|
+
try {
|
|
101
|
+
const css = await fs.readFile(css_path, "utf-8");
|
|
102
|
+
const root = parse(css);
|
|
103
|
+
const out = format(root, SORT_SPEC);
|
|
104
|
+
if (out !== css) {
|
|
105
|
+
await fs.writeFile(css_path, out);
|
|
106
|
+
console.log(`Formatted: ${path.relative(CWD, css_path)}`);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) { }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function sync_file(tsx_path) {
|
|
112
|
+
const css_path = await resolve_css_path(tsx_path);
|
|
113
|
+
if (!css_path) return;
|
|
114
|
+
let tsx, css;
|
|
115
|
+
try {
|
|
116
|
+
[tsx, css] = await Promise.all([fs.readFile(tsx_path, "utf-8"), fs.readFile(css_path, "utf-8")]);
|
|
117
|
+
} catch (e) { return; }
|
|
118
|
+
if (FLAGS.gen) {
|
|
119
|
+
const target = `import styles from "./${path.basename(css_path)}";`;
|
|
120
|
+
let found = false;
|
|
121
|
+
const next = tsx.replace(/^import\s+styles\s+from\s+["'].+["'];?\n?/gm, (m) => {
|
|
122
|
+
if (m.includes(`./${path.basename(css_path)}`)) { found = true; return m; }
|
|
123
|
+
return "";
|
|
124
|
+
});
|
|
125
|
+
const final = found ? next : `${target}\n${next}`;
|
|
126
|
+
if (final !== tsx) await fs.writeFile(tsx_path, tsx = final);
|
|
127
|
+
}
|
|
128
|
+
const used_ordered = extract_classes(tsx);
|
|
129
|
+
let root;
|
|
130
|
+
try { root = parse(css); } catch { return; }
|
|
131
|
+
const class_to_idxs = new Map();
|
|
132
|
+
root.children.forEach((node, i) => {
|
|
133
|
+
get_classes(node).forEach(c => {
|
|
134
|
+
const list = class_to_idxs.get(c) || [];
|
|
135
|
+
list.push(i);
|
|
136
|
+
class_to_idxs.set(c, list);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
const new_children = [], moved_idxs = new Set();
|
|
140
|
+
for (const cls of used_ordered) {
|
|
141
|
+
const idxs = class_to_idxs.get(cls);
|
|
142
|
+
if (idxs) {
|
|
143
|
+
for (const i of idxs) if (!moved_idxs.has(i)) {
|
|
144
|
+
update_marker(root.children[i], false);
|
|
145
|
+
new_children.push(root.children[i]);
|
|
146
|
+
moved_idxs.add(i);
|
|
147
|
+
}
|
|
148
|
+
} else new_children.push({ type: "block", selector: `.${cls}`, children: [], comments: [] });
|
|
149
|
+
}
|
|
150
|
+
root.children.forEach((node, i) => {
|
|
151
|
+
if (moved_idxs.has(i)) return;
|
|
152
|
+
const classes = get_classes(node);
|
|
153
|
+
if (classes.size === 0) new_children.push(node);
|
|
154
|
+
else if (has_content(node)) {
|
|
155
|
+
update_marker(node, true);
|
|
156
|
+
new_children.push(node);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
root.children = new_children;
|
|
160
|
+
let out;
|
|
161
|
+
try { out = format(root, SORT_SPEC); } catch { return; }
|
|
162
|
+
if (out !== css) {
|
|
163
|
+
await fs.writeFile(css_path, out);
|
|
164
|
+
console.log(`Updated: ${path.relative(CWD, css_path)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ------------------------------------------------------------------
|
|
169
|
+
// 3. Execution
|
|
170
|
+
// ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
async function run_scan() {
|
|
173
|
+
try {
|
|
174
|
+
const files = await fs.readdir(TARGET_DIR, { recursive: true });
|
|
175
|
+
for (const f of files) if (!f.includes("node_modules") && /\.(jsx|tsx)$/.test(f)) await sync_file(path.join(TARGET_DIR, f));
|
|
176
|
+
} catch (e) { console.error(`Error scanning ${TARGET_DIR}:`, e.message); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function main() {
|
|
180
|
+
const default_path = path.join(__dirname, "default_sort_spec.json");
|
|
181
|
+
let sort_path = default_path;
|
|
182
|
+
let custom_mode = false;
|
|
183
|
+
if (sort_idx > -1 && args[sort_idx + 1] && !args[sort_idx + 1].startsWith("--")) {
|
|
184
|
+
sort_path = path.resolve(CWD, args[sort_idx + 1]);
|
|
185
|
+
custom_mode = true;
|
|
186
|
+
}
|
|
187
|
+
async function load_spec(p) {
|
|
188
|
+
const raw = await fs.readFile(p, "utf-8");
|
|
189
|
+
const parsed = JSON.parse(raw);
|
|
190
|
+
if (!Array.isArray(parsed)) throw new Error("JSON is not an Array");
|
|
191
|
+
return parsed;
|
|
192
|
+
}
|
|
193
|
+
if (FLAGS.sort) {
|
|
194
|
+
try {
|
|
195
|
+
SORT_SPEC = await load_spec(sort_path);
|
|
196
|
+
console.log(`[SORT] Loaded: ${path.relative(CWD, sort_path)} (${SORT_SPEC.length} categories)`);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
if (custom_mode) {
|
|
199
|
+
console.warn(`[SORT] Custom spec failed (${e.message}). Falling back to default...`);
|
|
200
|
+
try {
|
|
201
|
+
SORT_SPEC = await load_spec(default_path);
|
|
202
|
+
console.log(`[SORT] Loaded default: ${path.relative(CWD, default_path)} (${SORT_SPEC.length} categories)`);
|
|
203
|
+
} catch (e2) {
|
|
204
|
+
console.error(`[SORT] Critical: Could not load default spec. Sorting disabled.`);
|
|
205
|
+
SORT_SPEC = [];
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
console.error(`[SORT] Error: Could not load default spec (${e.message}).`);
|
|
209
|
+
SORT_SPEC = [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
console.log(`Mode: ${FLAGS.watch ? "Watch" : "Scan"}\nDir: ${TARGET_DIR}\nGen: ${FLAGS.gen ? "Enabled" : "Disabled"}\nSort: ${FLAGS.sort ? "Enabled" : "Disabled"}\n`);
|
|
214
|
+
await run_scan();
|
|
215
|
+
if (FLAGS.watch) {
|
|
216
|
+
const debouncers = new Map();
|
|
217
|
+
console.log("Watching for changes...");
|
|
218
|
+
watch(TARGET_DIR, { recursive: true }, (_, filename) => {
|
|
219
|
+
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);
|
|
225
|
+
if (debouncers.has(full_path)) clearTimeout(debouncers.get(full_path));
|
|
226
|
+
debouncers.set(full_path, setTimeout(() => {
|
|
227
|
+
debouncers.delete(full_path);
|
|
228
|
+
if (is_tsx) sync_file(full_path).catch(() => { });
|
|
229
|
+
else if (FLAGS.sort) format_css_only(full_path).catch(() => { });
|
|
230
|
+
}, 100));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "css-module-sync",
|
|
3
|
+
"version": "0.1.31",
|
|
4
|
+
"description": "Auto sync css module classes with react components",
|
|
5
|
+
"author": "Max Matinpalo",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/max-matinpalo/css-module-sync#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/max-matinpalo/css-module-sync.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/max-matinpalo/css-module-sync/issues"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"css-sync": "./bin/css_sync.js"
|
|
18
|
+
},
|
|
19
|
+
"files": "src"
|
|
20
|
+
}
|