elcrm 1.1.5 → 1.1.7

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Vite plugin: короткие CSS-переменные и классы после сборки.
3
+ * @see minifyCssVars
4
+ */
5
+ export type MinifyCssVarsOptions = {
6
+ /** Длина короткого `--xx` (`2` → `--ab`). По умолчанию 3 */
7
+ var?: number;
8
+ /** Длина суффикса класса после prefix (`3` + prefix `x` → `xabc`) */
9
+ name?: number;
10
+ /** Общий стартовый индекс кодировки */
11
+ start?: number;
12
+ varStart?: number;
13
+ nameStart?: number;
14
+ vars?: boolean;
15
+ classes?: boolean;
16
+ minVar?: number;
17
+ minClass?: number;
18
+ classPrefix?: string;
19
+ report?: boolean;
20
+ js?: boolean;
21
+ css?: boolean;
22
+ };
23
+
24
+ export declare function toIndex(value: unknown, fallback?: number): number;
25
+
26
+ export declare function encodeName(index: number, length: number): string;
27
+
28
+ export declare function namespaceSize(length: number): number;
29
+
30
+ /**
31
+ * Короткие имена без коллизий: пропускает `reserved`, не выдаёт одно `to` дважды.
32
+ */
33
+ export declare function allocateUnique(
34
+ originals: string[],
35
+ reserved: Set<string>,
36
+ make: (index: number) => string,
37
+ start: number,
38
+ capacity: number,
39
+ kind: string,
40
+ ): Map<string, string>;
41
+
42
+ export declare function minifyCssVars(
43
+ options?: MinifyCssVarsOptions,
44
+ ): import("vite").Plugin;
@@ -0,0 +1,280 @@
1
+ // src/vite/minify-css-vars.ts
2
+ var VAR_RE = /--[a-zA-Z_][\w-]*/g;
3
+ var VAR_IN_JS_RE = /var\(\s*(--[a-zA-Z_][\w-]*)|["'](--[a-zA-Z_][\w-]*)["']/g;
4
+ var CLASS_SEL_RE = /\.(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)/g;
5
+ var LETTERS = "abcdefghijklmnopqrstuvwxyz";
6
+ function collect(texts, re) {
7
+ const set = new Set;
8
+ for (const text of texts) {
9
+ if (!text)
10
+ continue;
11
+ re.lastIndex = 0;
12
+ let m;
13
+ while (m = re.exec(text))
14
+ set.add(m[1] ?? m[0]);
15
+ }
16
+ return set;
17
+ }
18
+ function collectJsVars(texts) {
19
+ const set = new Set;
20
+ for (const text of texts) {
21
+ if (!text)
22
+ continue;
23
+ VAR_IN_JS_RE.lastIndex = 0;
24
+ let m;
25
+ while (m = VAR_IN_JS_RE.exec(text)) {
26
+ const name = m[1] ?? m[2];
27
+ if (name)
28
+ set.add(name);
29
+ }
30
+ }
31
+ return set;
32
+ }
33
+ function toIndex(value, fallback = 0) {
34
+ const n = typeof value === "number" ? value : Number(value);
35
+ if (!Number.isFinite(n) || n < 0)
36
+ return fallback;
37
+ return Math.floor(n);
38
+ }
39
+ function encodeName(index, length) {
40
+ const n = LETTERS.length;
41
+ const len = Math.max(1, Math.min(8, Math.floor(length)));
42
+ let i = toIndex(index, 0);
43
+ let s = "";
44
+ for (let k = 0;k < len; k++) {
45
+ s = LETTERS[i % n] + s;
46
+ i = Math.floor(i / n);
47
+ }
48
+ return s;
49
+ }
50
+ function byLongest(names) {
51
+ return [...names].sort((a, b) => b.length - a.length);
52
+ }
53
+ function alreadyShortVar(name, len) {
54
+ return new RegExp(`^--[a-z]{${len}}$`).test(name);
55
+ }
56
+ function shouldMinifyClass(name, minLen, prefix) {
57
+ if (name.length < minLen)
58
+ return false;
59
+ const esc = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60
+ if (new RegExp(`^${esc}[a-z0-9]+$`).test(name))
61
+ return false;
62
+ return /[-_]/.test(name);
63
+ }
64
+ function namespaceSize(length) {
65
+ const len = Math.max(1, Math.min(8, Math.floor(length)));
66
+ let n = 1;
67
+ for (let i = 0;i < len; i++)
68
+ n *= LETTERS.length;
69
+ return n;
70
+ }
71
+ function allocateUnique(originals, reserved, make, start, capacity, kind) {
72
+ const taken = new Set(reserved);
73
+ const out = new Map;
74
+ let cursor = start;
75
+ for (const from of originals) {
76
+ if (out.has(from))
77
+ continue;
78
+ let found;
79
+ for (let n = 0;n < capacity; n++) {
80
+ const candidate = make(cursor);
81
+ cursor++;
82
+ if (taken.has(candidate) || candidate === from)
83
+ continue;
84
+ found = candidate;
85
+ break;
86
+ }
87
+ if (!found) {
88
+ throw new Error(`[minify-css-vars] нет свободных имён для ${kind}: нужно ${originals.length}, ёмкость ${capacity}, занято ${taken.size}. Увеличьте var/name или сдвиньте start.`);
89
+ }
90
+ out.set(from, found);
91
+ taken.add(found);
92
+ }
93
+ const used = new Set;
94
+ for (const [from, to] of out) {
95
+ if (used.has(to)) {
96
+ throw new Error(`[minify-css-vars] коллизия ${kind}: «${from}» и ещё одно имя → «${to}»`);
97
+ }
98
+ used.add(to);
99
+ if (reserved.has(to)) {
100
+ throw new Error(`[minify-css-vars] «${from}» → «${to}» затирает существующее имя ${kind}`);
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ function replaceVarsInCss(text, map) {
106
+ let out = text;
107
+ for (const [from, to] of map)
108
+ out = out.split(from).join(to);
109
+ return out;
110
+ }
111
+ function replaceVarsInJs(text, map) {
112
+ let out = text;
113
+ for (const [from, to] of map) {
114
+ const esc = from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
115
+ out = out.replace(new RegExp(`var\\(\\s*${esc}\\b`, "g"), (m) => m.replace(from, to));
116
+ out = out.replace(new RegExp(`(["'])${esc}\\1`, "g"), `$1${to}$1`);
117
+ }
118
+ return out;
119
+ }
120
+ function replaceClasses(text, map, inCss) {
121
+ let out = text;
122
+ for (const [from, to] of map) {
123
+ if (inCss) {
124
+ out = out.split(`.${from}`).join(`.${to}`);
125
+ continue;
126
+ }
127
+ const re = new RegExp(`(^|[^\\w-])(${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(?=[^\\w-]|$)`, "g");
128
+ out = out.replace(re, `$1${to}`);
129
+ }
130
+ return out;
131
+ }
132
+ function bytes(s) {
133
+ return Buffer.byteLength(s, "utf8");
134
+ }
135
+ function fmtKb(n) {
136
+ if (n < 1024)
137
+ return `${n} B`;
138
+ return `${(n / 1024).toFixed(2)} kB`;
139
+ }
140
+ function pct(before, after) {
141
+ if (!before)
142
+ return "0%";
143
+ const p = (before - after) / before * 100;
144
+ const sign = p > 0 ? "−" : p < 0 ? "+" : "";
145
+ return `${sign}${Math.abs(p).toFixed(1)}%`;
146
+ }
147
+ function normalizeOpts(raw) {
148
+ const o = raw ?? {};
149
+ const varLen = o.var ?? 3;
150
+ const nameLen = o.name ?? 2;
151
+ const start = toIndex(o.start, 0);
152
+ return {
153
+ varLen,
154
+ nameLen,
155
+ varStart: toIndex(o.varStart, start),
156
+ nameStart: toIndex(o.nameStart, start),
157
+ vars: o.vars !== false,
158
+ classes: o.classes !== false,
159
+ minVar: o.minVar ?? Math.max(4, varLen + 2),
160
+ minClass: o.minClass ?? 5,
161
+ classPrefix: o.classPrefix ?? "x",
162
+ report: o.report !== false,
163
+ js: o.js !== false,
164
+ css: o.css !== false
165
+ };
166
+ }
167
+ function minifyCssVars(options) {
168
+ const opt = normalizeOpts(options);
169
+ return {
170
+ name: "elcrm-minify-css-vars",
171
+ apply: "build",
172
+ enforce: "post",
173
+ generateBundle(_opts, bundle) {
174
+ const files = [];
175
+ for (const [fileName, item] of Object.entries(bundle)) {
176
+ if (item.type !== "asset" && item.type !== "chunk")
177
+ continue;
178
+ const isCss = opt.css && fileName.endsWith(".css");
179
+ const isJs = opt.js && fileName.endsWith(".js");
180
+ if (!isCss && !isJs)
181
+ continue;
182
+ const raw = item.type === "chunk" ? item.code : typeof item.source === "string" ? item.source : item.source ? Buffer.from(item.source).toString("utf8") : "";
183
+ files.push({ item, fileName, raw, isCss, isJs });
184
+ }
185
+ const cssTexts = files.filter((f) => f.isCss).map((f) => f.raw);
186
+ const jsTexts = files.filter((f) => f.isJs).map((f) => f.raw);
187
+ const varMap = new Map;
188
+ if (opt.vars) {
189
+ const names = new Set([
190
+ ...collect(cssTexts, VAR_RE),
191
+ ...collectJsVars(jsTexts)
192
+ ]);
193
+ const remap = [];
194
+ const reserved = new Set;
195
+ for (const name of names) {
196
+ if (alreadyShortVar(name, opt.varLen) || /^--[a-zA-Z_]$/.test(name) || name.length < opt.minVar) {
197
+ reserved.add(name);
198
+ continue;
199
+ }
200
+ remap.push(name);
201
+ }
202
+ const cap = namespaceSize(opt.varLen);
203
+ const allocated = allocateUnique(byLongest(remap), reserved, (i) => `--${encodeName(i, opt.varLen)}`, opt.varStart, cap, "vars");
204
+ for (const [k, v] of allocated)
205
+ varMap.set(k, v);
206
+ }
207
+ const classMap = new Map;
208
+ if (opt.classes) {
209
+ const allClasses = new Set;
210
+ for (const file of files) {
211
+ if (!file.isCss)
212
+ continue;
213
+ CLASS_SEL_RE.lastIndex = 0;
214
+ let m;
215
+ while (m = CLASS_SEL_RE.exec(file.raw))
216
+ allClasses.add(m[1]);
217
+ }
218
+ const remap = [];
219
+ const reserved = new Set;
220
+ for (const name of allClasses) {
221
+ if (shouldMinifyClass(name, opt.minClass, opt.classPrefix)) {
222
+ remap.push(name);
223
+ } else {
224
+ reserved.add(name);
225
+ }
226
+ }
227
+ const cap = namespaceSize(opt.nameLen);
228
+ const allocated = allocateUnique(byLongest(remap), reserved, (i) => `${opt.classPrefix}${encodeName(i, opt.nameLen)}`, opt.nameStart, cap, "classes");
229
+ for (const [k, v] of allocated)
230
+ classMap.set(k, v);
231
+ }
232
+ if (!varMap.size && !classMap.size) {
233
+ if (opt.report) {
234
+ console.log("[minify-css-vars] нечего сокращать");
235
+ }
236
+ return;
237
+ }
238
+ const varEntries = [...varMap.entries()];
239
+ let beforeAll = 0;
240
+ let afterAll = 0;
241
+ const rows = [];
242
+ for (const file of files) {
243
+ const before = bytes(file.raw);
244
+ let next = file.raw;
245
+ if (varMap.size) {
246
+ next = file.isCss ? replaceVarsInCss(next, varEntries) : replaceVarsInJs(next, varEntries);
247
+ }
248
+ if (classMap.size) {
249
+ next = replaceClasses(next, classMap, file.isCss);
250
+ }
251
+ const after = bytes(next);
252
+ beforeAll += before;
253
+ afterAll += after;
254
+ if (next !== file.raw) {
255
+ if (file.item.type === "chunk") {
256
+ file.item.code = next;
257
+ } else {
258
+ file.item.source = next;
259
+ }
260
+ }
261
+ if (opt.report && (next !== file.raw || before !== after)) {
262
+ rows.push(` ${file.fileName} ${fmtKb(before)} → ${fmtKb(after)} (${pct(before, after)})`);
263
+ }
264
+ }
265
+ if (opt.report) {
266
+ console.log(`[minify-css-vars] vars: ${varMap.size} (пример --${encodeName(opt.varStart, opt.varLen)}, start=${opt.varStart}) classes: ${classMap.size} (пример ${opt.classPrefix}${encodeName(opt.nameStart, opt.nameLen)}, start=${opt.nameStart})`);
267
+ for (const row of rows)
268
+ console.log(row);
269
+ console.log(`[minify-css-vars] итого ${fmtKb(beforeAll)} → ${fmtKb(afterAll)} (${pct(beforeAll, afterAll)})`);
270
+ }
271
+ }
272
+ };
273
+ }
274
+ export {
275
+ toIndex,
276
+ namespaceSize,
277
+ minifyCssVars,
278
+ encodeName,
279
+ allocateUnique
280
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elcrm",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "CLI @elcrm/*: update --fix --test, css (prune imports), docs, cursor, migrate, audit",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,6 +33,11 @@
33
33
  "types": "./dist/vite/postbuild.d.ts",
34
34
  "import": "./dist/vite/postbuild.js",
35
35
  "default": "./dist/vite/postbuild.js"
36
+ },
37
+ "./vite/minify-css-vars": {
38
+ "types": "./dist/vite/minify-css-vars.d.ts",
39
+ "import": "./dist/vite/minify-css-vars.js",
40
+ "default": "./dist/vite/minify-css-vars.js"
36
41
  }
37
42
  },
38
43
  "files": [
@@ -44,10 +49,10 @@
44
49
  "node": ">=18"
45
50
  },
46
51
  "scripts": {
47
- "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm && bun build ./src/vite/plugin-css-scoped.ts --outfile ./dist/vite/plugin-css-scoped.js --target node --format esm --packages external && bun build ./src/vite/discover-lib.ts --outfile ./dist/vite/discover-lib.js --target node --format esm --packages external && bun build ./src/vite/concat-css.ts --outfile ./dist/vite/concat-css.js --target node --format esm --packages external && bun build ./src/vite/check-tokens.ts --outfile ./dist/vite/check-tokens.js --target node --format esm --packages external && bun build ./src/vite/postbuild.ts --outfile ./dist/vite/postbuild.js --target node --format esm --packages external && node ./scripts/chmod-bin.mjs && node ./scripts/copy-vite-dts.mjs",
52
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm && bun build ./src/vite/plugin-css-scoped.ts --outfile ./dist/vite/plugin-css-scoped.js --target node --format esm --packages external && bun build ./src/vite/discover-lib.ts --outfile ./dist/vite/discover-lib.js --target node --format esm --packages external && bun build ./src/vite/concat-css.ts --outfile ./dist/vite/concat-css.js --target node --format esm --packages external && bun build ./src/vite/check-tokens.ts --outfile ./dist/vite/check-tokens.js --target node --format esm --packages external && bun build ./src/vite/postbuild.ts --outfile ./dist/vite/postbuild.js --target node --format esm --packages external && bun build ./src/vite/minify-css-vars.ts --outfile ./dist/vite/minify-css-vars.js --target node --format esm --packages external && node ./scripts/chmod-bin.mjs && node ./scripts/copy-vite-dts.mjs",
48
53
  "dev": "bun run ./src/index.ts",
49
54
  "elcrm": "bun run ./src/index.ts",
50
- "test": "bun test src/lib/migrate",
55
+ "test": "bun test src/lib/migrate src/vite",
51
56
  "link:local": "bun run build && bun link",
52
57
  "prepublishOnly": "bun run test && bun run build",
53
58
  "publish:npm": "npm publish"
@@ -90,6 +90,23 @@ elcrm test # статический анализ + scripts п
90
90
  elcrm test --front|--server|--no-scripts
91
91
  ```
92
92
 
93
+ ### Vite-плагины из пакета `elcrm`
94
+
95
+ Нужен **elcrm ≥ 1.1.6** в `devDependencies` приложения (не только глобальный `bun i -g`). Иначе Vite: `Missing "./vite/minify-css-vars" specifier`.
96
+
97
+ ```js
98
+ import { minifyCssVars } from "elcrm/vite/minify-css-vars";
99
+
100
+ // только command === "build"
101
+ minifyCssVars({ var: 2, name: 3, classPrefix: "x" })
102
+ ```
103
+
104
+ `nameStart` / `varStart` — **числа** (индекс алфавита), не `"x"`. Префикс класса — `classPrefix`. В JS плагин не трогает декремент `--n` (иначе `aqt is not defined`).
105
+
106
+ Короткие имена **уникальны**: занятые `--aa` / `xaa` пропускаются, одно `to` не выдаётся дважды; если слотов нет — сборка падает, а не молча затирает токены.
107
+
108
+ Также: `elcrm/vite/plugin-css-scoped`, `discover-lib`, `concat-css`, `check-tokens`, `postbuild`.
109
+
93
110
  ### Прочее
94
111
 
95
112
  ```bash