chokibasic 1.0.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/index.js +1 -0
- package/package.json +37 -0
- package/readme.md +41 -0
- package/src/watcher.js +258 -0
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./src/watcher.js");
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chokibasic",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Basic chokidar watcher + esbuild + sass + csso helpers",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ZmotriN/chokibasic.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chokidar",
|
|
15
|
+
"watch",
|
|
16
|
+
"sass",
|
|
17
|
+
"esbuild",
|
|
18
|
+
"csso"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "commonjs",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ZmotriN/chokibasic/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/ZmotriN/chokibasic#readme",
|
|
27
|
+
"files": [
|
|
28
|
+
"index.js",
|
|
29
|
+
"src/"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chokidar": "^5.0.0",
|
|
33
|
+
"csso": "^5.0.5",
|
|
34
|
+
"esbuild": "^0.27.3",
|
|
35
|
+
"sass": "^1.97.3"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# chokibasic
|
|
2
|
+
|
|
3
|
+
Helpers:
|
|
4
|
+
- `createWatchers(rules, options)`
|
|
5
|
+
- `buildCSS(inputScss, outCssMin)`
|
|
6
|
+
- `buildJS(entry, outfile)`
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
```bash
|
|
10
|
+
npm i chokibasic
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
```JS
|
|
15
|
+
const { createWatchers, buildCSS, buildJS } = require("chokibasic");
|
|
16
|
+
|
|
17
|
+
const w = createWatchers(
|
|
18
|
+
[
|
|
19
|
+
{
|
|
20
|
+
name: "css",
|
|
21
|
+
patterns: ["src/styles/**/*.scss"],
|
|
22
|
+
callback: async (events) => {
|
|
23
|
+
console.log("css events:", events);
|
|
24
|
+
await buildCSS("src/styles/main.scss", "dist/app.min.css");
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "js",
|
|
29
|
+
patterns: ["src/scripts/**/*.js"],
|
|
30
|
+
ignored: ["**/*.min.js"],
|
|
31
|
+
callback: async (events) => {
|
|
32
|
+
console.log("js events:", events);
|
|
33
|
+
await buildJS("src/scripts/main.js", "dist/app.min.js");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
{ debug: true }
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// later: await w.close();
|
|
41
|
+
```
|
package/src/watcher.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const chokidar = require("chokidar");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const esbuild = require("esbuild");
|
|
5
|
+
const sass = require("sass");
|
|
6
|
+
const csso = require("csso");
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
function toPosix(p) {
|
|
10
|
+
return p.replace(/\\/g, "/");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizePatterns(patterns) {
|
|
14
|
+
return Array.isArray(patterns) ? patterns : [patterns];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function globBaseDir(glob) {
|
|
18
|
+
const g = toPosix(glob);
|
|
19
|
+
const idx = g.search(/[*?\[]/);
|
|
20
|
+
if (idx === -1) return g;
|
|
21
|
+
const base = g.slice(0, idx);
|
|
22
|
+
return base.replace(/\/+$/, "") || ".";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function globToRegExp(glob) {
|
|
26
|
+
const g = toPosix(glob);
|
|
27
|
+
|
|
28
|
+
let re = "^";
|
|
29
|
+
for (let i = 0; i < g.length; i++) {
|
|
30
|
+
const c = g[i];
|
|
31
|
+
|
|
32
|
+
if (c === "*" && g[i + 1] === "*") {
|
|
33
|
+
i++;
|
|
34
|
+
if (g[i + 1] === "/") {
|
|
35
|
+
i++;
|
|
36
|
+
re += "(?:.*/)?";
|
|
37
|
+
} else {
|
|
38
|
+
re += ".*";
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (c === "*") { re += "[^/]*"; continue; }
|
|
44
|
+
if (c === "?") { re += "[^/]"; continue; }
|
|
45
|
+
|
|
46
|
+
if ("\\.[]{}()+-^$|".includes(c)) re += "\\" + c;
|
|
47
|
+
else re += c;
|
|
48
|
+
}
|
|
49
|
+
re += "$";
|
|
50
|
+
return new RegExp(re);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isIgnoredPosix(relPosix, ignoreMatchers) {
|
|
54
|
+
for (const m of ignoreMatchers) {
|
|
55
|
+
if (m instanceof RegExp) {
|
|
56
|
+
if (m.test(relPosix)) return true;
|
|
57
|
+
} else if (typeof m === "function") {
|
|
58
|
+
// si l'user fournit une fonction ignorée, on lui passe le rel posix
|
|
59
|
+
if (m(relPosix)) return true;
|
|
60
|
+
} else if (typeof m === "string") {
|
|
61
|
+
// strings glob déjà compilées dans ignoreMatchers (voir compileIgnoreMatchers)
|
|
62
|
+
// (fallback: on ne devrait pas arriver ici)
|
|
63
|
+
if (globToRegExp(m).test(relPosix)) return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compileIgnoreMatchers(ignoreList) {
|
|
70
|
+
return (ignoreList || []).map((ig) => {
|
|
71
|
+
if (ig instanceof RegExp) return ig;
|
|
72
|
+
if (typeof ig === "function") return ig;
|
|
73
|
+
// string glob -> RegExp
|
|
74
|
+
return globToRegExp(ig);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* createWatchers(rules, options)
|
|
80
|
+
* - Watch les dossiers racines (baseDirs)
|
|
81
|
+
* - Filtre include via globs (rule.patterns)
|
|
82
|
+
* - Filtre ignore via globs/regex (globalIgnored + rule.ignored)
|
|
83
|
+
*/
|
|
84
|
+
function createWatchers(rules, options = {}) {
|
|
85
|
+
const opt = {
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
|
|
88
|
+
// Ignorés globaux (appliqués dans queue() — pas dans chokidar)
|
|
89
|
+
globalIgnored: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
|
|
90
|
+
|
|
91
|
+
ignoreInitial: true,
|
|
92
|
+
awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 10 },
|
|
93
|
+
|
|
94
|
+
// Si FS weird (OneDrive, réseau, WSL/Docker bind mount), mets true:
|
|
95
|
+
usePolling: false,
|
|
96
|
+
interval: 200,
|
|
97
|
+
binaryInterval: 300,
|
|
98
|
+
|
|
99
|
+
debug: true,
|
|
100
|
+
...options,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handles = [];
|
|
104
|
+
|
|
105
|
+
for (const rule of rules) {
|
|
106
|
+
if (!rule || typeof rule.callback !== "function") {
|
|
107
|
+
throw new Error("Each rule must have a callback(events, ctx).");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const name = rule.name || "rule";
|
|
111
|
+
const patterns = normalizePatterns(rule.patterns);
|
|
112
|
+
|
|
113
|
+
// include matchers (relatif à cwd, en posix)
|
|
114
|
+
const includeMatchers = patterns.map(globToRegExp);
|
|
115
|
+
|
|
116
|
+
// ignore matchers (relatif à cwd, en posix)
|
|
117
|
+
const ignoreMatchers = compileIgnoreMatchers([
|
|
118
|
+
...(opt.globalIgnored || []),
|
|
119
|
+
...(rule.ignored || []),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// baseDirs à watcher
|
|
123
|
+
const baseDirs = Array.from(new Set(patterns.map(globBaseDir).map((d) => d || ".")))
|
|
124
|
+
.map((d) => path.resolve(opt.cwd, d));
|
|
125
|
+
|
|
126
|
+
if (opt.debug) {
|
|
127
|
+
console.log(`\n[${name}] starting watcher`);
|
|
128
|
+
console.log(" cwd:", opt.cwd);
|
|
129
|
+
console.log(" patterns:", patterns);
|
|
130
|
+
console.log(" baseDirs:", baseDirs);
|
|
131
|
+
console.log(" ignored (applied in queue):", [...(opt.globalIgnored || []), ...(rule.ignored || [])]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pending = new Map();
|
|
135
|
+
let timer = null;
|
|
136
|
+
let running = false;
|
|
137
|
+
let rerun = false;
|
|
138
|
+
|
|
139
|
+
const flush = async () => {
|
|
140
|
+
if (running) { rerun = true; return; }
|
|
141
|
+
running = true;
|
|
142
|
+
try {
|
|
143
|
+
do {
|
|
144
|
+
rerun = false;
|
|
145
|
+
const batch = Array.from(pending.values());
|
|
146
|
+
pending.clear();
|
|
147
|
+
if (batch.length) await rule.callback(batch, { rule });
|
|
148
|
+
} while (rerun);
|
|
149
|
+
} finally {
|
|
150
|
+
running = false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const queue = (type, absPath) => {
|
|
155
|
+
const rel = toPosix(path.relative(opt.cwd, absPath));
|
|
156
|
+
|
|
157
|
+
// ignore d'abord
|
|
158
|
+
if (isIgnoredPosix(rel, ignoreMatchers)) {
|
|
159
|
+
// if (opt.debug) console.log(`[${name}] ignored`, type, rel);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// include ensuite
|
|
164
|
+
const ok = includeMatchers.some((rx) => rx.test(rel));
|
|
165
|
+
if (!ok) return;
|
|
166
|
+
|
|
167
|
+
if (opt.debug) console.log(`[${name}] queue`, type, rel);
|
|
168
|
+
|
|
169
|
+
pending.set(`${type}:${rel}`, { type, file: rel });
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
timer = setTimeout(flush, rule.debounceMs ?? 150);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// ⚠️ IMPORTANT: on ne met PAS "ignored" ici
|
|
175
|
+
const watcher = chokidar.watch(baseDirs, {
|
|
176
|
+
ignoreInitial: opt.ignoreInitial,
|
|
177
|
+
awaitWriteFinish: opt.awaitWriteFinish,
|
|
178
|
+
persistent: true,
|
|
179
|
+
usePolling: opt.usePolling,
|
|
180
|
+
interval: opt.interval,
|
|
181
|
+
binaryInterval: opt.binaryInterval,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
watcher.on("ready", () => {
|
|
185
|
+
console.log(`[${name}] ready`);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
watcher.on("add", (p) => queue("add", p));
|
|
189
|
+
watcher.on("change", (p) => queue("change", p));
|
|
190
|
+
watcher.on("unlink", (p) => queue("unlink", p));
|
|
191
|
+
watcher.on("error", (err) => console.error(`[${name}] watch error:`, err));
|
|
192
|
+
|
|
193
|
+
handles.push({
|
|
194
|
+
watcher,
|
|
195
|
+
stopTimers: () => { clearTimeout(timer); pending.clear(); }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
close: async () => {
|
|
201
|
+
for (const h of handles) h.stopTimers();
|
|
202
|
+
await Promise.all(handles.map((h) => h.watcher.close()));
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
const buildCSS = async (inputScss, outCssMin) => {
|
|
209
|
+
let compiled;
|
|
210
|
+
try {
|
|
211
|
+
compiled = sass.compile(inputScss, {
|
|
212
|
+
loadPaths: [path.resolve(__dirname, "../node_modules")],
|
|
213
|
+
style: "compressed",
|
|
214
|
+
sourceMap: false,
|
|
215
|
+
sourceMapIncludeSources: false,
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error("❌ Sass compile error:");
|
|
219
|
+
console.error(err?.formatted || err?.message || err);
|
|
220
|
+
}
|
|
221
|
+
const minified = csso.minify(compiled.css, { restructure: false });
|
|
222
|
+
fs.writeFileSync(outCssMin, minified.css);
|
|
223
|
+
console.log(`✅ CSS generated: ${outCssMin}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
const buildJS = async (entry, outfile) => {
|
|
228
|
+
try {
|
|
229
|
+
await esbuild.build({
|
|
230
|
+
entryPoints: [entry],
|
|
231
|
+
outfile,
|
|
232
|
+
bundle: true,
|
|
233
|
+
platform: "browser",
|
|
234
|
+
logLevel: "error",
|
|
235
|
+
treeShaking: true,
|
|
236
|
+
minify: true,
|
|
237
|
+
supported: { "template-literal": false },
|
|
238
|
+
target: ["es2020"],
|
|
239
|
+
legalComments: "none",
|
|
240
|
+
});
|
|
241
|
+
console.log(`✅ JS generated: ${outfile}`);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error("❌ esbuild build failed.");
|
|
244
|
+
if (err?.errors?.length) {
|
|
245
|
+
const formatted = await esbuild.formatMessages(err.errors, {
|
|
246
|
+
kind: "error",
|
|
247
|
+
color: true,
|
|
248
|
+
terminalWidth: process.stdout.columns || 80,
|
|
249
|
+
});
|
|
250
|
+
console.error(formatted.join("\n"));
|
|
251
|
+
} else {
|
|
252
|
+
console.error(err);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
module.exports = { createWatchers, buildCSS, buildJS };
|