tina4-nodejs 3.13.95 → 3.13.97

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.
Files changed (36) hide show
  1. package/CLAUDE.md +3 -4
  2. package/package.json +2 -1
  3. package/packages/cli/dist/bin.js +708 -1012
  4. package/packages/core/dist/index.js +588 -893
  5. package/packages/core/public/css/tina4.min.css +1 -1
  6. package/packages/core/src/index.ts +1 -3
  7. package/packages/core/src/messenger.ts +288 -96
  8. package/packages/core/src/queueBackends/kafkaBackend.ts +23 -2
  9. package/packages/core/src/queueBackends/rabbitmqBackend.ts +29 -17
  10. package/packages/core/src/request.ts +28 -7
  11. package/packages/core/src/server.ts +135 -7
  12. package/packages/core/src/session.ts +8 -1
  13. package/packages/orm/dist/index.js +639 -944
  14. package/packages/orm/src/autoCrud.ts +12 -10
  15. package/packages/orm/src/database.ts +62 -58
  16. package/packages/orm/src/databaseResult.ts +44 -73
  17. package/packages/orm/src/index.ts +0 -3
  18. package/packages/orm/src/migration.ts +26 -8
  19. package/packages/orm/src/model.ts +4 -0
  20. package/packages/orm/src/queryBuilder.ts +12 -5
  21. package/packages/orm/src/types.ts +7 -74
  22. package/packages/swagger/dist/index.js +78 -20
  23. package/packages/swagger/src/generator.ts +172 -29
  24. package/types/core/src/index.d.ts +1 -3
  25. package/types/core/src/messenger.d.ts +45 -4
  26. package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -0
  27. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +2 -1
  28. package/types/core/src/server.d.ts +0 -4
  29. package/types/core/src/session.d.ts +7 -0
  30. package/types/orm/src/database.d.ts +34 -30
  31. package/types/orm/src/databaseResult.d.ts +26 -36
  32. package/types/orm/src/index.d.ts +1 -2
  33. package/types/orm/src/migration.d.ts +4 -3
  34. package/types/orm/src/types.d.ts +7 -34
  35. package/packages/core/src/scss.ts +0 -623
  36. package/types/core/src/scss.d.ts +0 -19
