@loworbitstudio/visor 0.6.0 → 0.9.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/README.md +99 -0
- package/dist/CHANGELOG.json +37 -1
- package/dist/index.js +1478 -296
- package/dist/registry.json +272 -44
- package/dist/visor-manifest.json +905 -11
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { Command as Command2 } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/check.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
+
import { statSync as statSync3 } from "fs";
|
|
9
|
+
import { resolve as resolve3, dirname as dirname2 } from "path";
|
|
8
10
|
|
|
9
11
|
// src/registry/resolve.ts
|
|
10
12
|
import { readFileSync } from "fs";
|
|
@@ -310,7 +312,7 @@ function collectJsxFindings(source, filePath, parse) {
|
|
|
310
312
|
}
|
|
311
313
|
const mapping = NATIVE_TO_VISOR[tagName];
|
|
312
314
|
if (!mapping) return;
|
|
313
|
-
const
|
|
315
|
+
const finding2 = {
|
|
314
316
|
file: filePath,
|
|
315
317
|
line,
|
|
316
318
|
column,
|
|
@@ -318,8 +320,8 @@ function collectJsxFindings(source, filePath, parse) {
|
|
|
318
320
|
suggestedPrimitive: mapping.displayName,
|
|
319
321
|
installCmd: `npx visor add ${mapping.visorName}`
|
|
320
322
|
};
|
|
321
|
-
if (mapping.notes)
|
|
322
|
-
findings.push(
|
|
323
|
+
if (mapping.notes) finding2.rationale = mapping.notes;
|
|
324
|
+
findings.push(finding2);
|
|
323
325
|
});
|
|
324
326
|
return findings;
|
|
325
327
|
}
|
|
@@ -377,6 +379,491 @@ async function scanJsx(pathArg) {
|
|
|
377
379
|
};
|
|
378
380
|
}
|
|
379
381
|
|
|
382
|
+
// src/check/design.ts
|
|
383
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2, existsSync } from "fs";
|
|
384
|
+
import { resolve as resolve2, extname as extname2, join as join3, basename } from "path";
|
|
385
|
+
function loadVisorRc(dir) {
|
|
386
|
+
const rcPath = join3(dir, ".visorrc.json");
|
|
387
|
+
if (!existsSync(rcPath)) return {};
|
|
388
|
+
try {
|
|
389
|
+
const raw = readFileSync3(rcPath, "utf-8");
|
|
390
|
+
return JSON.parse(raw);
|
|
391
|
+
} catch {
|
|
392
|
+
return {};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
var CODE_EXTS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js"]);
|
|
396
|
+
var STYLE_EXTS = /* @__PURE__ */ new Set([".css", ".module.css"]);
|
|
397
|
+
var ALL_EXTS = /* @__PURE__ */ new Set([...CODE_EXTS, ...STYLE_EXTS]);
|
|
398
|
+
function collectFiles2(pathArg) {
|
|
399
|
+
const abs = resolve2(pathArg);
|
|
400
|
+
try {
|
|
401
|
+
const s = statSync2(abs);
|
|
402
|
+
if (s.isDirectory()) {
|
|
403
|
+
let recurse2 = function(dir) {
|
|
404
|
+
for (const entry of readdirSync2(dir)) {
|
|
405
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "dist") continue;
|
|
406
|
+
const full = join3(dir, entry);
|
|
407
|
+
const es = statSync2(full);
|
|
408
|
+
if (es.isDirectory()) recurse2(full);
|
|
409
|
+
else if (ALL_EXTS.has(extname2(full))) files.push(full);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
var recurse = recurse2;
|
|
413
|
+
const files = [];
|
|
414
|
+
recurse2(abs);
|
|
415
|
+
return files;
|
|
416
|
+
}
|
|
417
|
+
if (ALL_EXTS.has(extname2(abs))) return [abs];
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
function lines(source) {
|
|
423
|
+
return source.split("\n");
|
|
424
|
+
}
|
|
425
|
+
function finding(file, line, rule, severity, message, fix) {
|
|
426
|
+
return { file, line, rule, severity, message, ...fix ? { fix } : {} };
|
|
427
|
+
}
|
|
428
|
+
var TIER1_PREFIXES = [
|
|
429
|
+
"--primitive-",
|
|
430
|
+
"--raw-",
|
|
431
|
+
"--base-color-",
|
|
432
|
+
"--palette-"
|
|
433
|
+
];
|
|
434
|
+
function ruleTier1TokenDirectUsage(source, filePath) {
|
|
435
|
+
const found = [];
|
|
436
|
+
const ext = extname2(filePath);
|
|
437
|
+
if (STYLE_EXTS.has(ext)) return found;
|
|
438
|
+
const src = lines(source);
|
|
439
|
+
for (let i = 0; i < src.length; i++) {
|
|
440
|
+
const l = src[i];
|
|
441
|
+
for (const prefix of TIER1_PREFIXES) {
|
|
442
|
+
if (l.includes(prefix)) {
|
|
443
|
+
found.push(finding(
|
|
444
|
+
filePath,
|
|
445
|
+
i + 1,
|
|
446
|
+
"tier-1-token-direct-usage",
|
|
447
|
+
"error",
|
|
448
|
+
`Direct use of Tier-1 primitive token "${prefix}..." detected. Use a Tier-2 semantic token instead.`,
|
|
449
|
+
"Replace with the equivalent semantic token from the Borealis token registry."
|
|
450
|
+
));
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return found;
|
|
456
|
+
}
|
|
457
|
+
function ruleHardcodedHex(source, filePath) {
|
|
458
|
+
const found = [];
|
|
459
|
+
const HEX_RE = /#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6,8})\b/g;
|
|
460
|
+
const src = lines(source);
|
|
461
|
+
for (let i = 0; i < src.length; i++) {
|
|
462
|
+
const l = src[i];
|
|
463
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*") || l.trim().startsWith("/*")) continue;
|
|
464
|
+
if (basename(filePath).startsWith(".")) continue;
|
|
465
|
+
let m;
|
|
466
|
+
HEX_RE.lastIndex = 0;
|
|
467
|
+
while ((m = HEX_RE.exec(l)) !== null) {
|
|
468
|
+
found.push(finding(
|
|
469
|
+
filePath,
|
|
470
|
+
i + 1,
|
|
471
|
+
"hardcoded-hex",
|
|
472
|
+
"error",
|
|
473
|
+
`Hardcoded hex color "${m[0]}" bypasses the Borealis token system.`,
|
|
474
|
+
"Replace with the appropriate semantic token: var(--color-surface), var(--color-text-primary), etc."
|
|
475
|
+
));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return found;
|
|
479
|
+
}
|
|
480
|
+
var PX_WHITELIST = /* @__PURE__ */ new Set(["0px", "1px", "2px", "3px"]);
|
|
481
|
+
function ruleHardcodedPx(source, filePath) {
|
|
482
|
+
const found = [];
|
|
483
|
+
const PX_RE = /\b(\d+(?:\.\d+)?)px\b/g;
|
|
484
|
+
const src = lines(source);
|
|
485
|
+
for (let i = 0; i < src.length; i++) {
|
|
486
|
+
const l = src[i];
|
|
487
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*") || l.trim().startsWith("/*")) continue;
|
|
488
|
+
if (!/margin|padding|width|height|gap|top:|left:|right:|bottom:|font-size|line-height|min-width|max-width|min-height|max-height/.test(l)) continue;
|
|
489
|
+
let m;
|
|
490
|
+
PX_RE.lastIndex = 0;
|
|
491
|
+
while ((m = PX_RE.exec(l)) !== null) {
|
|
492
|
+
const full = m[0];
|
|
493
|
+
if (PX_WHITELIST.has(full)) continue;
|
|
494
|
+
found.push(finding(
|
|
495
|
+
filePath,
|
|
496
|
+
i + 1,
|
|
497
|
+
"hardcoded-px",
|
|
498
|
+
"error",
|
|
499
|
+
`Hardcoded pixel value "${full}" in spacing/sizing bypasses the Borealis spacing token system.`,
|
|
500
|
+
"Replace with a semantic spacing token: var(--space-1), var(--space-2), var(--space-4), etc."
|
|
501
|
+
));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return found;
|
|
505
|
+
}
|
|
506
|
+
function ruleMissingDarkModeBlock(source, filePath) {
|
|
507
|
+
const ext = extname2(filePath);
|
|
508
|
+
if (!STYLE_EXTS.has(ext)) return [];
|
|
509
|
+
if (source.trim().length < 20) return [];
|
|
510
|
+
const hasDarkMediaQuery = /@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)/.test(source);
|
|
511
|
+
const hasDarkAttribute = /\[data-theme\s*=\s*["']?dark["']?\]/.test(source);
|
|
512
|
+
const hasDarkClass = /\.dark\b/.test(source);
|
|
513
|
+
if (!hasDarkMediaQuery && !hasDarkAttribute && !hasDarkClass) {
|
|
514
|
+
return [finding(
|
|
515
|
+
filePath,
|
|
516
|
+
1,
|
|
517
|
+
"missing-dark-mode-block",
|
|
518
|
+
"error",
|
|
519
|
+
"CSS file has no dark mode block. Borealis requires dark + light support from day one.",
|
|
520
|
+
'Add @media (prefers-color-scheme: dark) { ... } or [data-theme="dark"] { ... } with dark-mode token overrides.'
|
|
521
|
+
)];
|
|
522
|
+
}
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
function ruleMissingHoverTransition(source, filePath) {
|
|
526
|
+
const ext = extname2(filePath);
|
|
527
|
+
if (!STYLE_EXTS.has(ext)) return [];
|
|
528
|
+
const hasHover = /:hover/.test(source);
|
|
529
|
+
if (!hasHover) return [];
|
|
530
|
+
const hasTransition = /\btransition\b/.test(source);
|
|
531
|
+
if (!hasTransition) {
|
|
532
|
+
const hoverLine = source.split("\n").findIndex((l) => /:hover/.test(l));
|
|
533
|
+
return [finding(
|
|
534
|
+
filePath,
|
|
535
|
+
hoverLine + 1,
|
|
536
|
+
"missing-hover-transition",
|
|
537
|
+
"error",
|
|
538
|
+
":hover selector found but no transition property in this file. Borealis requires CSS transitions for hover states.",
|
|
539
|
+
"Add a transition property to the element's base styles, e.g. transition: color 150ms ease, background 150ms ease."
|
|
540
|
+
)];
|
|
541
|
+
}
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
function ruleDivAsInput(source, filePath) {
|
|
545
|
+
const ext = extname2(filePath);
|
|
546
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
547
|
+
const found = [];
|
|
548
|
+
const src = lines(source);
|
|
549
|
+
for (let i = 0; i < src.length; i++) {
|
|
550
|
+
const l = src[i];
|
|
551
|
+
if (/<div\b[^>]*onClick/.test(l) && !/role=|type=/.test(l)) {
|
|
552
|
+
found.push(finding(
|
|
553
|
+
filePath,
|
|
554
|
+
i + 1,
|
|
555
|
+
"div-as-input",
|
|
556
|
+
"error",
|
|
557
|
+
"<div onClick> used without role= \u2014 this is a div masquerading as an interactive element.",
|
|
558
|
+
'Use a <button> element or add role="button" and tabIndex={0} with keyboard handlers.'
|
|
559
|
+
));
|
|
560
|
+
}
|
|
561
|
+
if (/<div\b[^>]*onChange/.test(l)) {
|
|
562
|
+
found.push(finding(
|
|
563
|
+
filePath,
|
|
564
|
+
i + 1,
|
|
565
|
+
"div-as-input",
|
|
566
|
+
"error",
|
|
567
|
+
"<div onChange> detected. Real form elements only \u2014 no div-as-input.",
|
|
568
|
+
"Use <input>, <select>, or <textarea> with appropriate Visor wrapper components."
|
|
569
|
+
));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return found;
|
|
573
|
+
}
|
|
574
|
+
function ruleSetStateHover(source, filePath) {
|
|
575
|
+
const ext = extname2(filePath);
|
|
576
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
577
|
+
const found = [];
|
|
578
|
+
const src = lines(source);
|
|
579
|
+
for (let i = 0; i < src.length; i++) {
|
|
580
|
+
const l = src[i];
|
|
581
|
+
if (/onMouseEnter|onMouseLeave/.test(l) && /set[A-Z]/.test(l)) {
|
|
582
|
+
found.push(finding(
|
|
583
|
+
filePath,
|
|
584
|
+
i + 1,
|
|
585
|
+
"setstate-hover",
|
|
586
|
+
"error",
|
|
587
|
+
"onMouseEnter/onMouseLeave used to manage hover state via setState. Use CSS :hover instead.",
|
|
588
|
+
"Remove the mouse event handlers and hover state variable. Apply hover styles via CSS :hover."
|
|
589
|
+
));
|
|
590
|
+
}
|
|
591
|
+
if (/\buse[Ss]tate\b/.test(l) && /[Hh]overed|hover[Ss]tate|isHover/.test(l)) {
|
|
592
|
+
found.push(finding(
|
|
593
|
+
filePath,
|
|
594
|
+
i + 1,
|
|
595
|
+
"setstate-hover",
|
|
596
|
+
"error",
|
|
597
|
+
"useState used to track hover state. CSS :hover is zero-cost and more correct.",
|
|
598
|
+
"Delete this state variable and replace with CSS :hover selector."
|
|
599
|
+
));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
function ruleMissingAriaPressed(source, filePath) {
|
|
605
|
+
const ext = extname2(filePath);
|
|
606
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
607
|
+
const found = [];
|
|
608
|
+
const src = lines(source);
|
|
609
|
+
for (let i = 0; i < src.length; i++) {
|
|
610
|
+
const l = src[i];
|
|
611
|
+
if (/<button\b[^>]*(isActive|isOpen|isSelected|isToggled|active=|selected=|toggled=)/.test(l) && !/aria-pressed/.test(l)) {
|
|
612
|
+
found.push(finding(
|
|
613
|
+
filePath,
|
|
614
|
+
i + 1,
|
|
615
|
+
"missing-aria-pressed",
|
|
616
|
+
"error",
|
|
617
|
+
"Toggle button appears to be missing aria-pressed. Toggleable buttons must expose their state to assistive technology.",
|
|
618
|
+
"Add aria-pressed={isActive} (or equivalent) to the button element."
|
|
619
|
+
));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return found;
|
|
623
|
+
}
|
|
624
|
+
var BANNED_FONT_LIST = ["Inter", "Roboto", "Arial", "system-ui", "'Arial'", '"Arial"', "'Roboto'", '"Roboto"', "'Inter'", '"Inter"'];
|
|
625
|
+
function ruleBannedFonts(source, filePath) {
|
|
626
|
+
const found = [];
|
|
627
|
+
const src = lines(source);
|
|
628
|
+
for (let i = 0; i < src.length; i++) {
|
|
629
|
+
const l = src[i];
|
|
630
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
|
631
|
+
for (const font of BANNED_FONT_LIST) {
|
|
632
|
+
if (l.includes(font)) {
|
|
633
|
+
found.push(finding(
|
|
634
|
+
filePath,
|
|
635
|
+
i + 1,
|
|
636
|
+
"banned-fonts",
|
|
637
|
+
"warn",
|
|
638
|
+
`Banned font "${font}" detected. Borealis projects use Satoshi (or the project's designated typeface).`,
|
|
639
|
+
"Remove this font reference and use the Borealis font stack via var(--font-sans)."
|
|
640
|
+
));
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return found;
|
|
646
|
+
}
|
|
647
|
+
function rulePurpleGradientOnWhite(source, filePath) {
|
|
648
|
+
const found = [];
|
|
649
|
+
const src = lines(source);
|
|
650
|
+
const PURPLE_RE = /gradient.*?(?:purple|violet|#[89abcde][0-9a-f]|#[6-9][0-9a-f]{5})/i;
|
|
651
|
+
for (let i = 0; i < src.length; i++) {
|
|
652
|
+
const l = src[i];
|
|
653
|
+
if (PURPLE_RE.test(l)) {
|
|
654
|
+
found.push(finding(
|
|
655
|
+
filePath,
|
|
656
|
+
i + 1,
|
|
657
|
+
"purple-gradient-on-white",
|
|
658
|
+
"warn",
|
|
659
|
+
"Purple gradient detected \u2014 this is a generic SaaS visual cliche.",
|
|
660
|
+
"Replace with a gradient using your project's actual brand tokens."
|
|
661
|
+
));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return found;
|
|
665
|
+
}
|
|
666
|
+
function rulePureBlackUntinted(source, filePath) {
|
|
667
|
+
const found = [];
|
|
668
|
+
const src = lines(source);
|
|
669
|
+
for (let i = 0; i < src.length; i++) {
|
|
670
|
+
const l = src[i];
|
|
671
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
|
672
|
+
if (/#000000\b|#000\b|rgb\(\s*0\s*,\s*0\s*,\s*0\s*\)/.test(l) || /:\s*black\b/.test(l) || /color:\s*["']?black["']?/.test(l)) {
|
|
673
|
+
found.push(finding(
|
|
674
|
+
filePath,
|
|
675
|
+
i + 1,
|
|
676
|
+
"pure-black-untinted",
|
|
677
|
+
"warn",
|
|
678
|
+
"Pure black (#000) detected. Use a near-black tinted token for softer, more intentional contrast.",
|
|
679
|
+
"Replace with var(--color-text-primary) or the project's near-black token."
|
|
680
|
+
));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return found;
|
|
684
|
+
}
|
|
685
|
+
function ruleBounceEasing(source, filePath) {
|
|
686
|
+
const found = [];
|
|
687
|
+
const src = lines(source);
|
|
688
|
+
const BOUNCE_RE = /cubic-bezier\s*\([^)]*(?:1\.[1-9]|[-]0\.\d+)[^)]*\)/;
|
|
689
|
+
const KEYWORD_RE = /\bease-in-back\b|\bease-out-back\b|\bease-in-out-back\b|\bbounce\b/;
|
|
690
|
+
for (let i = 0; i < src.length; i++) {
|
|
691
|
+
const l = src[i];
|
|
692
|
+
if (BOUNCE_RE.test(l) || KEYWORD_RE.test(l)) {
|
|
693
|
+
found.push(finding(
|
|
694
|
+
filePath,
|
|
695
|
+
i + 1,
|
|
696
|
+
"bounce-easing",
|
|
697
|
+
"warn",
|
|
698
|
+
"Bounce/overshoot easing detected. Bouncy transitions feel playful/cheap in most UI contexts.",
|
|
699
|
+
"Use ease, ease-out, or a subtle cubic-bezier like cubic-bezier(0.4, 0, 0.2, 1)."
|
|
700
|
+
));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return found;
|
|
704
|
+
}
|
|
705
|
+
function ruleSub44pxTouchTarget(source, filePath) {
|
|
706
|
+
const found = [];
|
|
707
|
+
const src = lines(source);
|
|
708
|
+
const SMALL_RE = /(?:width|height|min-width|min-height)\s*:\s*(?:[1-9]|[1-3][0-9]|4[0-3])px\b/;
|
|
709
|
+
const INTERACTIVE_SELECTOR_RE = /button|btn|icon|touch|tap/i;
|
|
710
|
+
let currentSelector = "";
|
|
711
|
+
for (let i = 0; i < src.length; i++) {
|
|
712
|
+
const l = src[i];
|
|
713
|
+
if (l.trim().startsWith("//") || l.trim().startsWith("*")) continue;
|
|
714
|
+
if (/\{/.test(l) && !l.trim().startsWith("@")) {
|
|
715
|
+
currentSelector = l;
|
|
716
|
+
}
|
|
717
|
+
const interactiveContext = INTERACTIVE_SELECTOR_RE.test(l) || INTERACTIVE_SELECTOR_RE.test(currentSelector);
|
|
718
|
+
if (SMALL_RE.test(l) && interactiveContext) {
|
|
719
|
+
found.push(finding(
|
|
720
|
+
filePath,
|
|
721
|
+
i + 1,
|
|
722
|
+
"sub-44px-touch-target",
|
|
723
|
+
"warn",
|
|
724
|
+
"Potential sub-44px touch target detected on an interactive element.",
|
|
725
|
+
"Ensure all interactive elements have a minimum 44x44px touch target (WCAG 2.5.5)."
|
|
726
|
+
));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return found;
|
|
730
|
+
}
|
|
731
|
+
function ruleLineLengthOver75ch(source, filePath) {
|
|
732
|
+
const found = [];
|
|
733
|
+
const src = lines(source);
|
|
734
|
+
const CH_RE = /max-width\s*:\s*(\d+)ch/;
|
|
735
|
+
for (let i = 0; i < src.length; i++) {
|
|
736
|
+
const l = src[i];
|
|
737
|
+
const m = CH_RE.exec(l);
|
|
738
|
+
if (m && parseInt(m[1], 10) > 75) {
|
|
739
|
+
found.push(finding(
|
|
740
|
+
filePath,
|
|
741
|
+
i + 1,
|
|
742
|
+
"line-length-over-75ch",
|
|
743
|
+
"warn",
|
|
744
|
+
`Text container max-width of ${m[1]}ch exceeds the 75ch readability limit.`,
|
|
745
|
+
"Cap text container max-width at 65-75ch for optimal reading comfort."
|
|
746
|
+
));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return found;
|
|
750
|
+
}
|
|
751
|
+
function ruleGradientText(source, filePath) {
|
|
752
|
+
const found = [];
|
|
753
|
+
const src = lines(source);
|
|
754
|
+
for (let i = 0; i < src.length; i++) {
|
|
755
|
+
const l = src[i];
|
|
756
|
+
if (/background-clip\s*:\s*text/.test(l) && /(?:transparent|-webkit-text-fill-color)/.test(source)) {
|
|
757
|
+
found.push(finding(
|
|
758
|
+
filePath,
|
|
759
|
+
i + 1,
|
|
760
|
+
"gradient-text",
|
|
761
|
+
"warn",
|
|
762
|
+
"Gradient text (background-clip: text) detected. Often illegible at small sizes.",
|
|
763
|
+
"Use a solid semantic text color token. Reserve gradient text for display/hero headings only."
|
|
764
|
+
));
|
|
765
|
+
}
|
|
766
|
+
if (/-webkit-text-fill-color\s*:\s*transparent/.test(l) && /gradient/.test(source)) {
|
|
767
|
+
found.push(finding(
|
|
768
|
+
filePath,
|
|
769
|
+
i + 1,
|
|
770
|
+
"gradient-text",
|
|
771
|
+
"warn",
|
|
772
|
+
"Gradient text via -webkit-text-fill-color: transparent detected.",
|
|
773
|
+
"Use a solid semantic text color token. Reserve gradient text for display/hero headings only."
|
|
774
|
+
));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const seen = /* @__PURE__ */ new Set();
|
|
778
|
+
return found.filter((f) => {
|
|
779
|
+
if (seen.has(f.line)) return false;
|
|
780
|
+
seen.add(f.line);
|
|
781
|
+
return true;
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function ruleExcessiveCardNesting(source, filePath) {
|
|
785
|
+
const ext = extname2(filePath);
|
|
786
|
+
if (!CODE_EXTS.has(ext)) return [];
|
|
787
|
+
const found = [];
|
|
788
|
+
const src = lines(source);
|
|
789
|
+
let depth = 0;
|
|
790
|
+
const CARD_OPEN_RE = /<(?:Card|Panel|Box|Surface|Tile|Widget)\b/;
|
|
791
|
+
const CARD_CLOSE_RE = /<\/(?:Card|Panel|Box|Surface|Tile|Widget)>/;
|
|
792
|
+
for (let i = 0; i < src.length; i++) {
|
|
793
|
+
const l = src[i];
|
|
794
|
+
if (CARD_OPEN_RE.test(l)) {
|
|
795
|
+
depth++;
|
|
796
|
+
if (depth >= 3) {
|
|
797
|
+
found.push(finding(
|
|
798
|
+
filePath,
|
|
799
|
+
i + 1,
|
|
800
|
+
"excessive-card-nesting",
|
|
801
|
+
"warn",
|
|
802
|
+
`Card/Panel nested ${depth} levels deep. Deep nesting creates visual noise and unclear hierarchy.`,
|
|
803
|
+
"Flatten the layout. Use spacing, dividers, or type scale to create hierarchy instead of nested containers."
|
|
804
|
+
));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (CARD_CLOSE_RE.test(l)) depth = Math.max(0, depth - 1);
|
|
808
|
+
}
|
|
809
|
+
return found;
|
|
810
|
+
}
|
|
811
|
+
var RULES = [
|
|
812
|
+
// Errors — Borealis non-negotiables
|
|
813
|
+
{ name: "tier-1-token-direct-usage", severity: "error", fn: ruleTier1TokenDirectUsage },
|
|
814
|
+
{ name: "hardcoded-hex", severity: "error", fn: ruleHardcodedHex },
|
|
815
|
+
{ name: "hardcoded-px", severity: "error", fn: ruleHardcodedPx },
|
|
816
|
+
{ name: "missing-dark-mode-block", severity: "error", fn: ruleMissingDarkModeBlock },
|
|
817
|
+
{ name: "missing-hover-transition", severity: "error", fn: ruleMissingHoverTransition },
|
|
818
|
+
{ name: "div-as-input", severity: "error", fn: ruleDivAsInput },
|
|
819
|
+
{ name: "setstate-hover", severity: "error", fn: ruleSetStateHover },
|
|
820
|
+
{ name: "missing-aria-pressed", severity: "error", fn: ruleMissingAriaPressed },
|
|
821
|
+
// Warns — general anti-patterns
|
|
822
|
+
{ name: "banned-fonts", severity: "warn", fn: ruleBannedFonts },
|
|
823
|
+
{ name: "purple-gradient-on-white", severity: "warn", fn: rulePurpleGradientOnWhite },
|
|
824
|
+
{ name: "pure-black-untinted", severity: "warn", fn: rulePureBlackUntinted },
|
|
825
|
+
{ name: "bounce-easing", severity: "warn", fn: ruleBounceEasing },
|
|
826
|
+
{ name: "sub-44px-touch-target", severity: "warn", fn: ruleSub44pxTouchTarget },
|
|
827
|
+
{ name: "line-length-over-75ch", severity: "warn", fn: ruleLineLengthOver75ch },
|
|
828
|
+
{ name: "gradient-text", severity: "warn", fn: ruleGradientText },
|
|
829
|
+
{ name: "excessive-card-nesting", severity: "warn", fn: ruleExcessiveCardNesting }
|
|
830
|
+
];
|
|
831
|
+
function scanDesign(pathArg, options = {}) {
|
|
832
|
+
const files = collectFiles2(pathArg);
|
|
833
|
+
const { disabledRules = [], errorsOnly = false } = options;
|
|
834
|
+
const activeRules = RULES.filter((r) => {
|
|
835
|
+
if (disabledRules.includes(r.name)) return false;
|
|
836
|
+
if (errorsOnly && r.severity !== "error") return false;
|
|
837
|
+
return true;
|
|
838
|
+
});
|
|
839
|
+
const errors = [];
|
|
840
|
+
const warnings = [];
|
|
841
|
+
for (const file of files) {
|
|
842
|
+
let source;
|
|
843
|
+
try {
|
|
844
|
+
source = readFileSync3(file, "utf-8");
|
|
845
|
+
} catch {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
for (const rule of activeRules) {
|
|
849
|
+
const ruleFindings = rule.fn(source, file);
|
|
850
|
+
for (const f of ruleFindings) {
|
|
851
|
+
if (f.severity === "error") errors.push(f);
|
|
852
|
+
else warnings.push(f);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
errors,
|
|
858
|
+
warnings,
|
|
859
|
+
summary: {
|
|
860
|
+
errorCount: errors.length,
|
|
861
|
+
warningCount: warnings.length,
|
|
862
|
+
filesScanned: files.length
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
380
867
|
// src/utils/logger.ts
|
|
381
868
|
import pc from "picocolors";
|
|
382
869
|
var logger = {
|
|
@@ -404,6 +891,7 @@ var logger = {
|
|
|
404
891
|
};
|
|
405
892
|
|
|
406
893
|
// src/commands/check.ts
|
|
894
|
+
import pc2 from "picocolors";
|
|
407
895
|
var TYPE_FILTER = {
|
|
408
896
|
ui: "component",
|
|
409
897
|
blocks: "block",
|
|
@@ -534,6 +1022,58 @@ async function checkDiffCommand(pathArg, options) {
|
|
|
534
1022
|
logger.blank();
|
|
535
1023
|
if (options.failOnHits) process.exit(1);
|
|
536
1024
|
}
|
|
1025
|
+
function checkDesignCommand(pathArg, options) {
|
|
1026
|
+
const absPath = resolve3(pathArg);
|
|
1027
|
+
const rcDir = statSync3(absPath).isDirectory() ? absPath : dirname2(absPath);
|
|
1028
|
+
const rc = loadVisorRc(rcDir);
|
|
1029
|
+
const result = scanDesign(absPath, {
|
|
1030
|
+
disabledRules: rc.disabledRules ?? [],
|
|
1031
|
+
errorsOnly: options.errorsOnly ?? false
|
|
1032
|
+
});
|
|
1033
|
+
const useJson = options.json || options.format === "json";
|
|
1034
|
+
if (useJson) {
|
|
1035
|
+
console.log(JSON.stringify({ success: true, ...result }, null, 2));
|
|
1036
|
+
if (!options.noFail && result.summary.errorCount > 0) process.exit(1);
|
|
1037
|
+
process.exit(0);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const { errors, warnings, summary } = result;
|
|
1041
|
+
if (summary.errorCount === 0 && summary.warningCount === 0) {
|
|
1042
|
+
logger.success(`No design anti-patterns found \u2014 ${summary.filesScanned} file(s) scanned.`);
|
|
1043
|
+
process.exit(0);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
logger.blank();
|
|
1047
|
+
logger.heading(`visor check design \u2014 ${summary.filesScanned} file(s) scanned`);
|
|
1048
|
+
logger.blank();
|
|
1049
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1050
|
+
const allFindings = [...errors, ...warnings];
|
|
1051
|
+
for (const f of allFindings) {
|
|
1052
|
+
if (!byFile.has(f.file)) byFile.set(f.file, []);
|
|
1053
|
+
byFile.get(f.file).push(f);
|
|
1054
|
+
}
|
|
1055
|
+
for (const [file, fileFindings] of byFile) {
|
|
1056
|
+
logger.heading(` ${file}`);
|
|
1057
|
+
for (const f of fileFindings) {
|
|
1058
|
+
const loc = pc2.dim(`${f.line}:`);
|
|
1059
|
+
const badge = f.severity === "error" ? pc2.red("error") : pc2.yellow("warn ");
|
|
1060
|
+
const ruleName = pc2.dim(`[${f.rule}]`);
|
|
1061
|
+
console.log(` ${loc} ${badge} ${f.message} ${ruleName}`);
|
|
1062
|
+
if (f.fix) {
|
|
1063
|
+
console.log(` ${pc2.dim("fix:")} ${pc2.cyan(f.fix)}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
logger.blank();
|
|
1067
|
+
}
|
|
1068
|
+
logger.blank();
|
|
1069
|
+
if (summary.errorCount > 0) {
|
|
1070
|
+
logger.error(`${summary.errorCount} error(s), ${summary.warningCount} warning(s)`);
|
|
1071
|
+
} else {
|
|
1072
|
+
logger.warn(`${summary.warningCount} warning(s) (0 errors)`);
|
|
1073
|
+
}
|
|
1074
|
+
logger.blank();
|
|
1075
|
+
if (!options.noFail && summary.errorCount > 0) process.exit(1);
|
|
1076
|
+
}
|
|
537
1077
|
function checkCommand() {
|
|
538
1078
|
const check = new Command("check").description("Check Visor catalog \u2014 list items, test existence, scan JSX for native HTML");
|
|
539
1079
|
check.command("list").description("List all catalog items (components, blocks, hooks, patterns)").option("--type <type>", "filter by type: ui, blocks, hooks, patterns, all (default: all)").option("--json", "output structured JSON (for AI agents)").action((options) => {
|
|
@@ -545,18 +1085,21 @@ function checkCommand() {
|
|
|
545
1085
|
check.command("diff").description("Scan JSX/TSX for native HTML elements that have Visor equivalents").argument("<path>", "file path, directory, or - for stdin").option("--fail-on-hits", "exit 1 if any native HTML usages are found (for CI use)").option("--json", "output structured JSON (for AI agents)").action(async (pathArg, options) => {
|
|
546
1086
|
await checkDiffCommand(pathArg, options);
|
|
547
1087
|
});
|
|
1088
|
+
check.command("design").description("Scan frontend code for Borealis design anti-patterns (deterministic, no LLM)").argument("<path>", "file path or directory to scan").option("--format <format>", "output format: json or human (default: human when TTY, json otherwise)").option("--errors-only", "report only error-severity rules (skip warnings)").option("--no-fail", "do not exit 1 on errors (advisory mode)").option("--json", "shorthand for --format json").action((pathArg, options) => {
|
|
1089
|
+
checkDesignCommand(pathArg, options);
|
|
1090
|
+
});
|
|
548
1091
|
return check;
|
|
549
1092
|
}
|
|
550
1093
|
|
|
551
1094
|
// src/commands/init.ts
|
|
552
|
-
import { existsSync as
|
|
553
|
-
import { join as
|
|
1095
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync, readFileSync as readFileSync6 } from "fs";
|
|
1096
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
554
1097
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
555
1098
|
import * as childProcess from "child_process";
|
|
556
1099
|
|
|
557
1100
|
// src/config/config.ts
|
|
558
|
-
import { readFileSync as
|
|
559
|
-
import { join as
|
|
1101
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
1102
|
+
import { join as join4 } from "path";
|
|
560
1103
|
|
|
561
1104
|
// src/config/defaults.ts
|
|
562
1105
|
var DEFAULT_CONFIG = {
|
|
@@ -573,19 +1116,19 @@ var CONFIG_FILE = "visor.json";
|
|
|
573
1116
|
|
|
574
1117
|
// src/config/config.ts
|
|
575
1118
|
function getConfigPath(cwd) {
|
|
576
|
-
return
|
|
1119
|
+
return join4(cwd, CONFIG_FILE);
|
|
577
1120
|
}
|
|
578
1121
|
function configExists(cwd) {
|
|
579
|
-
return
|
|
1122
|
+
return existsSync2(getConfigPath(cwd));
|
|
580
1123
|
}
|
|
581
1124
|
function loadConfig(cwd) {
|
|
582
1125
|
const configPath = getConfigPath(cwd);
|
|
583
|
-
if (!
|
|
1126
|
+
if (!existsSync2(configPath)) {
|
|
584
1127
|
throw new Error(
|
|
585
1128
|
`No ${CONFIG_FILE} found. Run "visor init" first.`
|
|
586
1129
|
);
|
|
587
1130
|
}
|
|
588
|
-
const raw =
|
|
1131
|
+
const raw = readFileSync4(configPath, "utf-8");
|
|
589
1132
|
const parsed = JSON.parse(raw);
|
|
590
1133
|
const knownKeys = /* @__PURE__ */ new Set(["paths"]);
|
|
591
1134
|
for (const key of Object.keys(parsed)) {
|
|
@@ -622,12 +1165,12 @@ function writeConfig(cwd, config) {
|
|
|
622
1165
|
|
|
623
1166
|
// src/utils/packages.ts
|
|
624
1167
|
import { execFileSync } from "child_process";
|
|
625
|
-
import { existsSync as
|
|
626
|
-
import { join as
|
|
1168
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
1169
|
+
import { join as join5 } from "path";
|
|
627
1170
|
function readPackageJson(cwd) {
|
|
628
|
-
const pkgPath =
|
|
629
|
-
if (!
|
|
630
|
-
return JSON.parse(
|
|
1171
|
+
const pkgPath = join5(cwd, "package.json");
|
|
1172
|
+
if (!existsSync3(pkgPath)) return null;
|
|
1173
|
+
return JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
631
1174
|
}
|
|
632
1175
|
function isPackageInstalled(packageName, cwd) {
|
|
633
1176
|
const pkg = readPackageJson(cwd);
|
|
@@ -704,7 +1247,7 @@ function initCommand(cwd, options) {
|
|
|
704
1247
|
emitError(json, `Unknown template: ${options.template}. Available templates: nextjs`);
|
|
705
1248
|
process.exit(1);
|
|
706
1249
|
}
|
|
707
|
-
if (options?.template === "nextjs" &&
|
|
1250
|
+
if (options?.template === "nextjs" && existsSync4(join6(cwd, "package.json"))) {
|
|
708
1251
|
emitError(
|
|
709
1252
|
json,
|
|
710
1253
|
"package.json already exists in this directory. visor init --template nextjs only scaffolds into empty directories. For an existing app, see the retrofit flow: https://visor.loworbit.studio/docs/guides/migration"
|
|
@@ -787,8 +1330,8 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
|
|
|
787
1330
|
}
|
|
788
1331
|
runCreateNextApp(cwd, json);
|
|
789
1332
|
runInstallVisorDeps(cwd, json);
|
|
790
|
-
const yamlPath =
|
|
791
|
-
if (
|
|
1333
|
+
const yamlPath = join6(cwd, ".visor.yaml");
|
|
1334
|
+
if (existsSync4(yamlPath)) {
|
|
792
1335
|
filesSkipped.push(".visor.yaml");
|
|
793
1336
|
if (!json) {
|
|
794
1337
|
logger.warn(".visor.yaml already exists. Skipping.");
|
|
@@ -806,10 +1349,10 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
|
|
|
806
1349
|
tokens: data.tokens,
|
|
807
1350
|
config: data.config
|
|
808
1351
|
});
|
|
809
|
-
const globalsPath =
|
|
810
|
-
const globalsDir =
|
|
1352
|
+
const globalsPath = join6(cwd, "app", "globals.css");
|
|
1353
|
+
const globalsDir = dirname3(globalsPath);
|
|
811
1354
|
mkdirSync(globalsDir, { recursive: true });
|
|
812
|
-
if (
|
|
1355
|
+
if (existsSync4(globalsPath)) {
|
|
813
1356
|
writeFileSync2(globalsPath, css, "utf-8");
|
|
814
1357
|
filesCreated.push("app/globals.css");
|
|
815
1358
|
} else {
|
|
@@ -819,15 +1362,15 @@ function scaffoldNextjs(cwd, json, filesCreated, filesSkipped, warnings) {
|
|
|
819
1362
|
if (!json) {
|
|
820
1363
|
logger.success("Created app/globals.css with theme tokens");
|
|
821
1364
|
}
|
|
822
|
-
const layoutPath =
|
|
1365
|
+
const layoutPath = join6(cwd, "app", "layout.tsx");
|
|
823
1366
|
writeFileSync2(layoutPath, generateNextjsLayout(), "utf-8");
|
|
824
1367
|
filesCreated.push("app/layout.tsx");
|
|
825
1368
|
if (!json) {
|
|
826
1369
|
logger.success("Wired app/layout.tsx with FOWT prevention and theme tokens");
|
|
827
1370
|
}
|
|
828
|
-
const stampDir =
|
|
829
|
-
const stampPath =
|
|
830
|
-
if (
|
|
1371
|
+
const stampDir = join6(cwd, ".lo");
|
|
1372
|
+
const stampPath = join6(stampDir, "borealis.json");
|
|
1373
|
+
if (existsSync4(stampPath)) {
|
|
831
1374
|
filesSkipped.push(".lo/borealis.json");
|
|
832
1375
|
if (!json) {
|
|
833
1376
|
logger.warn(".lo/borealis.json already exists. Skipping.");
|
|
@@ -891,12 +1434,12 @@ function assertSpawnSuccess(result, label) {
|
|
|
891
1434
|
}
|
|
892
1435
|
function readVisorCliVersion() {
|
|
893
1436
|
try {
|
|
894
|
-
const here =
|
|
1437
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
895
1438
|
for (let i = 0; i < 5; i++) {
|
|
896
1439
|
const segments = new Array(i).fill("..");
|
|
897
|
-
const candidate =
|
|
1440
|
+
const candidate = join6(here, ...segments, "package.json");
|
|
898
1441
|
try {
|
|
899
|
-
const pkg = JSON.parse(
|
|
1442
|
+
const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
|
|
900
1443
|
if (pkg.name === "@loworbitstudio/visor" && pkg.version) {
|
|
901
1444
|
return pkg.version;
|
|
902
1445
|
}
|
|
@@ -911,52 +1454,52 @@ function readVisorCliVersion() {
|
|
|
911
1454
|
// src/utils/fs.ts
|
|
912
1455
|
import {
|
|
913
1456
|
writeFileSync as writeFileSync3,
|
|
914
|
-
readFileSync as
|
|
915
|
-
existsSync as
|
|
1457
|
+
readFileSync as readFileSync7,
|
|
1458
|
+
existsSync as existsSync5,
|
|
916
1459
|
mkdirSync as mkdirSync2
|
|
917
1460
|
} from "fs";
|
|
918
|
-
import { dirname as
|
|
1461
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
919
1462
|
function resolveOutputPath(registryPath, type, config, cwd) {
|
|
920
1463
|
let relativePath;
|
|
921
1464
|
if (type === "registry:block") {
|
|
922
1465
|
relativePath = registryPath.replace(/^blocks\//, "");
|
|
923
|
-
return
|
|
1466
|
+
return join7(cwd, config.paths.blocks, relativePath);
|
|
924
1467
|
}
|
|
925
1468
|
if (type === "registry:ui") {
|
|
926
1469
|
if (registryPath.startsWith("components/deck/")) {
|
|
927
1470
|
relativePath = registryPath.replace(/^components\/deck\//, "");
|
|
928
|
-
return
|
|
1471
|
+
return join7(cwd, config.paths.deckComponents, relativePath);
|
|
929
1472
|
}
|
|
930
1473
|
if (registryPath.startsWith("components/flutter/")) {
|
|
931
1474
|
relativePath = registryPath.replace(/^components\/flutter\//, "");
|
|
932
|
-
return
|
|
1475
|
+
return join7(cwd, config.paths.flutterComponents, relativePath);
|
|
933
1476
|
}
|
|
934
1477
|
relativePath = registryPath.replace(/^components\/ui\//, "");
|
|
935
|
-
return
|
|
1478
|
+
return join7(cwd, config.paths.components, relativePath);
|
|
936
1479
|
}
|
|
937
1480
|
if (type === "registry:hook") {
|
|
938
1481
|
relativePath = registryPath.replace(/^hooks\//, "");
|
|
939
|
-
return
|
|
1482
|
+
return join7(cwd, config.paths.hooks, relativePath);
|
|
940
1483
|
}
|
|
941
1484
|
if (type === "registry:lib") {
|
|
942
1485
|
relativePath = registryPath.replace(/^lib\//, "");
|
|
943
|
-
return
|
|
1486
|
+
return join7(cwd, config.paths.lib, relativePath);
|
|
944
1487
|
}
|
|
945
|
-
return
|
|
1488
|
+
return join7(cwd, registryPath);
|
|
946
1489
|
}
|
|
947
1490
|
function writeFile(filePath, content) {
|
|
948
|
-
const dir =
|
|
949
|
-
if (!
|
|
1491
|
+
const dir = dirname4(filePath);
|
|
1492
|
+
if (!existsSync5(dir)) {
|
|
950
1493
|
mkdirSync2(dir, { recursive: true });
|
|
951
1494
|
}
|
|
952
1495
|
writeFileSync3(filePath, content, "utf-8");
|
|
953
1496
|
}
|
|
954
1497
|
function readFile(filePath) {
|
|
955
|
-
if (!
|
|
956
|
-
return
|
|
1498
|
+
if (!existsSync5(filePath)) return null;
|
|
1499
|
+
return readFileSync7(filePath, "utf-8");
|
|
957
1500
|
}
|
|
958
1501
|
function fileExists(filePath) {
|
|
959
|
-
return
|
|
1502
|
+
return existsSync5(filePath);
|
|
960
1503
|
}
|
|
961
1504
|
|
|
962
1505
|
// src/commands/list.ts
|
|
@@ -1097,8 +1640,8 @@ function listCommand(cwd, options = {}) {
|
|
|
1097
1640
|
}
|
|
1098
1641
|
|
|
1099
1642
|
// src/utils/pubspec.ts
|
|
1100
|
-
import { existsSync as
|
|
1101
|
-
import { join as
|
|
1643
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
1644
|
+
import { join as join8 } from "path";
|
|
1102
1645
|
import { parseDocument, YAMLMap } from "yaml";
|
|
1103
1646
|
function mergePubspec(pubspecText, deps) {
|
|
1104
1647
|
const doc = parseDocument(pubspecText);
|
|
@@ -1120,14 +1663,14 @@ function mergePubspec(pubspecText, deps) {
|
|
|
1120
1663
|
return { text: doc.toString(), added, skipped };
|
|
1121
1664
|
}
|
|
1122
1665
|
function pubspecPath(cwd) {
|
|
1123
|
-
return
|
|
1666
|
+
return join8(cwd, "pubspec.yaml");
|
|
1124
1667
|
}
|
|
1125
1668
|
function pubspecExists(cwd) {
|
|
1126
|
-
return
|
|
1669
|
+
return existsSync6(pubspecPath(cwd));
|
|
1127
1670
|
}
|
|
1128
1671
|
function isPubPackageInstalled(packageName, cwd) {
|
|
1129
1672
|
if (!pubspecExists(cwd)) return false;
|
|
1130
|
-
const text =
|
|
1673
|
+
const text = readFileSync8(pubspecPath(cwd), "utf-8");
|
|
1131
1674
|
const doc = parseDocument(text);
|
|
1132
1675
|
const depsNode = doc.get("dependencies");
|
|
1133
1676
|
if (!(depsNode instanceof YAMLMap)) return false;
|
|
@@ -1138,12 +1681,12 @@ function getUninstalledPubDeps(deps, cwd) {
|
|
|
1138
1681
|
}
|
|
1139
1682
|
function addPubDependencies(deps, cwd) {
|
|
1140
1683
|
const path2 = pubspecPath(cwd);
|
|
1141
|
-
if (!
|
|
1684
|
+
if (!existsSync6(path2)) {
|
|
1142
1685
|
throw new Error(
|
|
1143
1686
|
`No pubspec.yaml found at ${path2}. Run this command from a Flutter project root.`
|
|
1144
1687
|
);
|
|
1145
1688
|
}
|
|
1146
|
-
const text =
|
|
1689
|
+
const text = readFileSync8(path2, "utf-8");
|
|
1147
1690
|
const result = mergePubspec(text, deps);
|
|
1148
1691
|
if (result.added.length > 0) {
|
|
1149
1692
|
writeFileSync4(path2, result.text, "utf-8");
|
|
@@ -1153,12 +1696,12 @@ function addPubDependencies(deps, cwd) {
|
|
|
1153
1696
|
|
|
1154
1697
|
// src/utils/flutter.ts
|
|
1155
1698
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
1156
|
-
import { existsSync as
|
|
1699
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1157
1700
|
import { homedir } from "os";
|
|
1158
|
-
import { join as
|
|
1701
|
+
import { join as join9 } from "path";
|
|
1159
1702
|
function isExecutable(path2) {
|
|
1160
1703
|
try {
|
|
1161
|
-
const s =
|
|
1704
|
+
const s = statSync4(path2);
|
|
1162
1705
|
return s.isFile();
|
|
1163
1706
|
} catch {
|
|
1164
1707
|
return false;
|
|
@@ -1166,24 +1709,24 @@ function isExecutable(path2) {
|
|
|
1166
1709
|
}
|
|
1167
1710
|
function fromPath(env) {
|
|
1168
1711
|
const pathVar = env.PATH ?? "";
|
|
1169
|
-
const
|
|
1712
|
+
const sep2 = process.platform === "win32" ? ";" : ":";
|
|
1170
1713
|
const bin = process.platform === "win32" ? "flutter.bat" : "flutter";
|
|
1171
|
-
for (const dir of pathVar.split(
|
|
1714
|
+
for (const dir of pathVar.split(sep2)) {
|
|
1172
1715
|
if (!dir) continue;
|
|
1173
|
-
const candidate =
|
|
1716
|
+
const candidate = join9(dir, bin);
|
|
1174
1717
|
if (isExecutable(candidate)) return candidate;
|
|
1175
1718
|
}
|
|
1176
1719
|
return null;
|
|
1177
1720
|
}
|
|
1178
1721
|
function fromFvm(home) {
|
|
1179
|
-
const fvmDefault =
|
|
1722
|
+
const fvmDefault = join9(home, "fvm", "default", "bin", "flutter");
|
|
1180
1723
|
if (isExecutable(fvmDefault)) return fvmDefault;
|
|
1181
|
-
const versionsDir =
|
|
1182
|
-
if (!
|
|
1724
|
+
const versionsDir = join9(home, "fvm", "versions");
|
|
1725
|
+
if (!existsSync7(versionsDir)) return null;
|
|
1183
1726
|
let best = null;
|
|
1184
1727
|
try {
|
|
1185
|
-
for (const name of
|
|
1186
|
-
const candidate =
|
|
1728
|
+
for (const name of readdirSync3(versionsDir)) {
|
|
1729
|
+
const candidate = join9(versionsDir, name, "bin", "flutter");
|
|
1187
1730
|
if (!isExecutable(candidate)) continue;
|
|
1188
1731
|
if (!best || compareSemver(name, best.version) > 0) {
|
|
1189
1732
|
best = { version: name, path: candidate };
|
|
@@ -1209,7 +1752,7 @@ function findFlutterBin(options = {}) {
|
|
|
1209
1752
|
const home = options.home ?? homedir();
|
|
1210
1753
|
const envRoot = env.FLUTTER_ROOT;
|
|
1211
1754
|
if (envRoot) {
|
|
1212
|
-
const bin =
|
|
1755
|
+
const bin = join9(envRoot, "bin", "flutter");
|
|
1213
1756
|
if (isExecutable(bin)) return bin;
|
|
1214
1757
|
}
|
|
1215
1758
|
const fromPathBin = fromPath(env);
|
|
@@ -1852,8 +2395,8 @@ function infoCommand(name, cwd, options = {}) {
|
|
|
1852
2395
|
}
|
|
1853
2396
|
|
|
1854
2397
|
// src/commands/theme-apply.ts
|
|
1855
|
-
import { readFileSync as
|
|
1856
|
-
import { resolve as
|
|
2398
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
2399
|
+
import { resolve as resolve4, dirname as dirname5, join as join10 } from "path";
|
|
1857
2400
|
import { generateTheme, generateThemeData as generateThemeData2 } from "@loworbitstudio/visor-theme-engine";
|
|
1858
2401
|
import {
|
|
1859
2402
|
nextjsAdapter as nextjsAdapter2,
|
|
@@ -1883,10 +2426,10 @@ function defaultOutputPath(adapter, themeName) {
|
|
|
1883
2426
|
}
|
|
1884
2427
|
}
|
|
1885
2428
|
function themeApplyCommand(file, cwd, options) {
|
|
1886
|
-
const filePath =
|
|
2429
|
+
const filePath = resolve4(cwd, file);
|
|
1887
2430
|
let yamlContent;
|
|
1888
2431
|
try {
|
|
1889
|
-
yamlContent =
|
|
2432
|
+
yamlContent = readFileSync9(filePath, "utf-8");
|
|
1890
2433
|
} catch {
|
|
1891
2434
|
if (options.json) {
|
|
1892
2435
|
console.log(
|
|
@@ -1963,14 +2506,14 @@ function themeApplyCommand(file, cwd, options) {
|
|
|
1963
2506
|
process.exit(1);
|
|
1964
2507
|
}
|
|
1965
2508
|
const outputTarget = options.output ?? defaultOutputPath(options.adapter, themeName);
|
|
1966
|
-
const outputPath =
|
|
2509
|
+
const outputPath = resolve4(cwd, outputTarget);
|
|
1967
2510
|
if (fileMap) {
|
|
1968
2511
|
try {
|
|
1969
2512
|
mkdirSync3(outputPath, { recursive: true });
|
|
1970
2513
|
let totalBytes = 0;
|
|
1971
2514
|
for (const [relPath, content] of Object.entries(fileMap.files)) {
|
|
1972
|
-
const filePath2 =
|
|
1973
|
-
mkdirSync3(
|
|
2515
|
+
const filePath2 = join10(outputPath, relPath);
|
|
2516
|
+
mkdirSync3(dirname5(filePath2), { recursive: true });
|
|
1974
2517
|
writeFileSync5(filePath2, content, "utf-8");
|
|
1975
2518
|
totalBytes += content.length;
|
|
1976
2519
|
}
|
|
@@ -2008,7 +2551,7 @@ function themeApplyCommand(file, cwd, options) {
|
|
|
2008
2551
|
if (css === null) {
|
|
2009
2552
|
process.exit(1);
|
|
2010
2553
|
}
|
|
2011
|
-
const outputDir =
|
|
2554
|
+
const outputDir = dirname5(outputPath);
|
|
2012
2555
|
try {
|
|
2013
2556
|
mkdirSync3(outputDir, { recursive: true });
|
|
2014
2557
|
writeFileSync5(outputPath, css, "utf-8");
|
|
@@ -2052,8 +2595,8 @@ function formatSize(bytes) {
|
|
|
2052
2595
|
}
|
|
2053
2596
|
|
|
2054
2597
|
// src/commands/theme-export.ts
|
|
2055
|
-
import { readFileSync as
|
|
2056
|
-
import { resolve as
|
|
2598
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2599
|
+
import { resolve as resolve5 } from "path";
|
|
2057
2600
|
import {
|
|
2058
2601
|
parseConfig,
|
|
2059
2602
|
resolveConfig,
|
|
@@ -2061,10 +2604,10 @@ import {
|
|
|
2061
2604
|
exportTheme
|
|
2062
2605
|
} from "@loworbitstudio/visor-theme-engine";
|
|
2063
2606
|
function themeExportCommand(file, cwd, options) {
|
|
2064
|
-
const filePath =
|
|
2607
|
+
const filePath = resolve5(cwd, file ?? ".visor.yaml");
|
|
2065
2608
|
let yamlContent;
|
|
2066
2609
|
try {
|
|
2067
|
-
yamlContent =
|
|
2610
|
+
yamlContent = readFileSync10(filePath, "utf-8");
|
|
2068
2611
|
} catch {
|
|
2069
2612
|
if (options.json) {
|
|
2070
2613
|
console.log(
|
|
@@ -2116,16 +2659,16 @@ function themeExportCommand(file, cwd, options) {
|
|
|
2116
2659
|
}
|
|
2117
2660
|
|
|
2118
2661
|
// src/commands/theme-validate.ts
|
|
2119
|
-
import { readFileSync as
|
|
2120
|
-
import { resolve as
|
|
2662
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2663
|
+
import { resolve as resolve6 } from "path";
|
|
2121
2664
|
import { parse as parseYaml } from "yaml";
|
|
2122
2665
|
import { validate } from "@loworbitstudio/visor-theme-engine";
|
|
2123
|
-
import
|
|
2666
|
+
import pc3 from "picocolors";
|
|
2124
2667
|
function themeValidateCommand(file, cwd, options) {
|
|
2125
|
-
const filePath =
|
|
2668
|
+
const filePath = resolve6(cwd, file);
|
|
2126
2669
|
let fileContent;
|
|
2127
2670
|
try {
|
|
2128
|
-
fileContent =
|
|
2671
|
+
fileContent = readFileSync11(filePath, "utf-8");
|
|
2129
2672
|
} catch {
|
|
2130
2673
|
if (options.json) {
|
|
2131
2674
|
console.log(
|
|
@@ -2205,15 +2748,127 @@ function themeValidateCommand(file, cwd, options) {
|
|
|
2205
2748
|
}
|
|
2206
2749
|
}
|
|
2207
2750
|
function printIssue(issue) {
|
|
2208
|
-
const prefix = issue.severity === "error" ?
|
|
2209
|
-
const code =
|
|
2210
|
-
const path2 = issue.path ?
|
|
2751
|
+
const prefix = issue.severity === "error" ? pc3.red(" ERROR") : pc3.yellow(" WARN ");
|
|
2752
|
+
const code = pc3.dim(`[${issue.code}]`);
|
|
2753
|
+
const path2 = issue.path ? pc3.dim(` (${issue.path})`) : "";
|
|
2211
2754
|
console.log(`${prefix} ${code} ${issue.message}${path2}`);
|
|
2212
2755
|
}
|
|
2213
2756
|
|
|
2757
|
+
// src/commands/theme-verify.ts
|
|
2758
|
+
import { spawnSync as _spawnSync } from "child_process";
|
|
2759
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2760
|
+
import { resolve as resolve7 } from "path";
|
|
2761
|
+
function themeVerifyCommand(dir, cwd, options, _spawnFn = _spawnSync) {
|
|
2762
|
+
const target = options.target ?? "flutter";
|
|
2763
|
+
if (target !== "flutter") {
|
|
2764
|
+
if (options.json) {
|
|
2765
|
+
console.log(
|
|
2766
|
+
JSON.stringify({
|
|
2767
|
+
valid: false,
|
|
2768
|
+
target,
|
|
2769
|
+
errors: [
|
|
2770
|
+
{
|
|
2771
|
+
code: "UNSUPPORTED_TARGET",
|
|
2772
|
+
message: `Unsupported target: "${target}". Only "flutter" is supported.`
|
|
2773
|
+
}
|
|
2774
|
+
]
|
|
2775
|
+
})
|
|
2776
|
+
);
|
|
2777
|
+
} else {
|
|
2778
|
+
logger.error(`Unsupported target: "${target}". Only "flutter" is supported.`);
|
|
2779
|
+
}
|
|
2780
|
+
process.exit(1);
|
|
2781
|
+
}
|
|
2782
|
+
const dirPath = resolve7(cwd, dir);
|
|
2783
|
+
if (!existsSync8(dirPath)) {
|
|
2784
|
+
if (options.json) {
|
|
2785
|
+
console.log(
|
|
2786
|
+
JSON.stringify({
|
|
2787
|
+
valid: false,
|
|
2788
|
+
target,
|
|
2789
|
+
dir: dirPath,
|
|
2790
|
+
errors: [
|
|
2791
|
+
{
|
|
2792
|
+
code: "DIR_NOT_FOUND",
|
|
2793
|
+
message: `Directory not found: ${dirPath}`
|
|
2794
|
+
}
|
|
2795
|
+
]
|
|
2796
|
+
})
|
|
2797
|
+
);
|
|
2798
|
+
} else {
|
|
2799
|
+
logger.error(`Directory not found: ${dirPath}`);
|
|
2800
|
+
logger.info("Make sure the path exists and is readable.");
|
|
2801
|
+
}
|
|
2802
|
+
process.exit(1);
|
|
2803
|
+
}
|
|
2804
|
+
const flutterBin = findFlutterBin();
|
|
2805
|
+
if (!flutterBin) {
|
|
2806
|
+
if (options.json) {
|
|
2807
|
+
console.log(
|
|
2808
|
+
JSON.stringify({
|
|
2809
|
+
valid: false,
|
|
2810
|
+
target,
|
|
2811
|
+
dir: dirPath,
|
|
2812
|
+
errors: [
|
|
2813
|
+
{
|
|
2814
|
+
code: "FLUTTER_NOT_FOUND",
|
|
2815
|
+
message: "Flutter binary not found. Set FLUTTER_ROOT, add flutter to PATH, or install via FVM."
|
|
2816
|
+
}
|
|
2817
|
+
]
|
|
2818
|
+
})
|
|
2819
|
+
);
|
|
2820
|
+
} else {
|
|
2821
|
+
logger.error("Flutter binary not found.");
|
|
2822
|
+
logger.info(
|
|
2823
|
+
"Set FLUTTER_ROOT, add flutter to PATH, or install via FVM."
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
process.exit(1);
|
|
2827
|
+
}
|
|
2828
|
+
if (!options.json) {
|
|
2829
|
+
logger.info(`Verifying Flutter output at: ${dirPath}`);
|
|
2830
|
+
logger.item(`Using Flutter binary: ${flutterBin}`);
|
|
2831
|
+
}
|
|
2832
|
+
const result = _spawnFn(flutterBin, ["analyze", "--no-pub"], {
|
|
2833
|
+
cwd: dirPath,
|
|
2834
|
+
stdio: options.json ? "pipe" : "inherit",
|
|
2835
|
+
encoding: "utf-8"
|
|
2836
|
+
});
|
|
2837
|
+
if (options.json) {
|
|
2838
|
+
const stdout = (result.stdout ?? "").toString();
|
|
2839
|
+
const stderr = (result.stderr ?? "").toString();
|
|
2840
|
+
const success = result.status === 0;
|
|
2841
|
+
console.log(
|
|
2842
|
+
JSON.stringify({
|
|
2843
|
+
valid: success,
|
|
2844
|
+
target,
|
|
2845
|
+
dir: dirPath,
|
|
2846
|
+
exitCode: result.status,
|
|
2847
|
+
stdout: stdout.trim() || void 0,
|
|
2848
|
+
stderr: stderr.trim() || void 0,
|
|
2849
|
+
errors: success ? [] : [
|
|
2850
|
+
{
|
|
2851
|
+
code: "DART_ANALYZE_FAILED",
|
|
2852
|
+
message: stderr.trim() || stdout.trim() || "dart analyze reported errors"
|
|
2853
|
+
}
|
|
2854
|
+
]
|
|
2855
|
+
})
|
|
2856
|
+
);
|
|
2857
|
+
process.exit(success ? 0 : 1);
|
|
2858
|
+
}
|
|
2859
|
+
if (result.status === 0) {
|
|
2860
|
+
logger.success("Flutter output is clean \u2014 dart analyze passed.");
|
|
2861
|
+
process.exit(0);
|
|
2862
|
+
} else {
|
|
2863
|
+
logger.error("dart analyze reported errors in the generated output.");
|
|
2864
|
+
logger.info("Fix the errors above, then re-run: visor theme apply --target flutter");
|
|
2865
|
+
process.exit(1);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2214
2869
|
// src/commands/theme-extract.ts
|
|
2215
|
-
import { readFileSync as
|
|
2216
|
-
import { resolve as
|
|
2870
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
2871
|
+
import { resolve as resolve8, join as join11, basename as basename2, extname as extname3, relative } from "path";
|
|
2217
2872
|
import { stringify as stringifyYaml } from "yaml";
|
|
2218
2873
|
import {
|
|
2219
2874
|
extractFromCSS,
|
|
@@ -2242,8 +2897,8 @@ var CSS_DIRS = [
|
|
|
2242
2897
|
"packages/design-tokens"
|
|
2243
2898
|
];
|
|
2244
2899
|
function themeExtractCommand(cwd, options) {
|
|
2245
|
-
const targetDir =
|
|
2246
|
-
if (!
|
|
2900
|
+
const targetDir = resolve8(cwd, options.from ?? ".");
|
|
2901
|
+
if (!existsSync9(targetDir)) {
|
|
2247
2902
|
if (options.json) {
|
|
2248
2903
|
console.log(JSON.stringify({ success: false, error: `Directory not found: ${targetDir}` }));
|
|
2249
2904
|
} else {
|
|
@@ -2297,16 +2952,16 @@ function collectCSSFiles(targetDir) {
|
|
|
2297
2952
|
const files = [];
|
|
2298
2953
|
const seen = /* @__PURE__ */ new Set();
|
|
2299
2954
|
for (const pattern2 of CSS_FILE_PATTERNS) {
|
|
2300
|
-
const rootPath =
|
|
2955
|
+
const rootPath = join11(targetDir, pattern2);
|
|
2301
2956
|
addFileIfExists(rootPath, files, seen);
|
|
2302
2957
|
for (const dir of CSS_DIRS) {
|
|
2303
|
-
const dirPath =
|
|
2958
|
+
const dirPath = join11(targetDir, dir, pattern2);
|
|
2304
2959
|
addFileIfExists(dirPath, files, seen);
|
|
2305
2960
|
}
|
|
2306
2961
|
}
|
|
2307
2962
|
for (const dir of CSS_DIRS) {
|
|
2308
|
-
const dirPath =
|
|
2309
|
-
if (
|
|
2963
|
+
const dirPath = join11(targetDir, dir);
|
|
2964
|
+
if (existsSync9(dirPath) && statSync5(dirPath).isDirectory()) {
|
|
2310
2965
|
scanDirForCSS(dirPath, files, seen, 2);
|
|
2311
2966
|
}
|
|
2312
2967
|
}
|
|
@@ -2314,11 +2969,11 @@ function collectCSSFiles(targetDir) {
|
|
|
2314
2969
|
return files;
|
|
2315
2970
|
}
|
|
2316
2971
|
function addFileIfExists(filePath, files, seen) {
|
|
2317
|
-
const resolved =
|
|
2972
|
+
const resolved = resolve8(filePath);
|
|
2318
2973
|
if (seen.has(resolved)) return;
|
|
2319
|
-
if (!
|
|
2974
|
+
if (!existsSync9(resolved)) return;
|
|
2320
2975
|
try {
|
|
2321
|
-
const content =
|
|
2976
|
+
const content = readFileSync12(resolved, "utf-8");
|
|
2322
2977
|
if (content.includes("--")) {
|
|
2323
2978
|
files.push({ path: resolved, content });
|
|
2324
2979
|
seen.add(resolved);
|
|
@@ -2327,7 +2982,7 @@ function addFileIfExists(filePath, files, seen) {
|
|
|
2327
2982
|
}
|
|
2328
2983
|
}
|
|
2329
2984
|
function scanDirForCSS(dir, files, seen, maxDepth) {
|
|
2330
|
-
if (!
|
|
2985
|
+
if (!existsSync9(dir)) return;
|
|
2331
2986
|
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2332
2987
|
"node_modules",
|
|
2333
2988
|
".next",
|
|
@@ -2341,15 +2996,15 @@ function scanDirForCSS(dir, files, seen, maxDepth) {
|
|
|
2341
2996
|
".vercel"
|
|
2342
2997
|
]);
|
|
2343
2998
|
try {
|
|
2344
|
-
const entries =
|
|
2999
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
2345
3000
|
for (const entry of entries) {
|
|
2346
3001
|
if (entry.isDirectory()) {
|
|
2347
3002
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2348
3003
|
if (maxDepth > 0) {
|
|
2349
|
-
scanDirForCSS(
|
|
3004
|
+
scanDirForCSS(join11(dir, entry.name), files, seen, maxDepth - 1);
|
|
2350
3005
|
}
|
|
2351
|
-
} else if (entry.isFile() &&
|
|
2352
|
-
addFileIfExists(
|
|
3006
|
+
} else if (entry.isFile() && extname3(entry.name) === ".css") {
|
|
3007
|
+
addFileIfExists(join11(dir, entry.name), files, seen);
|
|
2353
3008
|
}
|
|
2354
3009
|
}
|
|
2355
3010
|
} catch {
|
|
@@ -2431,10 +3086,10 @@ function extractVarName(varExpr) {
|
|
|
2431
3086
|
function parseNextFontFromLayouts(targetDir) {
|
|
2432
3087
|
const fontMap = /* @__PURE__ */ new Map();
|
|
2433
3088
|
for (const relPath of LAYOUT_FILE_PATHS) {
|
|
2434
|
-
const fullPath =
|
|
2435
|
-
if (!
|
|
3089
|
+
const fullPath = join11(targetDir, relPath);
|
|
3090
|
+
if (!existsSync9(fullPath)) continue;
|
|
2436
3091
|
try {
|
|
2437
|
-
const content =
|
|
3092
|
+
const content = readFileSync12(fullPath, "utf-8");
|
|
2438
3093
|
parseNextFontDeclarations(content, fontMap);
|
|
2439
3094
|
} catch {
|
|
2440
3095
|
}
|
|
@@ -2481,7 +3136,7 @@ function parseNextFontDeclarations(content, fontMap) {
|
|
|
2481
3136
|
const srcMatch = block.match(/src\s*:\s*["']([^"']+)["']/);
|
|
2482
3137
|
if (srcMatch) {
|
|
2483
3138
|
const srcPath = srcMatch[1];
|
|
2484
|
-
const fileName =
|
|
3139
|
+
const fileName = basename2(srcPath, extname3(srcPath));
|
|
2485
3140
|
const fontBaseName = fileName.replace(/[-_](Variable|Regular|Bold|Light|Medium|SemiBold|ExtraBold|Thin|Black|Italic).*$/i, "").replace(/[-_]/g, " ").trim();
|
|
2486
3141
|
if (fontBaseName) {
|
|
2487
3142
|
fontMap.set(varName, fontBaseName);
|
|
@@ -2519,10 +3174,10 @@ var MONO_FONT_NAMES = /* @__PURE__ */ new Set([
|
|
|
2519
3174
|
"IBM Plex Mono"
|
|
2520
3175
|
]);
|
|
2521
3176
|
function extractFontHints(targetDir) {
|
|
2522
|
-
const pkgPath =
|
|
2523
|
-
if (!
|
|
3177
|
+
const pkgPath = join11(targetDir, "package.json");
|
|
3178
|
+
if (!existsSync9(pkgPath)) return void 0;
|
|
2524
3179
|
try {
|
|
2525
|
-
const pkg = JSON.parse(
|
|
3180
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
2526
3181
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2527
3182
|
const fonts2 = [];
|
|
2528
3183
|
for (const [dep, _] of Object.entries(allDeps)) {
|
|
@@ -2558,10 +3213,10 @@ function extractFontHints(targetDir) {
|
|
|
2558
3213
|
}
|
|
2559
3214
|
}
|
|
2560
3215
|
function inferThemeName(targetDir) {
|
|
2561
|
-
const pkgPath =
|
|
2562
|
-
if (
|
|
3216
|
+
const pkgPath = join11(targetDir, "package.json");
|
|
3217
|
+
if (existsSync9(pkgPath)) {
|
|
2563
3218
|
try {
|
|
2564
|
-
const pkg = JSON.parse(
|
|
3219
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
2565
3220
|
if (pkg.name) {
|
|
2566
3221
|
const name = pkg.name.replace(/^@[\w-]+\//, "");
|
|
2567
3222
|
return `${name}-theme`;
|
|
@@ -2569,7 +3224,7 @@ function inferThemeName(targetDir) {
|
|
|
2569
3224
|
} catch {
|
|
2570
3225
|
}
|
|
2571
3226
|
}
|
|
2572
|
-
return `${
|
|
3227
|
+
return `${basename2(targetDir)}-theme`;
|
|
2573
3228
|
}
|
|
2574
3229
|
function confidenceComment(confidence) {
|
|
2575
3230
|
return `# confidence: ${confidence}`;
|
|
@@ -2598,7 +3253,7 @@ function outputJSON(result, validationResult) {
|
|
|
2598
3253
|
}
|
|
2599
3254
|
function outputYAML(result, outputPath, cwd, validationResult) {
|
|
2600
3255
|
const yamlStr = buildAnnotatedYAML(result);
|
|
2601
|
-
const outFile =
|
|
3256
|
+
const outFile = resolve8(cwd, outputPath ?? ".visor.yaml");
|
|
2602
3257
|
const high = result.tokens.filter((t) => t.confidence === "high").length;
|
|
2603
3258
|
const med = result.tokens.filter((t) => t.confidence === "medium").length;
|
|
2604
3259
|
const low = result.tokens.filter((t) => t.confidence === "low").length;
|
|
@@ -2651,11 +3306,11 @@ function buildAnnotatedYAML(result) {
|
|
|
2651
3306
|
for (const token of result.tokens) {
|
|
2652
3307
|
confidenceMap.set(token.name, token.confidence);
|
|
2653
3308
|
}
|
|
2654
|
-
const
|
|
3309
|
+
const lines2 = baseYaml.split("\n");
|
|
2655
3310
|
const annotated = [];
|
|
2656
3311
|
let inColors = false;
|
|
2657
3312
|
let inColorsDark = false;
|
|
2658
|
-
for (const line of
|
|
3313
|
+
for (const line of lines2) {
|
|
2659
3314
|
if (/^colors:/.test(line)) {
|
|
2660
3315
|
inColors = true;
|
|
2661
3316
|
inColorsDark = false;
|
|
@@ -2684,14 +3339,15 @@ function buildAnnotatedYAML(result) {
|
|
|
2684
3339
|
}
|
|
2685
3340
|
|
|
2686
3341
|
// src/commands/theme-register.ts
|
|
2687
|
-
import { readFileSync as
|
|
2688
|
-
import { resolve as
|
|
3342
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync7, mkdirSync as mkdirSync4, existsSync as existsSync11 } from "fs";
|
|
3343
|
+
import { resolve as resolve10, join as join13 } from "path";
|
|
2689
3344
|
import { generateThemeData as generateThemeData3 } from "@loworbitstudio/visor-theme-engine";
|
|
2690
3345
|
import { docsAdapter as docsAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
2691
3346
|
|
|
2692
3347
|
// src/utils/theme-helpers.ts
|
|
2693
|
-
import { existsSync as
|
|
2694
|
-
import { resolve as
|
|
3348
|
+
import { existsSync as existsSync10, lstatSync, readdirSync as readdirSync5, readFileSync as readFileSync13, readlinkSync, realpathSync, statSync as statSync6 } from "fs";
|
|
3349
|
+
import { resolve as resolve9, dirname as dirname6, join as join12 } from "path";
|
|
3350
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2695
3351
|
function toSlug(name) {
|
|
2696
3352
|
return name.toLowerCase().replace(/\s+/g, "-");
|
|
2697
3353
|
}
|
|
@@ -2699,17 +3355,132 @@ function toLabel(name) {
|
|
|
2699
3355
|
return name.split(/[\s-]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2700
3356
|
}
|
|
2701
3357
|
function findRepoRoot(startDir) {
|
|
2702
|
-
let current =
|
|
3358
|
+
let current = resolve9(startDir);
|
|
2703
3359
|
while (true) {
|
|
2704
|
-
if (
|
|
3360
|
+
if (existsSync10(join12(current, "packages", "docs"))) {
|
|
2705
3361
|
return current;
|
|
2706
3362
|
}
|
|
2707
|
-
const parent =
|
|
3363
|
+
const parent = dirname6(current);
|
|
2708
3364
|
if (parent === current) break;
|
|
2709
3365
|
current = parent;
|
|
2710
3366
|
}
|
|
2711
3367
|
return null;
|
|
2712
3368
|
}
|
|
3369
|
+
function findMainRepoRoot(startDir) {
|
|
3370
|
+
try {
|
|
3371
|
+
const commonDir = execFileSync3("git", ["rev-parse", "--git-common-dir"], {
|
|
3372
|
+
cwd: startDir,
|
|
3373
|
+
encoding: "utf-8",
|
|
3374
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3375
|
+
}).trim();
|
|
3376
|
+
if (commonDir) {
|
|
3377
|
+
const absoluteCommonDir = resolve9(startDir, commonDir);
|
|
3378
|
+
const candidate = dirname6(absoluteCommonDir);
|
|
3379
|
+
if (existsSync10(join12(candidate, "packages", "docs"))) {
|
|
3380
|
+
return candidate;
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
} catch {
|
|
3384
|
+
}
|
|
3385
|
+
return findRepoRoot(startDir);
|
|
3386
|
+
}
|
|
3387
|
+
var BrokenSymlinkError = class extends Error {
|
|
3388
|
+
constructor(path2, target) {
|
|
3389
|
+
super(`Broken symlink: ${path2} \u2192 ${target}`);
|
|
3390
|
+
this.path = path2;
|
|
3391
|
+
this.target = target;
|
|
3392
|
+
}
|
|
3393
|
+
path;
|
|
3394
|
+
target;
|
|
3395
|
+
code = "BROKEN_SYMLINK";
|
|
3396
|
+
};
|
|
3397
|
+
function assertNoBrokenSymlinks(dir) {
|
|
3398
|
+
if (!existsSync10(dir)) return;
|
|
3399
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
3400
|
+
for (const entry of entries) {
|
|
3401
|
+
const entryPath = join12(dir, entry.name);
|
|
3402
|
+
let lst;
|
|
3403
|
+
try {
|
|
3404
|
+
lst = lstatSync(entryPath);
|
|
3405
|
+
} catch {
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
if (lst.isSymbolicLink()) {
|
|
3409
|
+
const target = readlinkSync(entryPath);
|
|
3410
|
+
try {
|
|
3411
|
+
statSync6(entryPath);
|
|
3412
|
+
} catch {
|
|
3413
|
+
throw new BrokenSymlinkError(entryPath, target);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
function scanParentForPrivateThemes(parentDir) {
|
|
3419
|
+
if (!existsSync10(parentDir)) return [];
|
|
3420
|
+
const entries = readdirSync5(parentDir, { withFileTypes: true });
|
|
3421
|
+
const matches = [];
|
|
3422
|
+
for (const entry of entries) {
|
|
3423
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3424
|
+
const candidate = join12(parentDir, entry.name, "visor-themes-private", "themes");
|
|
3425
|
+
if (existsSync10(candidate)) {
|
|
3426
|
+
matches.push(candidate);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
return matches.sort();
|
|
3430
|
+
}
|
|
3431
|
+
function scanNestedThemeDir(dir) {
|
|
3432
|
+
if (!existsSync10(dir)) return [];
|
|
3433
|
+
assertNoBrokenSymlinks(dir);
|
|
3434
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
3435
|
+
const out = [];
|
|
3436
|
+
for (const entry of entries) {
|
|
3437
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
3438
|
+
const themeFile = join12(dir, entry.name, "theme.visor.yaml");
|
|
3439
|
+
if (existsSync10(themeFile)) {
|
|
3440
|
+
out.push({ filePath: themeFile, slug: entry.name });
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return out;
|
|
3444
|
+
}
|
|
3445
|
+
function detectVisorWorkspace(cwd) {
|
|
3446
|
+
let current = resolve9(cwd);
|
|
3447
|
+
while (true) {
|
|
3448
|
+
const pkgPath = join12(current, "package.json");
|
|
3449
|
+
if (existsSync10(pkgPath)) {
|
|
3450
|
+
try {
|
|
3451
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
3452
|
+
const workspaces = pkg.workspaces ?? [];
|
|
3453
|
+
const hasCli = workspaces.some((w) => w.includes("packages/cli"));
|
|
3454
|
+
const hasEngine = workspaces.some((w) => w.includes("packages/theme-engine"));
|
|
3455
|
+
if (pkg.name === "visor" && hasCli && hasEngine) {
|
|
3456
|
+
return current;
|
|
3457
|
+
}
|
|
3458
|
+
} catch {
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
const parent = dirname6(current);
|
|
3462
|
+
if (parent === current) break;
|
|
3463
|
+
current = parent;
|
|
3464
|
+
}
|
|
3465
|
+
return null;
|
|
3466
|
+
}
|
|
3467
|
+
function isLocalVisorBinary(workspaceRoot, scriptPath) {
|
|
3468
|
+
if (!scriptPath) return false;
|
|
3469
|
+
const expectedPrefix = join12(workspaceRoot, "packages", "cli", "dist");
|
|
3470
|
+
let resolvedScript;
|
|
3471
|
+
let resolvedPrefix;
|
|
3472
|
+
try {
|
|
3473
|
+
resolvedScript = realpathSync(scriptPath);
|
|
3474
|
+
} catch {
|
|
3475
|
+
resolvedScript = resolve9(scriptPath);
|
|
3476
|
+
}
|
|
3477
|
+
try {
|
|
3478
|
+
resolvedPrefix = realpathSync(expectedPrefix);
|
|
3479
|
+
} catch {
|
|
3480
|
+
resolvedPrefix = resolve9(expectedPrefix);
|
|
3481
|
+
}
|
|
3482
|
+
return resolvedScript.startsWith(resolvedPrefix + "/") || resolvedScript === resolvedPrefix;
|
|
3483
|
+
}
|
|
2713
3484
|
|
|
2714
3485
|
// src/commands/theme-register.ts
|
|
2715
3486
|
function insertGlobalsImport(content, slug2) {
|
|
@@ -2717,32 +3488,32 @@ function insertGlobalsImport(content, slug2) {
|
|
|
2717
3488
|
if (content.includes(importLine)) {
|
|
2718
3489
|
return { updated: content, changed: false };
|
|
2719
3490
|
}
|
|
2720
|
-
const
|
|
3491
|
+
const lines2 = content.split("\n");
|
|
2721
3492
|
const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';/;
|
|
2722
3493
|
const themeImportIndices = [];
|
|
2723
|
-
for (let i = 0; i <
|
|
2724
|
-
if (themeImportPattern.test(
|
|
3494
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
3495
|
+
if (themeImportPattern.test(lines2[i])) {
|
|
2725
3496
|
themeImportIndices.push(i);
|
|
2726
3497
|
}
|
|
2727
3498
|
}
|
|
2728
3499
|
if (themeImportIndices.length === 0) {
|
|
2729
|
-
const lastImportIdx =
|
|
3500
|
+
const lastImportIdx = lines2.reduce(
|
|
2730
3501
|
(last, line, i) => line.startsWith("@import") ? i : last,
|
|
2731
3502
|
-1
|
|
2732
3503
|
);
|
|
2733
3504
|
const insertAt2 = lastImportIdx + 1;
|
|
2734
|
-
|
|
2735
|
-
return { updated:
|
|
3505
|
+
lines2.splice(insertAt2, 0, importLine);
|
|
3506
|
+
return { updated: lines2.join("\n"), changed: true };
|
|
2736
3507
|
}
|
|
2737
3508
|
let insertAt = themeImportIndices[themeImportIndices.length - 1] + 1;
|
|
2738
3509
|
for (const idx of themeImportIndices) {
|
|
2739
|
-
if (importLine <
|
|
3510
|
+
if (importLine < lines2[idx]) {
|
|
2740
3511
|
insertAt = idx;
|
|
2741
3512
|
break;
|
|
2742
3513
|
}
|
|
2743
3514
|
}
|
|
2744
|
-
|
|
2745
|
-
return { updated:
|
|
3515
|
+
lines2.splice(insertAt, 0, importLine);
|
|
3516
|
+
return { updated: lines2.join("\n"), changed: true };
|
|
2746
3517
|
}
|
|
2747
3518
|
function insertThemeConfig(content, slug2, label, group) {
|
|
2748
3519
|
if (content.includes(`value: "${slug2}"`)) {
|
|
@@ -2796,10 +3567,10 @@ ${indent}${newEntry},
|
|
|
2796
3567
|
return { updated, changed: true };
|
|
2797
3568
|
}
|
|
2798
3569
|
function themeRegisterCommand(file, cwd, options) {
|
|
2799
|
-
const filePath =
|
|
3570
|
+
const filePath = resolve10(cwd, file);
|
|
2800
3571
|
let yamlContent;
|
|
2801
3572
|
try {
|
|
2802
|
-
yamlContent =
|
|
3573
|
+
yamlContent = readFileSync14(filePath, "utf-8");
|
|
2803
3574
|
} catch {
|
|
2804
3575
|
if (options.json) {
|
|
2805
3576
|
console.log(JSON.stringify({ success: false, error: `Could not read file: ${filePath}` }));
|
|
@@ -2842,11 +3613,11 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2842
3613
|
process.exit(1);
|
|
2843
3614
|
return;
|
|
2844
3615
|
}
|
|
2845
|
-
const docsAppDir =
|
|
2846
|
-
const cssFilePath =
|
|
2847
|
-
const globalsPath =
|
|
2848
|
-
const themeConfigPath =
|
|
2849
|
-
if (!
|
|
3616
|
+
const docsAppDir = join13(repoRoot, "packages", "docs", "app");
|
|
3617
|
+
const cssFilePath = join13(docsAppDir, `${slug2}-theme.css`);
|
|
3618
|
+
const globalsPath = join13(docsAppDir, "globals.css");
|
|
3619
|
+
const themeConfigPath = join13(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
3620
|
+
if (!existsSync11(docsAppDir)) {
|
|
2850
3621
|
const msg = `Docs app directory not found: ${docsAppDir}`;
|
|
2851
3622
|
if (options.json) {
|
|
2852
3623
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
@@ -2859,8 +3630,8 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2859
3630
|
let globalsContent = "";
|
|
2860
3631
|
let themeConfigContent = "";
|
|
2861
3632
|
try {
|
|
2862
|
-
globalsContent =
|
|
2863
|
-
themeConfigContent =
|
|
3633
|
+
globalsContent = readFileSync14(globalsPath, "utf-8");
|
|
3634
|
+
themeConfigContent = readFileSync14(themeConfigPath, "utf-8");
|
|
2864
3635
|
} catch (err) {
|
|
2865
3636
|
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
2866
3637
|
if (options.json) {
|
|
@@ -2871,8 +3642,8 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2871
3642
|
process.exit(1);
|
|
2872
3643
|
return;
|
|
2873
3644
|
}
|
|
2874
|
-
const cssExists =
|
|
2875
|
-
const cssChanged = !cssExists ||
|
|
3645
|
+
const cssExists = existsSync11(cssFilePath);
|
|
3646
|
+
const cssChanged = !cssExists || readFileSync14(cssFilePath, "utf-8") !== css;
|
|
2876
3647
|
const { updated: newGlobals, changed: globalsChanged } = insertGlobalsImport(globalsContent, slug2);
|
|
2877
3648
|
const { updated: newThemeConfig, changed: themeConfigChanged, error: configError } = insertThemeConfig(
|
|
2878
3649
|
themeConfigContent,
|
|
@@ -2956,8 +3727,8 @@ function themeRegisterCommand(file, cwd, options) {
|
|
|
2956
3727
|
}
|
|
2957
3728
|
|
|
2958
3729
|
// src/commands/theme-unregister.ts
|
|
2959
|
-
import { readFileSync as
|
|
2960
|
-
import { join as
|
|
3730
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync12, unlinkSync } from "fs";
|
|
3731
|
+
import { join as join14 } from "path";
|
|
2961
3732
|
function removeGlobalsImport(content, slug2) {
|
|
2962
3733
|
const importLine = `@import './${slug2}-theme.css';`;
|
|
2963
3734
|
if (!content.includes(importLine)) {
|
|
@@ -2989,11 +3760,11 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
2989
3760
|
process.exit(1);
|
|
2990
3761
|
return;
|
|
2991
3762
|
}
|
|
2992
|
-
const docsAppDir =
|
|
2993
|
-
const cssFilePath =
|
|
2994
|
-
const globalsPath =
|
|
2995
|
-
const themeConfigPath =
|
|
2996
|
-
if (!
|
|
3763
|
+
const docsAppDir = join14(repoRoot, "packages", "docs", "app");
|
|
3764
|
+
const cssFilePath = join14(docsAppDir, `${slug2}-theme.css`);
|
|
3765
|
+
const globalsPath = join14(docsAppDir, "globals.css");
|
|
3766
|
+
const themeConfigPath = join14(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
3767
|
+
if (!existsSync12(docsAppDir)) {
|
|
2997
3768
|
const msg = `Docs app directory not found: ${docsAppDir}`;
|
|
2998
3769
|
if (options.json) {
|
|
2999
3770
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
@@ -3006,8 +3777,8 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3006
3777
|
let globalsContent = "";
|
|
3007
3778
|
let themeConfigContent = "";
|
|
3008
3779
|
try {
|
|
3009
|
-
globalsContent =
|
|
3010
|
-
themeConfigContent =
|
|
3780
|
+
globalsContent = readFileSync15(globalsPath, "utf-8");
|
|
3781
|
+
themeConfigContent = readFileSync15(themeConfigPath, "utf-8");
|
|
3011
3782
|
} catch (err) {
|
|
3012
3783
|
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
3013
3784
|
if (options.json) {
|
|
@@ -3018,7 +3789,7 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3018
3789
|
process.exit(1);
|
|
3019
3790
|
return;
|
|
3020
3791
|
}
|
|
3021
|
-
const cssExists =
|
|
3792
|
+
const cssExists = existsSync12(cssFilePath);
|
|
3022
3793
|
const { updated: newGlobals, changed: globalsChanged } = removeGlobalsImport(globalsContent, slug2);
|
|
3023
3794
|
const { updated: newThemeConfig, changed: themeConfigChanged } = removeThemeConfigEntry(themeConfigContent, slug2);
|
|
3024
3795
|
if (!cssExists && !globalsChanged && !themeConfigChanged) {
|
|
@@ -3059,30 +3830,150 @@ function themeUnregisterCommand(slug2, cwd, options) {
|
|
|
3059
3830
|
|
|
3060
3831
|
// src/commands/theme-sync.ts
|
|
3061
3832
|
import {
|
|
3062
|
-
readFileSync as
|
|
3833
|
+
readFileSync as readFileSync16,
|
|
3063
3834
|
writeFileSync as writeFileSync9,
|
|
3064
3835
|
mkdirSync as mkdirSync5,
|
|
3065
|
-
existsSync as
|
|
3066
|
-
readdirSync as
|
|
3836
|
+
existsSync as existsSync13,
|
|
3837
|
+
readdirSync as readdirSync6,
|
|
3067
3838
|
unlinkSync as unlinkSync2,
|
|
3068
3839
|
copyFileSync
|
|
3069
3840
|
} from "fs";
|
|
3070
|
-
import { join as
|
|
3841
|
+
import { join as join15, basename as basename3, resolve as resolve11, sep } from "path";
|
|
3071
3842
|
import { parse as parseYaml2 } from "yaml";
|
|
3072
3843
|
import { generateThemeData as generateThemeData4 } from "@loworbitstudio/visor-theme-engine";
|
|
3073
3844
|
import { docsAdapter as docsAdapter3 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
3845
|
+
var PRIVATE_THEMES_REPO_URL = "git@github.com:low-orbit-studio/visor-themes-private.git";
|
|
3846
|
+
var PRIVATE_THEMES_ENV_VAR = "VISOR_THEMES_PRIVATE_PATH";
|
|
3074
3847
|
var GLOBALS_BEGIN_MARKER = "/* BEGIN visor-theme-imports \u2014 managed by `visor theme sync` */";
|
|
3075
3848
|
var GLOBALS_END_MARKER = "/* END visor-theme-imports */";
|
|
3076
3849
|
var STOCK_GROUPS_BEGIN_MARKER = "/* BEGIN visor-stock-themes \u2014 managed by `visor theme sync` */";
|
|
3077
3850
|
var STOCK_GROUPS_END_MARKER = "/* END visor-stock-themes */";
|
|
3078
|
-
var GITIGNORE_BEGIN_MARKER = "# BEGIN visor-custom-theme-css (managed by `visor theme sync` \u2014 do not edit manually)";
|
|
3079
|
-
var GITIGNORE_END_MARKER = "# END visor-custom-theme-css";
|
|
3080
3851
|
var CUSTOM_OVERLAY_CSS_PATH = "packages/docs/app/custom-themes.generated.css";
|
|
3081
3852
|
var CUSTOM_OVERLAY_TS_PATH = "packages/docs/lib/theme-config.custom.generated.ts";
|
|
3082
3853
|
var CUSTOM_OVERLAY_IMPORT_LINE = "@import './custom-themes.generated.css';";
|
|
3083
3854
|
function scanThemeDir(dir) {
|
|
3084
|
-
if (!
|
|
3085
|
-
|
|
3855
|
+
if (!existsSync13(dir)) return [];
|
|
3856
|
+
assertNoBrokenSymlinks(dir);
|
|
3857
|
+
return readdirSync6(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join15(dir, f));
|
|
3858
|
+
}
|
|
3859
|
+
function resolveCustomSources(repoRoot, mainRepoRoot, warn) {
|
|
3860
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3861
|
+
const deprecationWarnings = [];
|
|
3862
|
+
const addNested = (dir, origin) => {
|
|
3863
|
+
const entries = scanNestedThemeDir(dir);
|
|
3864
|
+
for (const entry of entries) {
|
|
3865
|
+
const existing = merged.get(entry.slug);
|
|
3866
|
+
if (existing) {
|
|
3867
|
+
warn(
|
|
3868
|
+
`Duplicate theme slug "${entry.slug}" \u2014 keeping ${existing.origin} source (${existing.filePath}); ignoring ${origin} source (${entry.filePath}).`
|
|
3869
|
+
);
|
|
3870
|
+
continue;
|
|
3871
|
+
}
|
|
3872
|
+
merged.set(entry.slug, {
|
|
3873
|
+
filePath: entry.filePath,
|
|
3874
|
+
slug: entry.slug,
|
|
3875
|
+
origin
|
|
3876
|
+
});
|
|
3877
|
+
}
|
|
3878
|
+
};
|
|
3879
|
+
const envPath = process.env[PRIVATE_THEMES_ENV_VAR];
|
|
3880
|
+
if (envPath && envPath.trim() !== "") {
|
|
3881
|
+
const resolved = resolve11(envPath);
|
|
3882
|
+
if (!existsSync13(resolved)) {
|
|
3883
|
+
throw new Error(
|
|
3884
|
+
`${PRIVATE_THEMES_ENV_VAR} is set to "${envPath}" but the path does not exist. Expected a directory containing {slug}/theme.visor.yaml entries.`
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
addNested(resolved, "env");
|
|
3888
|
+
}
|
|
3889
|
+
const siblingPath = join15(mainRepoRoot, "..", "visor-themes-private", "themes");
|
|
3890
|
+
const siblingExists = existsSync13(siblingPath);
|
|
3891
|
+
if (siblingExists) {
|
|
3892
|
+
addNested(siblingPath, "sibling");
|
|
3893
|
+
}
|
|
3894
|
+
const parentDir = join15(mainRepoRoot, "..");
|
|
3895
|
+
const parentGlobMatches = scanParentForPrivateThemes(parentDir).filter(
|
|
3896
|
+
(path2) => !path2.startsWith(mainRepoRoot + sep)
|
|
3897
|
+
);
|
|
3898
|
+
if (parentGlobMatches.length > 0) {
|
|
3899
|
+
if (siblingExists) {
|
|
3900
|
+
for (const path2 of parentGlobMatches) {
|
|
3901
|
+
warn(
|
|
3902
|
+
`Found one-level-deeper theme source ${path2} but using true-sibling at ${siblingPath} (preferred per BO-29 D2).`
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
} else {
|
|
3906
|
+
const [first, ...rest] = parentGlobMatches;
|
|
3907
|
+
addNested(first, "parent-glob");
|
|
3908
|
+
for (const other of rest) {
|
|
3909
|
+
warn(
|
|
3910
|
+
`Multiple one-level-deeper theme sources found; using ${first} and ignoring ${other}. Set ${PRIVATE_THEMES_ENV_VAR} to override.`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
const legacyDir = join15(repoRoot, "custom-themes");
|
|
3916
|
+
const legacyFiles = scanThemeDir(legacyDir);
|
|
3917
|
+
for (const legacyFile of legacyFiles) {
|
|
3918
|
+
const slug2 = basename3(legacyFile).replace(/\.visor\.yaml$/, "");
|
|
3919
|
+
const existing = merged.get(slug2);
|
|
3920
|
+
if (existing) {
|
|
3921
|
+
warn(
|
|
3922
|
+
`Duplicate theme slug "${slug2}" \u2014 keeping ${existing.origin} source (${existing.filePath}); ignoring legacy source (${legacyFile}).`
|
|
3923
|
+
);
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3926
|
+
deprecationWarnings.push(
|
|
3927
|
+
`Deprecated legacy custom-themes/ source: ${legacyFile} \u2014 migrate to visor-themes-private (see docs).`
|
|
3928
|
+
);
|
|
3929
|
+
merged.set(slug2, {
|
|
3930
|
+
filePath: legacyFile,
|
|
3931
|
+
slug: slug2,
|
|
3932
|
+
origin: "legacy"
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
return { files: [...merged.values()], deprecationWarnings };
|
|
3936
|
+
}
|
|
3937
|
+
function reportBrokenSymlink(err, options) {
|
|
3938
|
+
const msg = `Broken symlink in theme source: ${err.path} \u2192 ${err.target}`;
|
|
3939
|
+
if (options.json) {
|
|
3940
|
+
console.log(JSON.stringify({ success: false, error: msg, path: err.path, target: err.target }));
|
|
3941
|
+
} else {
|
|
3942
|
+
logger.error(msg);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
function buildEmptySourcesMessage(mainRepoRoot) {
|
|
3946
|
+
const expectedSibling = join15(mainRepoRoot, "..", "visor-themes-private");
|
|
3947
|
+
return [
|
|
3948
|
+
"No theme sources discovered. Cannot proceed \u2014 refusing to wipe generated CSS.",
|
|
3949
|
+
"",
|
|
3950
|
+
"Resolution order checked:",
|
|
3951
|
+
` 1. Env var ${PRIVATE_THEMES_ENV_VAR} (unset or empty)`,
|
|
3952
|
+
` 2. Sibling checkout at ${expectedSibling}/themes/ (not found)`,
|
|
3953
|
+
` 3. One-level-deeper at ${join15(mainRepoRoot, "..")}/<parent>/visor-themes-private/themes/ (not found)`,
|
|
3954
|
+
" 4. Legacy custom-themes/ (no .visor.yaml files)",
|
|
3955
|
+
"",
|
|
3956
|
+
"To fix, clone the private themes repo as a sibling:",
|
|
3957
|
+
` git clone ${PRIVATE_THEMES_REPO_URL} ${expectedSibling}`,
|
|
3958
|
+
"",
|
|
3959
|
+
`Or set ${PRIVATE_THEMES_ENV_VAR} to a directory containing {slug}/theme.visor.yaml entries.`
|
|
3960
|
+
].join("\n");
|
|
3961
|
+
}
|
|
3962
|
+
function enforceWorkspaceGuard(cwd) {
|
|
3963
|
+
if (process.env.VITEST) return null;
|
|
3964
|
+
if (process.env.VISOR_SKIP_WORKSPACE_GUARD) return null;
|
|
3965
|
+
const workspaceRoot = detectVisorWorkspace(cwd);
|
|
3966
|
+
if (!workspaceRoot) return null;
|
|
3967
|
+
if (isLocalVisorBinary(workspaceRoot, process.argv[1])) return null;
|
|
3968
|
+
return [
|
|
3969
|
+
`Detected Visor workspace at ${workspaceRoot}.`,
|
|
3970
|
+
"The global `visor` CLI bundles a published theme-engine that lags HEAD and",
|
|
3971
|
+
"can regress stock theme CSS files. Run the workspace command instead:",
|
|
3972
|
+
"",
|
|
3973
|
+
" npm run theme:sync",
|
|
3974
|
+
"",
|
|
3975
|
+
`(Override with VISOR_SKIP_WORKSPACE_GUARD=1 if you really know what you're doing.)`
|
|
3976
|
+
].join("\n");
|
|
3086
3977
|
}
|
|
3087
3978
|
function extractGroup(yamlContent) {
|
|
3088
3979
|
const parsed = parseYaml2(yamlContent);
|
|
@@ -3119,28 +4010,28 @@ ${GLOBALS_END_MARKER}`;
|
|
|
3119
4010
|
updated = content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GLOBALS_END_MARKER.length);
|
|
3120
4011
|
} else {
|
|
3121
4012
|
const themeImportPattern = /^@import '\.\/[\w-]+-theme\.css';\n?/gm;
|
|
3122
|
-
const
|
|
4013
|
+
const lines2 = content.split("\n");
|
|
3123
4014
|
let firstThemeIdx = -1;
|
|
3124
4015
|
let lastThemeIdx = -1;
|
|
3125
|
-
for (let i = 0; i <
|
|
3126
|
-
if (/^@import '\.\/[\w-]+-theme\.css';/.test(
|
|
4016
|
+
for (let i = 0; i < lines2.length; i++) {
|
|
4017
|
+
if (/^@import '\.\/[\w-]+-theme\.css';/.test(lines2[i])) {
|
|
3127
4018
|
if (firstThemeIdx === -1) firstThemeIdx = i;
|
|
3128
4019
|
lastThemeIdx = i;
|
|
3129
4020
|
}
|
|
3130
4021
|
}
|
|
3131
4022
|
if (firstThemeIdx !== -1) {
|
|
3132
|
-
const before =
|
|
3133
|
-
const after =
|
|
4023
|
+
const before = lines2.slice(0, firstThemeIdx);
|
|
4024
|
+
const after = lines2.slice(lastThemeIdx + 1);
|
|
3134
4025
|
updated = [...before, newBlock, ...after].join("\n");
|
|
3135
4026
|
} else {
|
|
3136
4027
|
void themeImportPattern;
|
|
3137
|
-
const lastImportIdx =
|
|
4028
|
+
const lastImportIdx = lines2.reduce(
|
|
3138
4029
|
(last, line, i) => line.startsWith("@import") ? i : last,
|
|
3139
4030
|
-1
|
|
3140
4031
|
);
|
|
3141
4032
|
const insertAt = lastImportIdx + 1;
|
|
3142
|
-
|
|
3143
|
-
updated =
|
|
4033
|
+
lines2.splice(insertAt, 0, newBlock);
|
|
4034
|
+
updated = lines2.join("\n");
|
|
3144
4035
|
}
|
|
3145
4036
|
}
|
|
3146
4037
|
updated = ensureCustomOverlayImport(updated);
|
|
@@ -3241,19 +4132,17 @@ ${groupsTs}
|
|
|
3241
4132
|
];
|
|
3242
4133
|
`;
|
|
3243
4134
|
}
|
|
3244
|
-
function updateGitignoreBlock(content, customSlugs) {
|
|
3245
|
-
const cssLines = customSlugs.sort().map((slug2) => `packages/docs/app/${slug2}-theme.css`).join("\n");
|
|
3246
|
-
const newBlock = `${GITIGNORE_BEGIN_MARKER}
|
|
3247
|
-
${cssLines}
|
|
3248
|
-
${GITIGNORE_END_MARKER}`;
|
|
3249
|
-
const beginIdx = content.indexOf(GITIGNORE_BEGIN_MARKER);
|
|
3250
|
-
const endIdx = content.indexOf(GITIGNORE_END_MARKER);
|
|
3251
|
-
if (beginIdx !== -1 && endIdx !== -1) {
|
|
3252
|
-
return content.slice(0, beginIdx) + newBlock + content.slice(endIdx + GITIGNORE_END_MARKER.length);
|
|
3253
|
-
}
|
|
3254
|
-
return content.trimEnd() + "\n\n" + newBlock + "\n";
|
|
3255
|
-
}
|
|
3256
4135
|
function themeSyncCommand(cwd, options) {
|
|
4136
|
+
const guardError = enforceWorkspaceGuard(cwd);
|
|
4137
|
+
if (guardError) {
|
|
4138
|
+
if (options.json) {
|
|
4139
|
+
console.log(JSON.stringify({ success: false, error: guardError }));
|
|
4140
|
+
} else {
|
|
4141
|
+
logger.error(guardError);
|
|
4142
|
+
}
|
|
4143
|
+
process.exit(1);
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
3257
4146
|
const repoRoot = findRepoRoot(cwd);
|
|
3258
4147
|
if (!repoRoot) {
|
|
3259
4148
|
const msg = "Could not locate repo root (packages/docs/ not found). Run from within the visor repo.";
|
|
@@ -3265,33 +4154,72 @@ function themeSyncCommand(cwd, options) {
|
|
|
3265
4154
|
process.exit(1);
|
|
3266
4155
|
return;
|
|
3267
4156
|
}
|
|
3268
|
-
const
|
|
3269
|
-
const
|
|
3270
|
-
const docsAppDir =
|
|
3271
|
-
const docsLibDir =
|
|
3272
|
-
const docsPublicThemesDir =
|
|
3273
|
-
const themeConfigPath =
|
|
3274
|
-
const globalsPath =
|
|
3275
|
-
const
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
4157
|
+
const mainRepoRoot = findMainRepoRoot(cwd) ?? repoRoot;
|
|
4158
|
+
const themesDir = join15(repoRoot, "themes");
|
|
4159
|
+
const docsAppDir = join15(repoRoot, "packages", "docs", "app");
|
|
4160
|
+
const docsLibDir = join15(repoRoot, "packages", "docs", "lib");
|
|
4161
|
+
const docsPublicThemesDir = join15(repoRoot, "packages", "docs", "public", "themes");
|
|
4162
|
+
const themeConfigPath = join15(repoRoot, "packages", "docs", "lib", "theme-config.ts");
|
|
4163
|
+
const globalsPath = join15(docsAppDir, "globals.css");
|
|
4164
|
+
const customOverlayCssPath = join15(repoRoot, CUSTOM_OVERLAY_CSS_PATH);
|
|
4165
|
+
const customOverlayTsPath = join15(repoRoot, CUSTOM_OVERLAY_TS_PATH);
|
|
4166
|
+
let stockFiles;
|
|
4167
|
+
try {
|
|
4168
|
+
stockFiles = scanThemeDir(themesDir);
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
if (err instanceof BrokenSymlinkError) {
|
|
4171
|
+
reportBrokenSymlink(err, options);
|
|
4172
|
+
process.exit(1);
|
|
4173
|
+
return;
|
|
4174
|
+
}
|
|
4175
|
+
throw err;
|
|
4176
|
+
}
|
|
4177
|
+
let customSources = [];
|
|
4178
|
+
let deprecationWarnings = [];
|
|
4179
|
+
const discoveryWarnings = [];
|
|
4180
|
+
try {
|
|
4181
|
+
const result = resolveCustomSources(
|
|
4182
|
+
repoRoot,
|
|
4183
|
+
mainRepoRoot,
|
|
4184
|
+
(msg) => discoveryWarnings.push(msg)
|
|
4185
|
+
);
|
|
4186
|
+
customSources = result.files;
|
|
4187
|
+
deprecationWarnings = result.deprecationWarnings;
|
|
4188
|
+
} catch (err) {
|
|
4189
|
+
if (err instanceof BrokenSymlinkError) {
|
|
4190
|
+
reportBrokenSymlink(err, options);
|
|
4191
|
+
process.exit(1);
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const msg = err instanceof Error ? err.message : "Custom theme discovery failed";
|
|
3282
4195
|
if (options.json) {
|
|
3283
4196
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
3284
4197
|
} else {
|
|
3285
|
-
logger.
|
|
4198
|
+
logger.error(msg);
|
|
4199
|
+
}
|
|
4200
|
+
process.exit(1);
|
|
4201
|
+
return;
|
|
4202
|
+
}
|
|
4203
|
+
if (stockFiles.length === 0 && customSources.length === 0) {
|
|
4204
|
+
const msg = buildEmptySourcesMessage(mainRepoRoot);
|
|
4205
|
+
if (options.json) {
|
|
4206
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
4207
|
+
} else {
|
|
4208
|
+
logger.error(msg);
|
|
3286
4209
|
}
|
|
4210
|
+
process.exit(1);
|
|
3287
4211
|
return;
|
|
3288
4212
|
}
|
|
4213
|
+
if (!options.json) {
|
|
4214
|
+
for (const w of deprecationWarnings) logger.warn(w);
|
|
4215
|
+
for (const w of discoveryWarnings) logger.warn(w);
|
|
4216
|
+
}
|
|
3289
4217
|
const manifest = [];
|
|
3290
4218
|
const errors = [];
|
|
3291
|
-
const processFile = (filePath, isCustom) => {
|
|
4219
|
+
const processFile = (filePath, isCustom, slugOverride) => {
|
|
3292
4220
|
let yamlContent;
|
|
3293
4221
|
try {
|
|
3294
|
-
yamlContent =
|
|
4222
|
+
yamlContent = readFileSync16(filePath, "utf-8");
|
|
3295
4223
|
} catch {
|
|
3296
4224
|
errors.push(`Could not read: ${filePath}`);
|
|
3297
4225
|
return;
|
|
@@ -3300,19 +4228,22 @@ function themeSyncCommand(cwd, options) {
|
|
|
3300
4228
|
try {
|
|
3301
4229
|
data = generateThemeData4(yamlContent);
|
|
3302
4230
|
} catch (err) {
|
|
3303
|
-
errors.push(`Failed to parse ${
|
|
4231
|
+
errors.push(`Failed to parse ${basename3(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
3304
4232
|
return;
|
|
3305
4233
|
}
|
|
3306
|
-
const slug2 = toSlug(data.config.name);
|
|
4234
|
+
const slug2 = slugOverride ?? toSlug(data.config.name);
|
|
3307
4235
|
const label = extractLabel(yamlContent) ?? toLabel(data.config.name);
|
|
3308
4236
|
const group = extractGroup(yamlContent) ?? (isCustom ? "Custom" : "Visor");
|
|
3309
4237
|
const defaultMode = extractDefaultMode(yamlContent);
|
|
3310
4238
|
const css = docsAdapter3({ primitives: data.primitives, tokens: data.tokens, config: data.config });
|
|
3311
|
-
const yamlFilename =
|
|
4239
|
+
const yamlFilename = slugOverride ?? basename3(filePath).replace(/\.visor\.yaml$/, "");
|
|
3312
4240
|
manifest.push({ slug: slug2, label, group, defaultMode, css, yamlFilename, isCustom });
|
|
3313
4241
|
};
|
|
3314
4242
|
for (const f of stockFiles) processFile(f, false);
|
|
3315
|
-
for (const
|
|
4243
|
+
for (const c of customSources) {
|
|
4244
|
+
const isNested = c.origin !== "legacy";
|
|
4245
|
+
processFile(c.filePath, true, isNested ? c.slug : void 0);
|
|
4246
|
+
}
|
|
3316
4247
|
if (errors.length > 0) {
|
|
3317
4248
|
if (options.json) {
|
|
3318
4249
|
console.log(JSON.stringify({ success: false, errors }));
|
|
@@ -3325,15 +4256,12 @@ function themeSyncCommand(cwd, options) {
|
|
|
3325
4256
|
const stockManifest = manifest.filter((e) => !e.isCustom);
|
|
3326
4257
|
const customManifest = manifest.filter((e) => e.isCustom);
|
|
3327
4258
|
const stockSlugs = stockManifest.map((e) => e.slug);
|
|
3328
|
-
const customSlugs = customManifest.map((e) => e.slug);
|
|
3329
4259
|
const allSlugs = manifest.map((e) => e.slug);
|
|
3330
4260
|
let globalsContent;
|
|
3331
4261
|
let themeConfigContent;
|
|
3332
|
-
let gitignoreContent;
|
|
3333
4262
|
try {
|
|
3334
|
-
globalsContent =
|
|
3335
|
-
themeConfigContent =
|
|
3336
|
-
gitignoreContent = existsSync11(gitignorePath) ? readFileSync14(gitignorePath, "utf-8") : "";
|
|
4263
|
+
globalsContent = readFileSync16(globalsPath, "utf-8");
|
|
4264
|
+
themeConfigContent = readFileSync16(themeConfigPath, "utf-8");
|
|
3337
4265
|
} catch (err) {
|
|
3338
4266
|
const msg = err instanceof Error ? err.message : "Could not read docs files";
|
|
3339
4267
|
if (options.json) {
|
|
@@ -3346,15 +4274,14 @@ function themeSyncCommand(cwd, options) {
|
|
|
3346
4274
|
}
|
|
3347
4275
|
const newGlobals = updateGlobalsImports(globalsContent, stockSlugs);
|
|
3348
4276
|
const newThemeConfig = updateStockThemeConfigBlock(themeConfigContent, stockManifest);
|
|
3349
|
-
const newGitignore = customSlugs.length > 0 ? updateGitignoreBlock(gitignoreContent, customSlugs) : gitignoreContent;
|
|
3350
4277
|
const newCustomOverlayCss = generateCustomOverlayCss(customManifest);
|
|
3351
4278
|
const newCustomOverlayTs = generateCustomOverlayTs(customManifest);
|
|
3352
|
-
const existingCssFiles =
|
|
4279
|
+
const existingCssFiles = existsSync13(docsAppDir) ? readdirSync6(docsAppDir).filter(
|
|
3353
4280
|
(f) => f.endsWith("-theme.css") && f !== "custom-themes.generated.css"
|
|
3354
4281
|
) : [];
|
|
3355
4282
|
const newCssSet = new Set(allSlugs.map((s) => `${s}-theme.css`));
|
|
3356
4283
|
const staleCssFiles = existingCssFiles.filter((f) => !newCssSet.has(f));
|
|
3357
|
-
const existingPublicYamls =
|
|
4284
|
+
const existingPublicYamls = existsSync13(docsPublicThemesDir) ? readdirSync6(docsPublicThemesDir).filter((f) => f.endsWith(".visor.yaml")) : [];
|
|
3358
4285
|
const newPublicYamlSet = new Set(manifest.map((e) => `${e.yamlFilename}.visor.yaml`));
|
|
3359
4286
|
const stalePublicYamls = existingPublicYamls.filter((f) => !newPublicYamlSet.has(f));
|
|
3360
4287
|
if (options.dryRun) {
|
|
@@ -3366,7 +4293,6 @@ function themeSyncCommand(cwd, options) {
|
|
|
3366
4293
|
globalsCSS: globalsPath,
|
|
3367
4294
|
customOverlayCss: CUSTOM_OVERLAY_CSS_PATH,
|
|
3368
4295
|
customOverlayTs: CUSTOM_OVERLAY_TS_PATH,
|
|
3369
|
-
gitignore: gitignorePath,
|
|
3370
4296
|
publicYamlsCopied: manifest.map((e) => `packages/docs/public/themes/${e.yamlFilename}.visor.yaml`),
|
|
3371
4297
|
publicYamlsDeleted: stalePublicYamls.map((f) => `packages/docs/public/themes/${f}`)
|
|
3372
4298
|
};
|
|
@@ -3386,25 +4312,24 @@ function themeSyncCommand(cwd, options) {
|
|
|
3386
4312
|
mkdirSync5(docsLibDir, { recursive: true });
|
|
3387
4313
|
mkdirSync5(docsPublicThemesDir, { recursive: true });
|
|
3388
4314
|
for (const entry of manifest) {
|
|
3389
|
-
writeFileSync9(
|
|
4315
|
+
writeFileSync9(join15(docsAppDir, `${entry.slug}-theme.css`), entry.css, "utf-8");
|
|
3390
4316
|
}
|
|
3391
4317
|
for (const stale of staleCssFiles) {
|
|
3392
|
-
unlinkSync2(
|
|
4318
|
+
unlinkSync2(join15(docsAppDir, stale));
|
|
3393
4319
|
}
|
|
3394
4320
|
writeFileSync9(customOverlayCssPath, newCustomOverlayCss, "utf-8");
|
|
3395
4321
|
writeFileSync9(customOverlayTsPath, newCustomOverlayTs, "utf-8");
|
|
3396
4322
|
writeFileSync9(themeConfigPath, newThemeConfig, "utf-8");
|
|
3397
4323
|
writeFileSync9(globalsPath, newGlobals, "utf-8");
|
|
3398
|
-
|
|
3399
|
-
|
|
4324
|
+
for (const srcFile of stockFiles) {
|
|
4325
|
+
copyFileSync(srcFile, join15(docsPublicThemesDir, basename3(srcFile)));
|
|
3400
4326
|
}
|
|
3401
|
-
const
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
copyFileSync(srcFile, join14(docsPublicThemesDir, filename));
|
|
4327
|
+
for (const c of customSources) {
|
|
4328
|
+
const targetName = c.origin === "legacy" ? basename3(c.filePath) : `${c.slug}.visor.yaml`;
|
|
4329
|
+
copyFileSync(c.filePath, join15(docsPublicThemesDir, targetName));
|
|
3405
4330
|
}
|
|
3406
4331
|
for (const stale of stalePublicYamls) {
|
|
3407
|
-
unlinkSync2(
|
|
4332
|
+
unlinkSync2(join15(docsPublicThemesDir, stale));
|
|
3408
4333
|
}
|
|
3409
4334
|
} catch (err) {
|
|
3410
4335
|
const msg = err instanceof Error ? err.message : "Write failed";
|
|
@@ -3417,6 +4342,7 @@ function themeSyncCommand(cwd, options) {
|
|
|
3417
4342
|
return;
|
|
3418
4343
|
}
|
|
3419
4344
|
if (options.json) {
|
|
4345
|
+
const warnings = [...deprecationWarnings, ...discoveryWarnings];
|
|
3420
4346
|
console.log(JSON.stringify({
|
|
3421
4347
|
success: true,
|
|
3422
4348
|
themes: manifest.length,
|
|
@@ -3424,7 +4350,8 @@ function themeSyncCommand(cwd, options) {
|
|
|
3424
4350
|
custom: customManifest.length,
|
|
3425
4351
|
staleCssDeleted: staleCssFiles.length,
|
|
3426
4352
|
staleYamlsDeleted: stalePublicYamls.length,
|
|
3427
|
-
slugs: allSlugs
|
|
4353
|
+
slugs: allSlugs,
|
|
4354
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
3428
4355
|
}));
|
|
3429
4356
|
} else {
|
|
3430
4357
|
logger.success(`Theme sync complete \u2014 ${manifest.length} themes registered`);
|
|
@@ -3440,19 +4367,19 @@ function themeSyncCommand(cwd, options) {
|
|
|
3440
4367
|
|
|
3441
4368
|
// src/commands/theme-batch-apply-flutter.ts
|
|
3442
4369
|
import {
|
|
3443
|
-
readFileSync as
|
|
4370
|
+
readFileSync as readFileSync17,
|
|
3444
4371
|
writeFileSync as writeFileSync10,
|
|
3445
4372
|
mkdirSync as mkdirSync6,
|
|
3446
|
-
existsSync as
|
|
3447
|
-
readdirSync as
|
|
4373
|
+
existsSync as existsSync14,
|
|
4374
|
+
readdirSync as readdirSync7,
|
|
3448
4375
|
rmSync
|
|
3449
4376
|
} from "fs";
|
|
3450
|
-
import { join as
|
|
4377
|
+
import { join as join16, basename as basename4, dirname as dirname7 } from "path";
|
|
3451
4378
|
import { generateThemeData as generateThemeData5 } from "@loworbitstudio/visor-theme-engine";
|
|
3452
4379
|
import { flutterAdapter as flutterAdapter2 } from "@loworbitstudio/visor-theme-engine/adapters";
|
|
3453
4380
|
function scanThemeDir2(dir) {
|
|
3454
|
-
if (!
|
|
3455
|
-
return
|
|
4381
|
+
if (!existsSync14(dir)) return [];
|
|
4382
|
+
return readdirSync7(dir).filter((f) => f.endsWith(".visor.yaml")).map((f) => join16(dir, f)).sort();
|
|
3456
4383
|
}
|
|
3457
4384
|
function slugToCamel(slug2) {
|
|
3458
4385
|
return slug2.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -3515,7 +4442,7 @@ function slugToDartPrefix(slug2) {
|
|
|
3515
4442
|
return slug2.replace(/-/g, "_") + "_t";
|
|
3516
4443
|
}
|
|
3517
4444
|
function emitMetaBarrel(slugs) {
|
|
3518
|
-
const
|
|
4445
|
+
const lines2 = [
|
|
3519
4446
|
`// GENERATED BY visor \u2014 DO NOT EDIT.`,
|
|
3520
4447
|
`// Regenerate with \`npm run themes:apply-flutter\`.`,
|
|
3521
4448
|
`//`,
|
|
@@ -3531,37 +4458,37 @@ function emitMetaBarrel(slugs) {
|
|
|
3531
4458
|
];
|
|
3532
4459
|
for (const slug2 of slugs) {
|
|
3533
4460
|
const prefix = slugToDartPrefix(slug2);
|
|
3534
|
-
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
4461
|
+
lines2.push(`import 'src/${slug2}/theme/visor_theme.dart' as ${prefix};`);
|
|
4462
|
+
}
|
|
4463
|
+
lines2.push(``);
|
|
4464
|
+
lines2.push(`/// A light/dark [ThemeData] pair for a single Visor theme.`);
|
|
4465
|
+
lines2.push(`class VisorThemePair {`);
|
|
4466
|
+
lines2.push(` final ThemeData light;`);
|
|
4467
|
+
lines2.push(` final ThemeData dark;`);
|
|
4468
|
+
lines2.push(` const VisorThemePair({required this.light, required this.dark});`);
|
|
4469
|
+
lines2.push(`}`);
|
|
4470
|
+
lines2.push(``);
|
|
4471
|
+
lines2.push(`/// Static access to all Visor-generated Flutter themes.`);
|
|
4472
|
+
lines2.push(`///`);
|
|
4473
|
+
lines2.push(`/// Usage:`);
|
|
4474
|
+
lines2.push(`/// \`\`\`dart`);
|
|
4475
|
+
lines2.push(`/// MaterialApp(`);
|
|
4476
|
+
lines2.push(`/// theme: VisorThemes.blackout.light,`);
|
|
4477
|
+
lines2.push(`/// darkTheme: VisorThemes.blackout.dark,`);
|
|
4478
|
+
lines2.push(`/// );`);
|
|
4479
|
+
lines2.push(`/// \`\`\``);
|
|
4480
|
+
lines2.push(`sealed class VisorThemes {`);
|
|
3554
4481
|
for (const slug2 of slugs) {
|
|
3555
4482
|
const camel = slugToCamel(slug2);
|
|
3556
4483
|
const prefix = slugToDartPrefix(slug2);
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
4484
|
+
lines2.push(` static VisorThemePair get ${camel} => VisorThemePair(`);
|
|
4485
|
+
lines2.push(` light: ${prefix}.VisorAppTheme.light,`);
|
|
4486
|
+
lines2.push(` dark: ${prefix}.VisorAppTheme.dark,`);
|
|
4487
|
+
lines2.push(` );`);
|
|
3561
4488
|
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
return
|
|
4489
|
+
lines2.push(`}`);
|
|
4490
|
+
lines2.push(``);
|
|
4491
|
+
return lines2.join("\n");
|
|
3565
4492
|
}
|
|
3566
4493
|
function emitGitignore() {
|
|
3567
4494
|
return [
|
|
@@ -3585,9 +4512,9 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3585
4512
|
process.exit(1);
|
|
3586
4513
|
return;
|
|
3587
4514
|
}
|
|
3588
|
-
const themesDir =
|
|
3589
|
-
const customThemesDir =
|
|
3590
|
-
const outputDir =
|
|
4515
|
+
const themesDir = join16(repoRoot, "themes");
|
|
4516
|
+
const customThemesDir = join16(repoRoot, "custom-themes");
|
|
4517
|
+
const outputDir = join16(repoRoot, "packages", "visor_themes");
|
|
3591
4518
|
const stockFiles = scanThemeDir2(themesDir);
|
|
3592
4519
|
const customFiles = scanThemeDir2(customThemesDir);
|
|
3593
4520
|
const allFiles = [...stockFiles, ...customFiles];
|
|
@@ -3608,7 +4535,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3608
4535
|
for (const filePath of allFiles) {
|
|
3609
4536
|
let yamlContent;
|
|
3610
4537
|
try {
|
|
3611
|
-
yamlContent =
|
|
4538
|
+
yamlContent = readFileSync17(filePath, "utf-8");
|
|
3612
4539
|
} catch {
|
|
3613
4540
|
errors.push(`Could not read: ${filePath}`);
|
|
3614
4541
|
continue;
|
|
@@ -3618,7 +4545,7 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3618
4545
|
data = generateThemeData5(yamlContent);
|
|
3619
4546
|
} catch (err) {
|
|
3620
4547
|
errors.push(
|
|
3621
|
-
`Failed to parse ${
|
|
4548
|
+
`Failed to parse ${basename4(filePath)}: ${err instanceof Error ? err.message : "Unknown error"}`
|
|
3622
4549
|
);
|
|
3623
4550
|
continue;
|
|
3624
4551
|
}
|
|
@@ -3687,8 +4614,8 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3687
4614
|
return;
|
|
3688
4615
|
}
|
|
3689
4616
|
const slugs = processed.map((p) => p.slug);
|
|
3690
|
-
const libSrcDir =
|
|
3691
|
-
if (
|
|
4617
|
+
const libSrcDir = join16(outputDir, "lib", "src");
|
|
4618
|
+
if (existsSync14(libSrcDir)) {
|
|
3692
4619
|
rmSync(libSrcDir, { recursive: true, force: true });
|
|
3693
4620
|
}
|
|
3694
4621
|
const packageFiles = {
|
|
@@ -3700,16 +4627,16 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3700
4627
|
};
|
|
3701
4628
|
let totalFiles = 0;
|
|
3702
4629
|
for (const [relPath, content] of Object.entries(packageFiles)) {
|
|
3703
|
-
const absPath =
|
|
3704
|
-
mkdirSync6(
|
|
4630
|
+
const absPath = join16(outputDir, relPath);
|
|
4631
|
+
mkdirSync6(dirname7(absPath), { recursive: true });
|
|
3705
4632
|
writeFileSync10(absPath, content, "utf-8");
|
|
3706
4633
|
totalFiles++;
|
|
3707
4634
|
}
|
|
3708
4635
|
for (const { slug: slug2, tokenFiles } of processed) {
|
|
3709
|
-
const themeBaseDir =
|
|
4636
|
+
const themeBaseDir = join16(outputDir, "lib", "src", slug2);
|
|
3710
4637
|
for (const [relPath, content] of Object.entries(tokenFiles)) {
|
|
3711
|
-
const absPath =
|
|
3712
|
-
mkdirSync6(
|
|
4638
|
+
const absPath = join16(themeBaseDir, relPath);
|
|
4639
|
+
mkdirSync6(dirname7(absPath), { recursive: true });
|
|
3713
4640
|
writeFileSync10(absPath, content, "utf-8");
|
|
3714
4641
|
totalFiles++;
|
|
3715
4642
|
}
|
|
@@ -3731,11 +4658,11 @@ function themeBatchApplyFlutterCommand(cwd, options) {
|
|
|
3731
4658
|
}
|
|
3732
4659
|
|
|
3733
4660
|
// src/commands/fonts-add.ts
|
|
3734
|
-
import { existsSync as
|
|
3735
|
-
import { resolve as
|
|
4661
|
+
import { existsSync as existsSync15, statSync as statSync7, readdirSync as readdirSync8, readFileSync as readFileSync18 } from "fs";
|
|
4662
|
+
import { resolve as resolve12, basename as basename5, extname as extname4 } from "path";
|
|
3736
4663
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
3737
4664
|
function deriveFamilySlug(filename) {
|
|
3738
|
-
const name =
|
|
4665
|
+
const name = basename5(filename, extname4(filename));
|
|
3739
4666
|
const WEIGHT_STYLE_SUFFIXES = /* @__PURE__ */ new Set([
|
|
3740
4667
|
"thin",
|
|
3741
4668
|
"hairline",
|
|
@@ -3776,28 +4703,28 @@ function deriveFamilySlug(filename) {
|
|
|
3776
4703
|
return parts.join("-").toLowerCase();
|
|
3777
4704
|
}
|
|
3778
4705
|
function collectWoff2Files(inputPath) {
|
|
3779
|
-
const resolved =
|
|
3780
|
-
if (!
|
|
4706
|
+
const resolved = resolve12(inputPath);
|
|
4707
|
+
if (!existsSync15(resolved)) {
|
|
3781
4708
|
throw new Error(`Path not found: ${resolved}`);
|
|
3782
4709
|
}
|
|
3783
|
-
const stat =
|
|
4710
|
+
const stat = statSync7(resolved);
|
|
3784
4711
|
if (stat.isFile()) {
|
|
3785
|
-
if (
|
|
4712
|
+
if (extname4(resolved).toLowerCase() !== ".woff2") {
|
|
3786
4713
|
throw new Error(
|
|
3787
|
-
`Invalid file format: ${
|
|
4714
|
+
`Invalid file format: ${basename5(resolved)}. Only .woff2 files are accepted.`
|
|
3788
4715
|
);
|
|
3789
4716
|
}
|
|
3790
4717
|
return [resolved];
|
|
3791
4718
|
}
|
|
3792
4719
|
if (stat.isDirectory()) {
|
|
3793
|
-
const files =
|
|
4720
|
+
const files = readdirSync8(resolved).filter((f) => extname4(f).toLowerCase() === ".woff2").map((f) => resolve12(resolved, f));
|
|
3794
4721
|
if (files.length === 0) {
|
|
3795
4722
|
throw new Error(
|
|
3796
4723
|
`No .woff2 files found in directory: ${resolved}`
|
|
3797
4724
|
);
|
|
3798
4725
|
}
|
|
3799
|
-
const nonWoff2Fonts =
|
|
3800
|
-
const ext =
|
|
4726
|
+
const nonWoff2Fonts = readdirSync8(resolved).filter((f) => {
|
|
4727
|
+
const ext = extname4(f).toLowerCase();
|
|
3801
4728
|
return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
|
|
3802
4729
|
});
|
|
3803
4730
|
return files.sort();
|
|
@@ -3805,12 +4732,12 @@ function collectWoff2Files(inputPath) {
|
|
|
3805
4732
|
throw new Error(`Path is neither a file nor a directory: ${resolved}`);
|
|
3806
4733
|
}
|
|
3807
4734
|
function getNonWoff2Fonts(inputPath) {
|
|
3808
|
-
const resolved =
|
|
3809
|
-
if (!
|
|
4735
|
+
const resolved = resolve12(inputPath);
|
|
4736
|
+
if (!existsSync15(resolved) || !statSync7(resolved).isDirectory()) {
|
|
3810
4737
|
return [];
|
|
3811
4738
|
}
|
|
3812
|
-
return
|
|
3813
|
-
const ext =
|
|
4739
|
+
return readdirSync8(resolved).filter((f) => {
|
|
4740
|
+
const ext = extname4(f).toLowerCase();
|
|
3814
4741
|
return [".ttf", ".otf", ".woff", ".eot"].includes(ext);
|
|
3815
4742
|
});
|
|
3816
4743
|
}
|
|
@@ -3843,7 +4770,7 @@ function createR2Client(config) {
|
|
|
3843
4770
|
});
|
|
3844
4771
|
}
|
|
3845
4772
|
async function uploadFile(client, bucket, key, filePath) {
|
|
3846
|
-
const body =
|
|
4773
|
+
const body = readFileSync18(filePath);
|
|
3847
4774
|
await client.send(
|
|
3848
4775
|
new PutObjectCommand({
|
|
3849
4776
|
Bucket: bucket,
|
|
@@ -3858,9 +4785,9 @@ async function fontsAddCommand(inputPath, options) {
|
|
|
3858
4785
|
try {
|
|
3859
4786
|
const r2Config = getR2Config();
|
|
3860
4787
|
const files = collectWoff2Files(inputPath);
|
|
3861
|
-
const familySlug = options.family ?? deriveFamilySlug(
|
|
3862
|
-
const resolved =
|
|
3863
|
-
const nonWoff2 =
|
|
4788
|
+
const familySlug = options.family ?? deriveFamilySlug(basename5(files[0]));
|
|
4789
|
+
const resolved = resolve12(inputPath);
|
|
4790
|
+
const nonWoff2 = statSync7(resolved).isDirectory() ? getNonWoff2Fonts(resolved) : [];
|
|
3864
4791
|
if (!json) {
|
|
3865
4792
|
logger.heading("Visor Font Upload");
|
|
3866
4793
|
logger.info(`Organization: ${org}`);
|
|
@@ -3878,13 +4805,13 @@ async function fontsAddCommand(inputPath, options) {
|
|
|
3878
4805
|
const bucket = "visor-fonts";
|
|
3879
4806
|
const results = [];
|
|
3880
4807
|
for (const filePath of files) {
|
|
3881
|
-
const filename =
|
|
4808
|
+
const filename = basename5(filePath);
|
|
3882
4809
|
const key = buildS3Key(org, familySlug, filename);
|
|
3883
4810
|
if (!json) {
|
|
3884
4811
|
logger.info(`Uploading ${filename}...`);
|
|
3885
4812
|
}
|
|
3886
4813
|
await uploadFile(client, bucket, key, filePath);
|
|
3887
|
-
const size =
|
|
4814
|
+
const size = statSync7(filePath).size;
|
|
3888
4815
|
results.push({ file: filename, key, size });
|
|
3889
4816
|
if (!json) {
|
|
3890
4817
|
logger.success(`Uploaded: ${key} (${formatBytes(size)})`);
|
|
@@ -3934,7 +4861,7 @@ function formatBytes(bytes) {
|
|
|
3934
4861
|
// src/commands/doctor.ts
|
|
3935
4862
|
import * as fs from "fs";
|
|
3936
4863
|
import * as path from "path";
|
|
3937
|
-
import { execFileSync as
|
|
4864
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
3938
4865
|
async function doctorCommand(cwd, options, cliVersion) {
|
|
3939
4866
|
const checks = [];
|
|
3940
4867
|
const visorJsonPath = path.join(cwd, "visor.json");
|
|
@@ -4069,9 +4996,9 @@ async function doctorCommand(cwd, options, cliVersion) {
|
|
|
4069
4996
|
}
|
|
4070
4997
|
if (process.platform !== "win32") {
|
|
4071
4998
|
try {
|
|
4072
|
-
const globalPath =
|
|
4999
|
+
const globalPath = execFileSync4("which", ["visor"], { encoding: "utf-8" }).trim();
|
|
4073
5000
|
if (globalPath) {
|
|
4074
|
-
const globalVersionRaw =
|
|
5001
|
+
const globalVersionRaw = execFileSync4(globalPath, ["--version"], { encoding: "utf-8" }).trim();
|
|
4075
5002
|
const globalVersion = globalVersionRaw.split(/\s+/).pop() ?? "";
|
|
4076
5003
|
if (isOlder(globalVersion, cliVersion)) {
|
|
4077
5004
|
checks.push({
|
|
@@ -4147,27 +5074,27 @@ function findCssFiles(dir, maxDepth = 3) {
|
|
|
4147
5074
|
}
|
|
4148
5075
|
|
|
4149
5076
|
// src/utils/patterns.ts
|
|
4150
|
-
import { existsSync as
|
|
4151
|
-
import { join as
|
|
5077
|
+
import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync20 } from "fs";
|
|
5078
|
+
import { join as join18 } from "path";
|
|
4152
5079
|
import { parse as parseYAML } from "yaml";
|
|
4153
5080
|
function loadPatternsFromYaml(repoRoot) {
|
|
4154
|
-
const patternsDir =
|
|
4155
|
-
if (!
|
|
4156
|
-
const files =
|
|
5081
|
+
const patternsDir = join18(repoRoot, "patterns");
|
|
5082
|
+
if (!existsSync17(patternsDir)) return [];
|
|
5083
|
+
const files = readdirSync10(patternsDir).filter(
|
|
4157
5084
|
(f) => f.endsWith(".visor-pattern.yaml")
|
|
4158
5085
|
);
|
|
4159
5086
|
return files.map((file) => {
|
|
4160
|
-
const content =
|
|
5087
|
+
const content = readFileSync20(join18(patternsDir, file), "utf-8");
|
|
4161
5088
|
return parseYAML(content);
|
|
4162
5089
|
}).filter(Boolean);
|
|
4163
5090
|
}
|
|
4164
5091
|
function findRepoRoot2(startDir) {
|
|
4165
5092
|
let current = startDir;
|
|
4166
5093
|
while (true) {
|
|
4167
|
-
if (
|
|
5094
|
+
if (existsSync17(join18(current, "patterns"))) {
|
|
4168
5095
|
return current;
|
|
4169
5096
|
}
|
|
4170
|
-
const parent =
|
|
5097
|
+
const parent = join18(current, "..");
|
|
4171
5098
|
if (parent === current) return null;
|
|
4172
5099
|
current = parent;
|
|
4173
5100
|
}
|
|
@@ -4545,6 +5472,244 @@ Visor Tokens (${categoryLabel}) \u2014 ${tokens2.length} tokens
|
|
|
4545
5472
|
);
|
|
4546
5473
|
}
|
|
4547
5474
|
|
|
5475
|
+
// src/commands/migrate-token-substitution.ts
|
|
5476
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync11, readdirSync as readdirSync11, statSync as statSync8, existsSync as existsSync18 } from "fs";
|
|
5477
|
+
import { resolve as resolve13, join as join19, relative as relative3 } from "path";
|
|
5478
|
+
import { parse as parseYaml3 } from "yaml";
|
|
5479
|
+
import pc4 from "picocolors";
|
|
5480
|
+
var V7_ENTR_SUBSTITUTION_MAP = {
|
|
5481
|
+
"--panel": "--surface-card",
|
|
5482
|
+
"--panel-2": "--surface-interactive-default",
|
|
5483
|
+
"--panel-3": "--surface-interactive-active",
|
|
5484
|
+
"--text": "--text-primary",
|
|
5485
|
+
"--text-2": "--text-secondary",
|
|
5486
|
+
"--text-3": "--text-tertiary",
|
|
5487
|
+
"--text-4": "--text-tertiary",
|
|
5488
|
+
"--mint": "--accent-primary",
|
|
5489
|
+
"--mint-soft": "--surface-accent-subtle",
|
|
5490
|
+
"--warn": "--text-warning",
|
|
5491
|
+
"--warn-soft": "--surface-warning-subtle"
|
|
5492
|
+
};
|
|
5493
|
+
var BUILT_IN_SUBSTITUTION_MAPS = {
|
|
5494
|
+
"entr": V7_ENTR_SUBSTITUTION_MAP
|
|
5495
|
+
// future themes: "kaiah": KAIAH_SUBSTITUTION_MAP, etc.
|
|
5496
|
+
};
|
|
5497
|
+
var DEFAULT_THEME_ID = "entr";
|
|
5498
|
+
function readMapFromThemeFile(themeId, cwd) {
|
|
5499
|
+
const candidates = [
|
|
5500
|
+
join19(cwd, "themes", `${themeId}.visor.yaml`),
|
|
5501
|
+
join19(cwd, "custom-themes", `${themeId}.visor.yaml`),
|
|
5502
|
+
join19(cwd, "packages", "docs", "public", "themes", `${themeId}.visor.yaml`)
|
|
5503
|
+
];
|
|
5504
|
+
for (const candidate of candidates) {
|
|
5505
|
+
if (existsSync18(candidate)) {
|
|
5506
|
+
try {
|
|
5507
|
+
const raw = readFileSync21(candidate, "utf-8");
|
|
5508
|
+
const parsed = parseYaml3(raw);
|
|
5509
|
+
if (parsed?.migrate?.["token-substitution"]) {
|
|
5510
|
+
return parsed.migrate["token-substitution"];
|
|
5511
|
+
}
|
|
5512
|
+
} catch {
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
return void 0;
|
|
5517
|
+
}
|
|
5518
|
+
function resolveSubstitutionMap(themeId, cwd) {
|
|
5519
|
+
const fromYaml = readMapFromThemeFile(themeId, cwd);
|
|
5520
|
+
if (fromYaml) return fromYaml;
|
|
5521
|
+
return BUILT_IN_SUBSTITUTION_MAPS[themeId];
|
|
5522
|
+
}
|
|
5523
|
+
function applySubstitutionsToContent(content, map) {
|
|
5524
|
+
const substitutions = [];
|
|
5525
|
+
const lines2 = content.split("\n");
|
|
5526
|
+
const newLines = lines2.map((line, lineIndex) => {
|
|
5527
|
+
let newLine = line;
|
|
5528
|
+
for (const [from, to] of Object.entries(map)) {
|
|
5529
|
+
const pattern2 = new RegExp(`var\\(\\s*${escapeRegex(from)}\\s*(?:,[^)]*)?\\)`, "g");
|
|
5530
|
+
let match;
|
|
5531
|
+
while ((match = pattern2.exec(newLine)) !== null) {
|
|
5532
|
+
const column = match.index;
|
|
5533
|
+
const originalLine = line;
|
|
5534
|
+
const fullMatch = match[0];
|
|
5535
|
+
const replaced = fullMatch.replace(
|
|
5536
|
+
new RegExp(escapeRegex(from)),
|
|
5537
|
+
to
|
|
5538
|
+
);
|
|
5539
|
+
newLine = newLine.slice(0, match.index) + replaced + newLine.slice(match.index + fullMatch.length);
|
|
5540
|
+
substitutions.push({
|
|
5541
|
+
line: lineIndex + 1,
|
|
5542
|
+
column,
|
|
5543
|
+
from,
|
|
5544
|
+
to,
|
|
5545
|
+
originalLine,
|
|
5546
|
+
replacedLine: newLine
|
|
5547
|
+
});
|
|
5548
|
+
pattern2.lastIndex = match.index + replaced.length;
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
return newLine;
|
|
5552
|
+
});
|
|
5553
|
+
return { newContent: newLines.join("\n"), substitutions };
|
|
5554
|
+
}
|
|
5555
|
+
function escapeRegex(s) {
|
|
5556
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5557
|
+
}
|
|
5558
|
+
function collectCssFiles(dirPath) {
|
|
5559
|
+
const results = [];
|
|
5560
|
+
function walk2(current) {
|
|
5561
|
+
const entries = readdirSync11(current, { withFileTypes: true });
|
|
5562
|
+
for (const entry of entries) {
|
|
5563
|
+
const fullPath = join19(current, entry.name);
|
|
5564
|
+
if (entry.isDirectory()) {
|
|
5565
|
+
if (["node_modules", ".git", ".next", "dist", "build", ".cache"].includes(entry.name)) {
|
|
5566
|
+
continue;
|
|
5567
|
+
}
|
|
5568
|
+
walk2(fullPath);
|
|
5569
|
+
} else if (entry.isFile()) {
|
|
5570
|
+
const name = entry.name;
|
|
5571
|
+
if (name.endsWith(".module.css") || name.endsWith(".scss") || name.endsWith(".css")) {
|
|
5572
|
+
results.push(fullPath);
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
walk2(dirPath);
|
|
5578
|
+
return results;
|
|
5579
|
+
}
|
|
5580
|
+
function runSubstitutionPass(targetPath, map, themeId) {
|
|
5581
|
+
let files;
|
|
5582
|
+
const stat = statSync8(targetPath);
|
|
5583
|
+
if (stat.isFile()) {
|
|
5584
|
+
files = [targetPath];
|
|
5585
|
+
} else {
|
|
5586
|
+
files = collectCssFiles(targetPath);
|
|
5587
|
+
}
|
|
5588
|
+
const fileResults = [];
|
|
5589
|
+
let totalSubstitutions = 0;
|
|
5590
|
+
for (const file of files) {
|
|
5591
|
+
const content = readFileSync21(file, "utf-8");
|
|
5592
|
+
const { newContent, substitutions } = applySubstitutionsToContent(content, map);
|
|
5593
|
+
if (substitutions.length > 0) {
|
|
5594
|
+
fileResults.push({ file, substitutions, originalContent: content, newContent });
|
|
5595
|
+
totalSubstitutions += substitutions.length;
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
return {
|
|
5599
|
+
themeId,
|
|
5600
|
+
targetPath,
|
|
5601
|
+
filesScanned: files.length,
|
|
5602
|
+
filesChanged: fileResults.length,
|
|
5603
|
+
totalSubstitutions,
|
|
5604
|
+
files: fileResults
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
function migrateTokenSubstitutionCommand(targetArg, cwd, options) {
|
|
5608
|
+
const themeId = options.themeId ?? DEFAULT_THEME_ID;
|
|
5609
|
+
const apply = options.apply ?? false;
|
|
5610
|
+
const dryRun = !apply;
|
|
5611
|
+
const map = resolveSubstitutionMap(themeId, cwd);
|
|
5612
|
+
if (!map) {
|
|
5613
|
+
const available = Object.keys(BUILT_IN_SUBSTITUTION_MAPS).join(", ");
|
|
5614
|
+
if (options.json) {
|
|
5615
|
+
console.log(JSON.stringify({
|
|
5616
|
+
success: false,
|
|
5617
|
+
error: `Unknown theme-id: "${themeId}". Available: ${available}`
|
|
5618
|
+
}));
|
|
5619
|
+
process.exit(1);
|
|
5620
|
+
}
|
|
5621
|
+
logger.error(`Unknown theme-id: "${themeId}". Available: ${available}`);
|
|
5622
|
+
process.exit(1);
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
const targetPath = resolve13(cwd, targetArg ?? ".");
|
|
5626
|
+
try {
|
|
5627
|
+
statSync8(targetPath);
|
|
5628
|
+
} catch {
|
|
5629
|
+
if (options.json) {
|
|
5630
|
+
console.log(JSON.stringify({
|
|
5631
|
+
success: false,
|
|
5632
|
+
error: `Target path not found: ${targetPath}`
|
|
5633
|
+
}));
|
|
5634
|
+
process.exit(1);
|
|
5635
|
+
}
|
|
5636
|
+
logger.error(`Target path not found: ${targetPath}`);
|
|
5637
|
+
process.exit(1);
|
|
5638
|
+
return;
|
|
5639
|
+
}
|
|
5640
|
+
const result = runSubstitutionPass(targetPath, map, themeId);
|
|
5641
|
+
if (result.filesChanged === 0) {
|
|
5642
|
+
if (options.json) {
|
|
5643
|
+
console.log(JSON.stringify({ success: true, ...result, message: "No V7 primitives found \u2014 already up to date." }));
|
|
5644
|
+
process.exit(0);
|
|
5645
|
+
}
|
|
5646
|
+
logger.success(`No V7 primitives found \u2014 ${result.filesScanned} file(s) scanned. Already up to date.`);
|
|
5647
|
+
process.exit(0);
|
|
5648
|
+
return;
|
|
5649
|
+
}
|
|
5650
|
+
if (options.json) {
|
|
5651
|
+
if (apply) {
|
|
5652
|
+
for (const f of result.files) {
|
|
5653
|
+
writeFileSync11(f.file, f.newContent, "utf-8");
|
|
5654
|
+
}
|
|
5655
|
+
console.log(JSON.stringify({
|
|
5656
|
+
success: true,
|
|
5657
|
+
applied: true,
|
|
5658
|
+
...result
|
|
5659
|
+
}));
|
|
5660
|
+
} else {
|
|
5661
|
+
console.log(JSON.stringify({
|
|
5662
|
+
success: true,
|
|
5663
|
+
dryRun: true,
|
|
5664
|
+
...result
|
|
5665
|
+
}));
|
|
5666
|
+
}
|
|
5667
|
+
process.exit(0);
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
const relTarget = relative3(cwd, targetPath) || ".";
|
|
5671
|
+
if (dryRun) {
|
|
5672
|
+
logger.heading(`visor migrate token-substitution \u2014 dry run`);
|
|
5673
|
+
logger.blank();
|
|
5674
|
+
logger.info(` Theme: ${pc4.bold(themeId)}`);
|
|
5675
|
+
logger.info(` Target: ${pc4.dim(relTarget)}`);
|
|
5676
|
+
logger.info(` Scanned: ${result.filesScanned} file(s)`);
|
|
5677
|
+
logger.blank();
|
|
5678
|
+
logger.heading(`Proposed changes (${result.filesChanged} file(s), ${result.totalSubstitutions} substitution(s)):`);
|
|
5679
|
+
logger.blank();
|
|
5680
|
+
for (const f of result.files) {
|
|
5681
|
+
const relFile = relative3(cwd, f.file);
|
|
5682
|
+
logger.info(pc4.bold(` ${relFile}`));
|
|
5683
|
+
for (const sub of f.substitutions) {
|
|
5684
|
+
logger.info(
|
|
5685
|
+
` line ${String(sub.line).padEnd(4)} ${pc4.red(`var(${sub.from})`)} \u2192 ${pc4.green(`var(${sub.to})`)}`
|
|
5686
|
+
);
|
|
5687
|
+
}
|
|
5688
|
+
logger.blank();
|
|
5689
|
+
}
|
|
5690
|
+
logger.warn(`Dry run \u2014 no files written. Re-run with ${pc4.bold("--apply")} to commit changes.`);
|
|
5691
|
+
} else {
|
|
5692
|
+
for (const f of result.files) {
|
|
5693
|
+
writeFileSync11(f.file, f.newContent, "utf-8");
|
|
5694
|
+
}
|
|
5695
|
+
logger.heading(`visor migrate token-substitution \u2014 applied`);
|
|
5696
|
+
logger.blank();
|
|
5697
|
+
logger.info(` Theme: ${pc4.bold(themeId)}`);
|
|
5698
|
+
logger.info(` Target: ${pc4.dim(relTarget)}`);
|
|
5699
|
+
logger.info(` Scanned: ${result.filesScanned} file(s)`);
|
|
5700
|
+
logger.blank();
|
|
5701
|
+
for (const f of result.files) {
|
|
5702
|
+
const relFile = relative3(cwd, f.file);
|
|
5703
|
+
logger.success(` ${relFile} (${f.substitutions.length} substitution(s))`);
|
|
5704
|
+
}
|
|
5705
|
+
logger.blank();
|
|
5706
|
+
logger.success(
|
|
5707
|
+
`Done \u2014 ${result.filesChanged} file(s) updated, ${result.totalSubstitutions} substitution(s) applied.`
|
|
5708
|
+
);
|
|
5709
|
+
}
|
|
5710
|
+
process.exit(0);
|
|
5711
|
+
}
|
|
5712
|
+
|
|
4548
5713
|
// src/index.ts
|
|
4549
5714
|
var program = new Command2();
|
|
4550
5715
|
program.name("visor").description("CLI for the Visor design system").version("0.3.0");
|
|
@@ -4618,6 +5783,11 @@ theme.command("validate").description("Run full validation ruleset on a .visor.y
|
|
|
4618
5783
|
themeValidateCommand(file, process.cwd(), options);
|
|
4619
5784
|
}
|
|
4620
5785
|
);
|
|
5786
|
+
theme.command("verify").description("Verify generated theme output for a target platform").argument("<dir>", "path to generated output directory").option("--target <platform>", "target platform (flutter)", "flutter").option("--json", "output structured JSON (for AI agents)").action(
|
|
5787
|
+
(dir, options) => {
|
|
5788
|
+
themeVerifyCommand(dir, process.cwd(), options);
|
|
5789
|
+
}
|
|
5790
|
+
);
|
|
4621
5791
|
theme.command("extract").description(
|
|
4622
5792
|
"Scan an existing project's CSS and produce a best-effort .visor.yaml theme file"
|
|
4623
5793
|
).option("--from <path>", "path to project directory to scan").option("--json", "output structured JSON (for AI agents)").option("-o, --output <path>", "output file path (default: .visor.yaml)").option("--validate", "run validator on the extracted theme").action(
|
|
@@ -4676,4 +5846,16 @@ var tokens = program.command("tokens").description("Explore design tokens");
|
|
|
4676
5846
|
tokens.command("list").description("List all design tokens").option("--json", "output as JSON (for AI agents)").option("--category <category>", "filter by tier: primitives, semantic, adaptive").action(async (options) => {
|
|
4677
5847
|
await tokensListCommand(process.cwd(), options);
|
|
4678
5848
|
});
|
|
5849
|
+
var migrate = program.command("migrate").description("Migration helpers \u2014 mechanically transform source files during design-system adoption");
|
|
5850
|
+
migrate.command("token-substitution").description(
|
|
5851
|
+
"Apply the \xA73.1 V7-primitive \u2192 Visor-semantic substitution table across a target directory. Dry-run by default; use --apply to commit changes. Idempotent \u2014 running twice is a no-op."
|
|
5852
|
+
).argument("[path]", "path to file or directory to migrate (default: current directory)").option("--theme-id <id>", "theme whose substitution map to apply (default: entr)", "entr").option("--dry-run", "preview proposed changes without writing files (default when --apply is omitted)").option("--apply", "write changes to disk").option("--json", "output structured JSON (for AI agents)").action(
|
|
5853
|
+
(pathArg, options) => {
|
|
5854
|
+
migrateTokenSubstitutionCommand(pathArg, process.cwd(), {
|
|
5855
|
+
themeId: options.themeId,
|
|
5856
|
+
apply: options.apply,
|
|
5857
|
+
json: options.json
|
|
5858
|
+
});
|
|
5859
|
+
}
|
|
5860
|
+
);
|
|
4679
5861
|
program.parse();
|