@@ -1,623 +0,0 @@
1
- // Tina4 SCSS — Zero-dependency SCSS-to-CSS compiler (subset).
2
- // Supports variables, nesting, & parent selector, @import, @mixin/@include, comments, basic math.
3
-
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
5
- import { join, resolve, dirname, basename } from "node:path";
6
-
7
- // ── Types ────────────────────────────────────────────────────────
8
-
9
- export interface ScssConfig {
10
- importPaths?: string[];
11
- variables?: Record<string, string>;
12
- }
13
-
14
- // ── ScssCompiler ─────────────────────────────────────────────────
15
-
16
- export class ScssCompiler {
17
- private _importPaths: string[];
18
- private _variables: Record<string, string>;
19
-
20
- constructor(config?: ScssConfig) {
21
- this._importPaths = config?.importPaths ? [...config.importPaths] : [];
22
- this._variables = config?.variables ? { ...config.variables } : {};
23
- }
24
-
25
- /** Compile an SCSS string to CSS. */
26
- compile(source: string): string {
27
- return compileString(source, this._importPaths, { ...this._variables });
28
- }
29
-
30
- /** Compile an SCSS file to CSS. */
31
- compileFile(filePath: string): string {
32
- const absPath = resolve(filePath);
33
- const content = readFileSync(absPath, "utf-8");
34
- const paths = [dirname(absPath), ...this._importPaths];
35
- return compileString(content, paths, { ...this._variables });
36
- }
37
-
38
- /** Add a directory to the import resolution path. */
39
- addImportPath(path: string): void {
40
- this._importPaths.push(resolve(path));
41
- }
42
-
43
- /** Set or override an SCSS variable. */
44
- setVariable(name: string, value: string): void {
45
- // Strip leading $ if provided
46
- const key = name.startsWith("$") ? name.slice(1) : name;
47
- this._variables[key] = value;
48
- }
49
-
50
- /** Compile all .scss files in a directory into a single CSS output file. */
51
- compileScss(scssDir: string = "src/scss", output: string = "src/public/css/default.css", minify: boolean = false): string {
52
- const absDir = resolve(scssDir);
53
- if (!existsSync(absDir)) return "";
54
-
55
- // Collect non-partial .scss files, sorted
56
- const files = readdirSync(absDir)
57
- .filter((f) => f.endsWith(".scss") && !f.startsWith("_"))
58
- .sort()
59
- .map((f) => join(absDir, f));
60
-
61
- if (files.length === 0) return "";
62
-
63
- // Merge all files, resolving imports
64
- const paths = [absDir, ...this._importPaths];
65
- const imported = new Set<string>();
66
- let merged = "";
67
- for (const file of files) {
68
- const content = readFileSync(file, "utf-8");
69
- imported.add(file);
70
- merged += resolveImports(content, paths, imported) + "\n";
71
- }
72
-
73
- let css = compileString(merged, paths, { ...this._variables });
74
-
75
- if (minify) {
76
- css = css.replace(/\/\*.*?\*\//gs, "");
77
- css = css.replace(/\s+/g, " ");
78
- css = css.replace(/\s*([{}:;,])\s*/g, "$1");
79
- css = css.replace(/;}/g, "}");
80
- css = css.trim();
81
- }
82
-
83
- // Write output only if content changed (avoids triggering DevReload loops)
84
- const absOutput = resolve(output);
85
- const outDir = dirname(absOutput);
86
- if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
87
- let existing: string | null = null;
88
- try {
89
- existing = existsSync(absOutput) ? readFileSync(absOutput, "utf-8") : null;
90
- } catch {
91
- existing = null;
92
- }
93
- if (existing !== css) {
94
- writeFileSync(absOutput, css, "utf-8");
95
- }
96
-
97
- return css;
98
- }
99
- }
100
-
101
- // ── Internal Compilation Pipeline ────────────────────────────────
102
-
103
- function compileString(
104
- scss: string,
105
- importPaths: string[],
106
- variables: Record<string, string>
107
- ): string {
108
- // 1. Resolve @import statements
109
- const imported = new Set<string>();
110
- scss = resolveImports(scss, importPaths, imported);
111
-
112
- // 2. Strip single-line comments (preserve /* */ block comments)
113
- scss = scss.replace(/(?<![:"'])\/\/[^\n]*/g, "");
114
-
115
- // 3. Extract and store variables
116
- scss = extractVariables(scss, variables);
117
-
118
- // 4. Extract mixins
119
- const mixins: Record<string, { params: string[]; body: string }> = {};
120
- scss = extractMixins(scss, mixins);
121
-
122
- // 5. Resolve @include
123
- scss = resolveIncludes(scss, mixins);
124
-
125
- // 5.5. Resolve #{ ... } interpolation (before $var substitution + nesting).
126
- scss = resolveInterpolation(scss, variables);
127
-
128
- // 6. Substitute variables
129
- scss = substituteVariables(scss, variables);
130
-
131
- // 7. Evaluate basic math in property values
132
- scss = evalMath(scss);
133
-
134
- // 7.5. Resolve color functions (lighten/darken/rgba/rgb/mix) — after variable
135
- // substitution so a $colour arg is already a hex literal (issue #124).
136
- scss = resolveColorFunctions(scss);
137
-
138
- // 8. Flatten nested rules
139
- const css = flattenNesting(scss);
140
-
141
- // 9. Cleanup
142
- return cleanup(css);
143
- }
144
-
145
- // ── Import Resolution ────────────────────────────────────────────
146
-
147
- function resolveImports(content: string, paths: string[], imported: Set<string>): string {
148
- return content.replace(/@import\s+["']?([^"';\n]+)["']?\s*;/g, (_match, name: string) => {
149
- name = name.trim();
150
- const candidates: string[] = [];
151
- for (const base of paths) {
152
- candidates.push(
153
- join(base, `${name}.scss`),
154
- join(base, `_${name}.scss`),
155
- join(base, name),
156
- );
157
- }
158
- for (const candidate of candidates) {
159
- if (existsSync(candidate) && !imported.has(candidate)) {
160
- imported.add(candidate);
161
- const fileContent = readFileSync(candidate, "utf-8");
162
- return resolveImports(fileContent, [dirname(candidate), ...paths], imported);
163
- }
164
- }
165
- return `/* IMPORT NOT FOUND: ${name} */`;
166
- });
167
- }
168
-
169
- // ── Variables ────────────────────────────────────────────────────
170
-
171
- /**
172
- * Flags that may trail a variable declaration's value. `!default` means "assign
173
- * only if this variable is not already set" — the flag that makes a variable
174
- * themeable. `!global` is the scope flag. Both are compiler directives: they are
175
- * consumed at the declaration and must never reach the CSS, because
176
- * `padding: 1.5rem !default` is invalid CSS and browsers drop the whole
177
- * declaration. Sass flag names are case-SENSITIVE (`!DEFAULT` is an error in
178
- * Dart Sass), so the match is deliberately case-sensitive.
179
- */
180
- const VARIABLE_FLAG = /\s*!(default|global)\s*$/;
181
-
182
- /**
183
- * Split trailing `!default` / `!global` flags off a variable declaration value.
184
- * Returns `[valueWithoutFlags, declaresDefault]`.
185
- *
186
- * Only ever called on the value of a `$name: value;` declaration, so a literal
187
- * `!default` anywhere else — inside a quoted string (`content: "x !default y"`)
188
- * or a function argument — is left untouched, exactly as Dart Sass leaves it. A
189
- * blanket strip would corrupt real string content, and would silently turn
190
- * `rgba(#000 !default, 0.1)` (a syntax error in Dart Sass) into valid-looking
191
- * CSS that Sass would never emit.
192
- */
193
- function stripVariableFlags(value: string): [string, boolean] {
194
- let declaresDefault = false;
195
- for (;;) {
196
- const match = VARIABLE_FLAG.exec(value);
197
- if (match === null) return [value.trim(), declaresDefault];
198
- if (match[1] === "default") declaresDefault = true;
199
- value = value.slice(0, match.index);
200
- }
201
- }
202
-
203
- /**
204
- * Extract `$variable: value;` declarations, honouring the `!default` flag.
205
- *
206
- * `$x: value !default;` assigns only when `$x` is not already set. That is what
207
- * makes a variable themeable — a user who writes `$primary: red;` BEFORE
208
- * importing a partial that declares `$primary: blue !default;` keeps red.
209
- * Declarations are visited in source order, so "already set" means "set by an
210
- * earlier declaration or by a preset variable". A value of `null` counts as
211
- * unset, as in Sass.
212
- */
213
- function extractVariables(scss: string, variables: Record<string, string>): string {
214
- return scss.replace(/\$([a-zA-Z_][\w-]*)\s*:\s*([^;]+);/g, (_m, name: string, value: string) => {
215
- const [stripped, declaresDefault] = stripVariableFlags(value.trim());
216
- // !default must not overwrite a value that is already set.
217
- if (declaresDefault && (variables[name] ?? "null") !== "null") {
218
- return "";
219
- }
220
- let resolved = stripped;
221
- // Resolve variable references within the value
222
- for (const [vName, vVal] of Object.entries(variables)) {
223
- resolved = resolved.replaceAll(`$${vName}`, vVal);
224
- }
225
- variables[name] = resolved;
226
- return "";
227
- });
228
- }
229
-
230
- function substituteVariables(scss: string, variables: Record<string, string>): string {
231
- // Sort by longest name first to avoid partial matches
232
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
233
- for (const name of sorted) {
234
- scss = scss.replaceAll(`$${name}`, variables[name]);
235
- }
236
- return scss;
237
- }
238
-
239
- /**
240
- * Resolve SCSS `#{ ... }` interpolation. Each `#{ expr }` is replaced by its
241
- * resolved inner text: a `$variable` inside the braces resolves to its value,
242
- * anything else is inlined verbatim (trimmed). This lets a value carry a
243
- * variable inside a string context plain `$var` substitution can't reach —
244
- * e.g. `calc(100% - #{$gap})` → `calc(100% - 20px)` — and lets a variable
245
- * appear in a selector (`.icon-#{$name}` → `.icon-home`). Run BEFORE nested
246
- * rule flattening so the literal braces never confuse the block matcher.
247
- */
248
- function resolveInterpolation(scss: string, variables: Record<string, string>): string {
249
- const sorted = Object.keys(variables).sort((a, b) => b.length - a.length);
250
- return scss.replace(/#\{([^{}]*)\}/g, (_m, inner: string) => {
251
- let resolved = inner.trim();
252
- for (const name of sorted) {
253
- resolved = resolved.replaceAll(`$${name}`, variables[name]);
254
- }
255
- return resolved;
256
- });
257
- }
258
-
259
- // ── Mixins ───────────────────────────────────────────────────────
260
-
261
- function extractMixins(
262
- scss: string,
263
- mixins: Record<string, { params: string[]; body: string }>
264
- ): string {
265
- const pattern = /@mixin\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*\{/g;
266
- let match: RegExpExecArray | null;
267
- const locations: { start: number; end: number; name: string }[] = [];
268
-
269
- while ((match = pattern.exec(scss)) !== null) {
270
- const name = match[1];
271
- const paramsStr = match[2] ?? "";
272
- const params = paramsStr
273
- .split(",")
274
- .map((p) => p.trim().replace(/^\$/, ""))
275
- .filter(Boolean);
276
-
277
- const bodyStart = match.index + match[0].length;
278
- const body = findBlock(scss, bodyStart);
279
- if (body !== null) {
280
- mixins[name] = { params, body };
281
- locations.push({
282
- start: match.index,
283
- end: bodyStart + body.length + 1,
284
- name,
285
- });
286
- }
287
- }
288
-
289
- // Remove mixin definitions from source (reverse order to preserve indices)
290
- let result = scss;
291
- for (const loc of locations.reverse()) {
292
- result = result.slice(0, loc.start) + result.slice(loc.end);
293
- }
294
- return result;
295
- }
296
-
297
- function resolveIncludes(
298
- scss: string,
299
- mixins: Record<string, { params: string[]; body: string }>
300
- ): string {
301
- return scss.replace(
302
- /@include\s+([\w-]+)\s*(?:\(([^)]*)\))?\s*;/g,
303
- (_m, name: string, argsStr: string | undefined) => {
304
- if (!(name in mixins)) {
305
- return `/* MIXIN NOT FOUND: ${name} */`;
306
- }
307
- const mixin = mixins[name];
308
- const args = argsStr
309
- ? argsStr.split(",").map((a) => a.trim()).filter(Boolean)
310
- : [];
311
- let body = mixin.body;
312
- for (let i = 0; i < mixin.params.length; i++) {
313
- const paramName = mixin.params[i].split(":")[0].trim();
314
- const defaultVal = mixin.params[i].includes(":")
315
- ? mixin.params[i].split(":").slice(1).join(":").trim()
316
- : "";
317
- const value = i < args.length ? args[i] : defaultVal;
318
- body = body.replaceAll(`$${paramName}`, value);
319
- }
320
- return body;
321
- }
322
- );
323
- }
324
-
325
- // ── Math Evaluation ──────────────────────────────────────────────
326
-
327
- function evalMath(scss: string): string {
328
- // Mixed-unit arithmetic is left verbatim — that is exactly what CSS calc()
329
- // is for, and folding it silently produces invalid output (see tina4-nodejs#1).
330
- // Math inside calc(...) is preserved untouched on the same principle: the
331
- // author asked the browser to compute it.
332
- //
333
- // Rules for folding:
334
- // * Both operands unitless → fold
335
- // * Same unit on both operands → fold, keep unit
336
- // * One operand unitless for * or / → fold, keep the other unit
337
- // * Anything else (mixed units on +/-, etc) → leave verbatim
338
-
339
- // Step 1 — mask calc(...) ranges so the math regex cannot eat into them.
340
- const placeholders: string[] = [];
341
- const masked = scss.replace(/calc\([^()]*\)/g, (m) => {
342
- placeholders.push(m);
343
- return `\x00CALC${placeholders.length - 1}\x00`;
344
- });
345
-
346
- // Step 2 — run the math fold on what remains.
347
- const folded = masked.replace(
348
- /([\d.]+)([a-z%]*)\s*([+\-*/])\s*([\d.]+)([a-z%]*)/g,
349
- (full, n1: string, u1: string, op: string, n2: string, u2: string) => {
350
- const num1 = parseFloat(n1);
351
- const num2 = parseFloat(n2);
352
- if (Number.isNaN(num1) || Number.isNaN(num2)) return full;
353
-
354
- const unit1 = u1 || "";
355
- const unit2 = u2 || "";
356
-
357
- // Decide result unit; bail if units are incompatible.
358
- let unit: string;
359
- if (unit1 === unit2) {
360
- unit = unit1;
361
- } else if ((op === "*" || op === "/") && unit1 === "") {
362
- unit = unit2;
363
- } else if ((op === "*" || op === "/") && unit2 === "") {
364
- unit = unit1;
365
- } else {
366
- return full;
367
- }
368
-
369
- let result: number;
370
- switch (op) {
371
- case "+": result = num1 + num2; break;
372
- case "-": result = num1 - num2; break;
373
- case "*": result = num1 * num2; break;
374
- case "/":
375
- if (num2 === 0) return full;
376
- result = num1 / num2;
377
- break;
378
- default: return full;
379
- }
380
-
381
- if (result === Math.floor(result)) {
382
- return `${Math.floor(result)}${unit}`;
383
- }
384
- return `${result.toFixed(2)}${unit}`;
385
- }
386
- );
387
-
388
- // Step 3 — restore the calc() ranges verbatim.
389
- return folded.replace(/\x00CALC(\d+)\x00/g, (_m, idx: string) => {
390
- return placeholders[parseInt(idx, 10)];
391
- });
392
- }
393
-
394
- // ── Color Functions ──────────────────────────────────────────────
395
-
396
- /**
397
- * Resolve lighten(), darken(), rgba()/rgb(), and mix() color functions.
398
- *
399
- * rgba(<hex>, <alpha>) is the damaging case (issue #124): the functional rgba()
400
- * notation cannot take a hex, so rgba(#0f3460, 0.12) is invalid CSS and browsers
401
- * drop the whole declaration. Convert the hex to its r,g,b components. Runs after
402
- * variable substitution so a $colour arg is already a hex literal.
403
- */
404
- function resolveColorFunctions(scss: string): string {
405
- scss = scss.replace(/lighten\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g, (_m, color: string, amt: string) =>
406
- adjustLightness(color.trim(), parseFloat(amt.trim().replace(/%$/, "")) / 100)
407
- );
408
- scss = scss.replace(/darken\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/g, (_m, color: string, amt: string) =>
409
- adjustLightness(color.trim(), -(parseFloat(amt.trim().replace(/%$/, "")) / 100))
410
- );
411
- // rgba(<hex>, <alpha>) — only the two-arg hex form; leave rgba(r,g,b,a) alone.
412
- scss = scss.replace(/rgba\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*([\d.]+)\s*\)/g, (whole, hex: string, alpha: string) => {
413
- const rgb = hexToRgb(hex);
414
- return rgb === null ? whole : `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha.trim()})`;
415
- });
416
- scss = scss.replace(/rgb\(\s*(#[0-9a-fA-F]{3,8})\s*\)/g, (whole, hex: string) => {
417
- const rgb = hexToRgb(hex);
418
- return rgb === null ? whole : `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
419
- });
420
- // mix(<c1>, <c2>[, <weight>]) — Sass weight is c1's proportion (default 50%).
421
- scss = scss.replace(
422
- /mix\(\s*(#[0-9a-fA-F]{3,8})\s*,\s*(#[0-9a-fA-F]{3,8})\s*(?:,\s*([\d.]+%?)\s*)?\)/g,
423
- (whole, h1: string, h2: string, weight?: string) => {
424
- const c1 = hexToRgb(h1);
425
- const c2 = hexToRgb(h2);
426
- if (c1 === null || c2 === null) return whole;
427
- const w = weight ? parseFloat(weight.replace(/%$/, "")) / 100 : 0.5;
428
- const mixed = [0, 1, 2].map((i) => Math.round(c1[i] * w + c2[i] * (1 - w)));
429
- return `#${mixed.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
430
- }
431
- );
432
- return scss;
433
- }
434
-
435
- /** Parse a #rgb / #rrggbb hex string into an [r, g, b] tuple, or null. */
436
- function hexToRgb(color: string): [number, number, number] | null {
437
- let c = color.trim().replace(/^#/, "");
438
- if (c.length === 3) {
439
- c = c.split("").map((ch) => ch + ch).join("");
440
- }
441
- if (!/^[0-9a-fA-F]{6}$/.test(c)) return null;
442
- return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
443
- }
444
-
445
- /** Adjust the HSL lightness of a hex color by `amount` (-1..1), return hex.
446
- * Truncates (Math.trunc) to match the Python master's int(x*255) byte-for-byte. */
447
- function adjustLightness(color: string, amount: number): string {
448
- const rgb = hexToRgb(color);
449
- if (rgb === null) return color;
450
- let [r, g, b] = rgb.map((v) => v / 255) as [number, number, number];
451
- const max = Math.max(r, g, b);
452
- const min = Math.min(r, g, b);
453
- let l = (max + min) / 2;
454
- const d = max - min;
455
- let h = 0;
456
- let s = 0;
457
- if (d !== 0) {
458
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
459
- if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
460
- else if (max === g) h = (b - r) / d + 2;
461
- else h = (r - g) / d + 4;
462
- h /= 6;
463
- }
464
- l = Math.max(0, Math.min(1, l + amount));
465
- if (s === 0) {
466
- r = g = b = l;
467
- } else {
468
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
469
- const p = 2 * l - q;
470
- r = hueToRgb(p, q, h + 1 / 3);
471
- g = hueToRgb(p, q, h);
472
- b = hueToRgb(p, q, h - 1 / 3);
473
- }
474
- const hex = (v: number): string => Math.trunc(v * 255).toString(16).padStart(2, "0");
475
- return `#${hex(r)}${hex(g)}${hex(b)}`;
476
- }
477
-
478
- function hueToRgb(p: number, q: number, t: number): number {
479
- if (t < 0) t += 1;
480
- if (t > 1) t -= 1;
481
- if (t < 1 / 6) return p + (q - p) * 6 * t;
482
- if (t < 1 / 2) return q;
483
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
484
- return p;
485
- }
486
-
487
- // ── Nesting Flattener ────────────────────────────────────────────
488
-
489
- function flattenNesting(scss: string): string {
490
- const output: string[] = [];
491
- flattenBlock(scss, [], output);
492
- return output.join("\n");
493
- }
494
-
495
- function flattenBlock(content: string, parentSelectors: string[], output: string[]): void {
496
- let pos = 0;
497
- const properties: string[] = [];
498
-
499
- while (pos < content.length) {
500
- // Skip whitespace
501
- while (pos < content.length && /[\s]/.test(content[pos])) {
502
- pos++;
503
- }
504
- if (pos >= content.length) break;
505
-
506
- // Block comment — preserve
507
- if (content[pos] === "/" && content[pos + 1] === "*") {
508
- const end = content.indexOf("*/", pos + 2);
509
- if (end === -1) break;
510
- output.push(content.slice(pos, end + 2));
511
- pos = end + 2;
512
- continue;
513
- }
514
-
515
- // @media query — special handling
516
- if (content.slice(pos, pos + 6) === "@media") {
517
- const brace = content.indexOf("{", pos);
518
- if (brace === -1) break;
519
- const mediaQuery = content.slice(pos, brace).trim();
520
- const body = findBlock(content, brace + 1);
521
- if (body === null) break;
522
- pos = brace + 1 + body.length + 1;
523
-
524
- const innerOutput: string[] = [];
525
- flattenBlock(body, parentSelectors, innerOutput);
526
- if (innerOutput.length > 0) {
527
- output.push(`${mediaQuery} {`);
528
- for (const line of innerOutput) {
529
- output.push(` ${line}`);
530
- }
531
- output.push("}");
532
- }
533
- continue;
534
- }
535
-
536
- // Find next { or ;
537
- const bracePos = content.indexOf("{", pos);
538
- const semiPos = content.indexOf(";", pos);
539
-
540
- // Property (has ; before { or no { at all)
541
- if (semiPos !== -1 && (bracePos === -1 || semiPos < bracePos)) {
542
- const prop = content.slice(pos, semiPos).trim();
543
- if (prop && !prop.startsWith("@")) {
544
- properties.push(prop);
545
- }
546
- pos = semiPos + 1;
547
- continue;
548
- }
549
-
550
- // Nested block
551
- if (bracePos !== -1) {
552
- const selectorText = content.slice(pos, bracePos).trim();
553
- const body = findBlock(content, bracePos + 1);
554
- if (body === null) break;
555
- pos = bracePos + 1 + body.length + 1;
556
-
557
- if (!selectorText) continue;
558
-
559
- // Expand selectors with parent reference (&)
560
- const selectors = selectorText.split(",").map((s) => s.trim());
561
- const newSelectors: string[] = [];
562
- for (const sel of selectors) {
563
- if (parentSelectors.length > 0) {
564
- for (const parent of parentSelectors) {
565
- if (sel.includes("&")) {
566
- newSelectors.push(sel.replace(/&/g, parent));
567
- } else {
568
- newSelectors.push(`${parent} ${sel}`);
569
- }
570
- }
571
- } else {
572
- newSelectors.push(sel);
573
- }
574
- }
575
-
576
- flattenBlock(body, newSelectors, output);
577
- continue;
578
- }
579
-
580
- // Remaining text — treat as property
581
- const remaining = content.slice(pos).trim();
582
- if (remaining) {
583
- properties.push(remaining);
584
- }
585
- break;
586
- }
587
-
588
- // Emit properties for current selector
589
- if (properties.length > 0 && parentSelectors.length > 0) {
590
- const selectorStr = parentSelectors.join(", ");
591
- output.push(`${selectorStr} {`);
592
- for (const prop of properties) {
593
- output.push(` ${prop};`);
594
- }
595
- output.push("}");
596
- }
597
- }
598
-
599
- // ── Utilities ────────────────────────────────────────────────────
600
-
601
- function findBlock(content: string, start: number): string | null {
602
- let depth = 1;
603
- let pos = start;
604
- while (pos < content.length && depth > 0) {
605
- if (content[pos] === "{") depth++;
606
- else if (content[pos] === "}") depth--;
607
- if (depth > 0) pos++;
608
- }
609
- return depth === 0 ? content.slice(start, pos) : null;
610
- }
611
-
612
- function cleanup(css: string): string {
613
- // Remove empty rulesets
614
- css = css.replace(/[^{}]+\{\s*\}/g, "");
615
- // Remove multiple blank lines
616
- css = css.replace(/\n{3,}/g, "\n\n");
617
- // Remove trailing whitespace per line
618
- css = css
619
- .split("\n")
620
- .map((line) => line.trimEnd())
621
- .join("\n");
622
- return css.trim() + "\n";
623
- }
@@ -1,19 +0,0 @@
1
- export interface ScssConfig {
2
- importPaths?: string[];
3
- variables?: Record<string, string>;
4
- }
5
- export declare class ScssCompiler {
6
- private _importPaths;
7
- private _variables;
8
- constructor(config?: ScssConfig);
9
- /** Compile an SCSS string to CSS. */
10
- compile(source: string): string;
11
- /** Compile an SCSS file to CSS. */
12
- compileFile(filePath: string): string;
13
- /** Add a directory to the import resolution path. */
14
- addImportPath(path: string): void;
15
- /** Set or override an SCSS variable. */
16
- setVariable(name: string, value: string): void;
17
- /** Compile all .scss files in a directory into a single CSS output file. */
18
- compileScss(scssDir?: string, output?: string, minify?: boolean): string;
19
- }