@codefast/cli 0.3.12 → 0.3.13-canary.1
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 +206 -51
- package/dist/{bin.js → bin.mjs} +4 -1
- package/dist/commands/arrange.mjs +107 -0
- package/dist/commands/mirror.mjs +66 -0
- package/dist/commands/tag.mjs +58 -0
- package/dist/lib/arrange/application/analyze.mjs +92 -0
- package/dist/lib/arrange/application/group-file.mjs +113 -0
- package/dist/lib/arrange/application/run-target.mjs +65 -0
- package/dist/lib/arrange/domain/ast/ast-helpers.mjs +115 -0
- package/dist/lib/arrange/domain/ast/collectors-cn.mjs +61 -0
- package/dist/lib/arrange/domain/ast/collectors-jsx.mjs +20 -0
- package/dist/lib/arrange/domain/ast/collectors-tv.mjs +170 -0
- package/dist/lib/arrange/domain/ast/targets.mjs +106 -0
- package/dist/lib/arrange/domain/constants.mjs +169 -0
- package/dist/lib/arrange/domain/errors.mjs +19 -0
- package/dist/lib/arrange/domain/grouping.mjs +235 -0
- package/dist/lib/arrange/domain/imports.mjs +54 -0
- package/dist/lib/arrange/domain/tokenizer.mjs +216 -0
- package/dist/lib/arrange/index.mjs +15 -0
- package/dist/lib/arrange/infra/walk.mjs +33 -0
- package/dist/lib/arrange/presentation/formatters.mjs +52 -0
- package/dist/lib/arrange/presentation/report.mjs +24 -0
- package/dist/lib/config/domain/schema.mjs +25 -0
- package/dist/lib/config/index.mjs +3 -0
- package/dist/lib/config/infra/loader.mjs +78 -0
- package/dist/lib/infra/caught-unknown-message.mjs +20 -0
- package/dist/lib/infra/config-reporter.mjs +10 -0
- package/dist/lib/infra/node-io.mjs +30 -0
- package/dist/lib/infra/workspace/repo-root.mjs +22 -0
- package/dist/lib/mirror/application/engine.mjs +248 -0
- package/dist/lib/mirror/application/sync.mjs +162 -0
- package/dist/lib/mirror/domain/constants.mjs +12 -0
- package/dist/lib/mirror/domain/errors.mjs +15 -0
- package/dist/lib/mirror/domain/types.mjs +1 -0
- package/dist/lib/mirror/index.mjs +2 -0
- package/dist/lib/mirror/infra/dirent-list.mjs +14 -0
- package/dist/lib/mirror/infra/package-filter.mjs +26 -0
- package/dist/lib/mirror/infra/path-normalizer.mjs +7 -0
- package/dist/lib/mirror/infra/update-pkg.mjs +105 -0
- package/dist/lib/mirror/infra/workspace-packages.mjs +159 -0
- package/dist/lib/mirror/presentation/reporter.mjs +111 -0
- package/dist/lib/tag/application/engine.mjs +287 -0
- package/dist/lib/tag/domain/types.mjs +1 -0
- package/dist/lib/tag/index.mjs +4 -0
- package/dist/lib/tag/infra/target-resolver.mjs +170 -0
- package/dist/lib/tag/presentation/colors.mjs +12 -0
- package/dist/lib/tag/presentation/tag-presenter.mjs +63 -0
- package/dist/program.mjs +34 -0
- package/package.json +11 -11
- package/dist/bin.d.ts +0 -3
- package/dist/bin.d.ts.map +0 -1
- package/dist/commands/arrange.d.ts +0 -3
- package/dist/commands/arrange.d.ts.map +0 -1
- package/dist/commands/arrange.js +0 -114
- package/dist/commands/mirror.d.ts +0 -4
- package/dist/commands/mirror.d.ts.map +0 -1
- package/dist/commands/mirror.js +0 -63
- package/dist/lib/arrange/analyze.d.ts +0 -4
- package/dist/lib/arrange/analyze.d.ts.map +0 -1
- package/dist/lib/arrange/analyze.js +0 -105
- package/dist/lib/arrange/ast/collectors-cn.d.ts +0 -10
- package/dist/lib/arrange/ast/collectors-cn.d.ts.map +0 -1
- package/dist/lib/arrange/ast/collectors-cn.js +0 -84
- package/dist/lib/arrange/ast/collectors-jsx.d.ts +0 -4
- package/dist/lib/arrange/ast/collectors-jsx.d.ts.map +0 -1
- package/dist/lib/arrange/ast/collectors-jsx.js +0 -18
- package/dist/lib/arrange/ast/collectors-tv.d.ts +0 -13
- package/dist/lib/arrange/ast/collectors-tv.d.ts.map +0 -1
- package/dist/lib/arrange/ast/collectors-tv.js +0 -273
- package/dist/lib/arrange/ast/targets.d.ts +0 -8
- package/dist/lib/arrange/ast/targets.d.ts.map +0 -1
- package/dist/lib/arrange/ast/targets.js +0 -143
- package/dist/lib/arrange/ast/utils.d.ts +0 -36
- package/dist/lib/arrange/ast/utils.d.ts.map +0 -1
- package/dist/lib/arrange/ast/utils.js +0 -138
- package/dist/lib/arrange/constants.d.ts +0 -68
- package/dist/lib/arrange/constants.d.ts.map +0 -1
- package/dist/lib/arrange/constants.js +0 -179
- package/dist/lib/arrange/errors.d.ts +0 -14
- package/dist/lib/arrange/errors.d.ts.map +0 -1
- package/dist/lib/arrange/errors.js +0 -16
- package/dist/lib/arrange/formatters.d.ts +0 -16
- package/dist/lib/arrange/formatters.d.ts.map +0 -1
- package/dist/lib/arrange/formatters.js +0 -57
- package/dist/lib/arrange/group-file.d.ts +0 -4
- package/dist/lib/arrange/group-file.d.ts.map +0 -1
- package/dist/lib/arrange/group-file.js +0 -115
- package/dist/lib/arrange/grouping.d.ts +0 -41
- package/dist/lib/arrange/grouping.d.ts.map +0 -1
- package/dist/lib/arrange/grouping.js +0 -280
- package/dist/lib/arrange/imports.d.ts +0 -6
- package/dist/lib/arrange/imports.d.ts.map +0 -1
- package/dist/lib/arrange/imports.js +0 -74
- package/dist/lib/arrange/report.d.ts +0 -4
- package/dist/lib/arrange/report.d.ts.map +0 -1
- package/dist/lib/arrange/report.js +0 -37
- package/dist/lib/arrange/run-target.d.ts +0 -4
- package/dist/lib/arrange/run-target.d.ts.map +0 -1
- package/dist/lib/arrange/run-target.js +0 -28
- package/dist/lib/arrange/tokenizer.d.ts +0 -37
- package/dist/lib/arrange/tokenizer.d.ts.map +0 -1
- package/dist/lib/arrange/tokenizer.js +0 -390
- package/dist/lib/arrange/types.d.ts +0 -104
- package/dist/lib/arrange/types.d.ts.map +0 -1
- package/dist/lib/arrange/types.js +0 -7
- package/dist/lib/arrange/walk.d.ts +0 -4
- package/dist/lib/arrange/walk.d.ts.map +0 -1
- package/dist/lib/arrange/walk.js +0 -34
- package/dist/lib/arrange.d.ts +0 -41
- package/dist/lib/arrange.d.ts.map +0 -1
- package/dist/lib/arrange.js +0 -38
- package/dist/lib/infra/fs-contract.d.ts +0 -28
- package/dist/lib/infra/fs-contract.d.ts.map +0 -1
- package/dist/lib/infra/node-io.d.ts +0 -4
- package/dist/lib/infra/node-io.d.ts.map +0 -1
- package/dist/lib/infra/node-io.js +0 -27
- package/dist/lib/mirror/config.d.ts +0 -7
- package/dist/lib/mirror/config.d.ts.map +0 -1
- package/dist/lib/mirror/config.js +0 -146
- package/dist/lib/mirror/constants.d.ts +0 -10
- package/dist/lib/mirror/constants.d.ts.map +0 -1
- package/dist/lib/mirror/constants.js +0 -13
- package/dist/lib/mirror/engine.d.ts +0 -12
- package/dist/lib/mirror/engine.d.ts.map +0 -1
- package/dist/lib/mirror/engine.js +0 -248
- package/dist/lib/mirror/errors.d.ts +0 -10
- package/dist/lib/mirror/errors.d.ts.map +0 -1
- package/dist/lib/mirror/errors.js +0 -12
- package/dist/lib/mirror/package-filter.d.ts +0 -8
- package/dist/lib/mirror/package-filter.d.ts.map +0 -1
- package/dist/lib/mirror/package-filter.js +0 -33
- package/dist/lib/mirror/reporter.d.ts +0 -24
- package/dist/lib/mirror/reporter.d.ts.map +0 -1
- package/dist/lib/mirror/reporter.js +0 -126
- package/dist/lib/mirror/sync.d.ts +0 -4
- package/dist/lib/mirror/sync.d.ts.map +0 -1
- package/dist/lib/mirror/sync.js +0 -135
- package/dist/lib/mirror/types.d.ts +0 -81
- package/dist/lib/mirror/types.d.ts.map +0 -1
- package/dist/lib/mirror/update-pkg.d.ts +0 -13
- package/dist/lib/mirror/update-pkg.d.ts.map +0 -1
- package/dist/lib/mirror/update-pkg.js +0 -39
- package/dist/lib/mirror/workspace-packages.d.ts +0 -26
- package/dist/lib/mirror/workspace-packages.d.ts.map +0 -1
- package/dist/lib/mirror/workspace-packages.js +0 -160
- package/dist/lib/mirror.d.ts +0 -7
- package/dist/lib/mirror.d.ts.map +0 -1
- package/dist/lib/mirror.js +0 -5
- package/dist/lib/repo-root.d.ts +0 -7
- package/dist/lib/repo-root.d.ts.map +0 -1
- package/dist/lib/repo-root.js +0 -23
- package/dist/lib/shared/utils.d.ts +0 -9
- package/dist/lib/shared/utils.d.ts.map +0 -1
- package/dist/lib/shared/utils.js +0 -12
- package/dist/program.d.ts +0 -4
- package/dist/program.d.ts.map +0 -1
- package/dist/program.js +0 -38
- /package/dist/lib/{infra/fs-contract.js → arrange/domain/types.mjs} +0 -0
- /package/dist/lib/{mirror/types.js → infra/fs-contract.mjs} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { COMPATIBLE_BUCKET_SETS, RESPONSIVE_PREFIX, STATE_PREFIXES } from "./constants.mjs";
|
|
2
|
+
//#region src/lib/arrange/domain/tokenizer.ts
|
|
3
|
+
/**
|
|
4
|
+
* Variant stems not covered by a single entry in STATE_PREFIXES (compound `has-*`,
|
|
5
|
+
* numbered `nth-*`, media features, v4 `in-[…]`, child selectors `*` / `**`, …).
|
|
6
|
+
* When these are missed, `isStateToken` is false and the variant is stripped — the
|
|
7
|
+
* utility is then bucketed as if it were unconditional, which breaks preview grouping.
|
|
8
|
+
*/
|
|
9
|
+
function isCompoundOrMediaVariantPrefix(prefix) {
|
|
10
|
+
if (prefix === "*" || prefix === "**") return true;
|
|
11
|
+
if (prefix === "inert") return true;
|
|
12
|
+
if (prefix.startsWith("has-")) return true;
|
|
13
|
+
if (prefix.startsWith("in-[")) return true;
|
|
14
|
+
if (/^nth(?:-last)?(?:-of-type)?(?:-\d+|-\[)/.test(prefix) || /^nth-of-type-\d+$/.test(prefix) || /^nth-last-of-type-\d+$/.test(prefix)) return true;
|
|
15
|
+
if (/^(?:user-valid|user-invalid|contrast-more|contrast-less|forced-colors|inverted-colors)$/.test(prefix)) return true;
|
|
16
|
+
if (/^(?:any-)?pointer-(?:fine|coarse|none)$/.test(prefix)) return true;
|
|
17
|
+
if (/^(?:portrait|landscape|noscript)$/.test(prefix)) return true;
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
function tokenizeClassString(classString) {
|
|
21
|
+
return classString.trim().split(/\s+/).filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Index of the first `:` that separates a Tailwind variant segment from the rest.
|
|
25
|
+
* Colons inside `[...]` (at positive bracket depth) are ignored so selectors like
|
|
26
|
+
* `[&_a:hover]:text-red-500` split as `[&_a:hover]:` + `text-red-500`.
|
|
27
|
+
*/
|
|
28
|
+
function indexOfFirstVariantColon(text) {
|
|
29
|
+
let depth = 0;
|
|
30
|
+
for (let i = 0; i < text.length; i++) {
|
|
31
|
+
const ch = text[i];
|
|
32
|
+
if (ch === "[") depth++;
|
|
33
|
+
else if (ch === "]") depth = Math.max(0, depth - 1);
|
|
34
|
+
else if (ch === ":" && depth === 0) return i;
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
function stripVariants(token) {
|
|
39
|
+
let withoutVariants = token;
|
|
40
|
+
const MAX_PASSES = 12;
|
|
41
|
+
for (let i = 0; i < MAX_PASSES; i++) {
|
|
42
|
+
const colonIdx = indexOfFirstVariantColon(withoutVariants);
|
|
43
|
+
if (colonIdx === -1) break;
|
|
44
|
+
withoutVariants = withoutVariants.slice(colonIdx + 1);
|
|
45
|
+
}
|
|
46
|
+
return withoutVariants;
|
|
47
|
+
}
|
|
48
|
+
/** Outermost variant segment: first `:` at bracket depth 0 → text before it. */
|
|
49
|
+
function firstLeadingVariantPrefix(token) {
|
|
50
|
+
const colonIdx = indexOfFirstVariantColon(token);
|
|
51
|
+
if (colonIdx === -1) return;
|
|
52
|
+
return token.slice(0, colonIdx);
|
|
53
|
+
}
|
|
54
|
+
function isSelectorVariantToken(token) {
|
|
55
|
+
const prefix = firstLeadingVariantPrefix(token);
|
|
56
|
+
if (prefix === void 0) return false;
|
|
57
|
+
if (prefix === "*" || prefix === "**") return true;
|
|
58
|
+
if (prefix.startsWith("has-")) return true;
|
|
59
|
+
if (prefix.startsWith("in-[")) return true;
|
|
60
|
+
if (prefix.startsWith("group-[")) return true;
|
|
61
|
+
if (prefix.startsWith("peer-[")) return true;
|
|
62
|
+
if (prefix.startsWith("not-[")) return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
function isStateToken(token) {
|
|
66
|
+
const prefix = firstLeadingVariantPrefix(token);
|
|
67
|
+
if (prefix === void 0) return false;
|
|
68
|
+
if (RESPONSIVE_PREFIX.test(token)) return true;
|
|
69
|
+
if (STATE_PREFIXES.has(prefix)) return true;
|
|
70
|
+
if (prefix.startsWith("data-[")) return true;
|
|
71
|
+
if (prefix.startsWith("aria-[")) return true;
|
|
72
|
+
if (prefix.startsWith("supports-[")) return true;
|
|
73
|
+
if (prefix.startsWith("in-data-")) return true;
|
|
74
|
+
if (/^data-(?!\[)/.test(prefix)) return true;
|
|
75
|
+
if (/^aria-/.test(prefix)) return true;
|
|
76
|
+
if (/^(group|peer)-/.test(prefix)) return true;
|
|
77
|
+
if (/^not-/.test(prefix)) return true;
|
|
78
|
+
if (isCompoundOrMediaVariantPrefix(prefix)) return true;
|
|
79
|
+
if (prefix.startsWith("[") && prefix.endsWith("]")) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Secondary sort inside the **composite** bucket: opacity / blend / isolation →
|
|
84
|
+
* 3D context → 3D transforms → 2D transforms → filters → will-change.
|
|
85
|
+
*/
|
|
86
|
+
function compositeSecondaryOrder(bareUtility) {
|
|
87
|
+
const b = bareUtility;
|
|
88
|
+
if (/^opacity(?:-|$)/.test(b) || /^mix-blend-/.test(b) || /^isolation(?:-|$)/.test(b) || b === "isolate") return 0;
|
|
89
|
+
if (b === "transform-3d" || /^perspective(?:-|$)/.test(b)) return 10;
|
|
90
|
+
if (/^rotate-[xyz](?:-|$)/.test(b) || /^translate-z(?:-|$)/.test(b) || /^scale-z(?:-|$)/.test(b)) return 20;
|
|
91
|
+
if (/^translate(?:-|$)/.test(b) || /^scale(?:-|$)/.test(b) || /^rotate(?:-|$)/.test(b) || /^skew(?:-|$)/.test(b)) return 30;
|
|
92
|
+
if (/^transform(?:-(?:gpu|cpu|none))?$/.test(b)) return 35;
|
|
93
|
+
if (/^blur(?:-|$)/.test(b) || /^backdrop-/.test(b) || /^filter(?:-|$)/.test(b) || /^brightness-/.test(b) || /^contrast-/.test(b) || /^grayscale(?:-|$)/.test(b) || /^hue-rotate-/.test(b) || /^invert(?:-|$)/.test(b) || /^saturate-/.test(b) || /^sepia(?:-|$)/.test(b)) return 40;
|
|
94
|
+
if (/^will-change/.test(b)) return 50;
|
|
95
|
+
return 99;
|
|
96
|
+
}
|
|
97
|
+
/** Classify a **bare** utility (no `hover:` / `md:` / … prefixes). */
|
|
98
|
+
function classifyBareUtility(bareUtility) {
|
|
99
|
+
const b = bareUtility;
|
|
100
|
+
if (/^@container(?:\/[a-z][a-z0-9-]*)?$/i.test(b)) return "existence";
|
|
101
|
+
if (/^(?:hidden|contents|sr-only|not-sr-only|list-item|flow-root)$/.test(b) || /^(?:block|inline-block|inline)$/.test(b)) return "existence";
|
|
102
|
+
if (/^table(?:$|-)/.test(b)) return "existence";
|
|
103
|
+
if (/^contain-/.test(b)) return "existence";
|
|
104
|
+
if (/^(?:group|peer)(?:\/[a-z][a-z0-9-]*)?$/.test(b)) return "existence";
|
|
105
|
+
if (/^(?:static|relative|absolute|fixed|sticky)$/.test(b)) return "position";
|
|
106
|
+
if (/^-?(?:inset|top|right|bottom|left|start|end)(?:-|\/|$)/.test(b) || /^-?(?:inset-x|inset-y|inset-s|inset-e)-/.test(b)) return "position";
|
|
107
|
+
if (/^-?z-/.test(b) || b === "z-auto") return "position";
|
|
108
|
+
if (/^(?:flex|inline-flex|grid|inline-grid|subgrid|masonry)(?:$|-)/.test(b) || /^(?:items|justify|justify-items|justify-self|content|self|place|place-content|place-items|place-self)-/.test(b) || /^-?(?:gap|space-[xy]|col-|row-|grid-|auto-cols|auto-rows|order-|order$)/.test(b) || /^-?(?:auto-flow|grid-flow)-/.test(b) || /^(?:columns-|break-after|break-before|break-inside)-/.test(b) || /^(?:float|clear)-/.test(b) || /^(?:wrap-|flex-wrap|flex-nowrap|flex-row|flex-col|flex-auto|flex-initial|flex-none|flex-1|flex-\[)/.test(b) || /^container$/.test(b)) return "layout";
|
|
109
|
+
if (/^-?(?:w|h|min-w|max-w|min-h|max-h|size|aspect|shrink|grow|basis)-/.test(b) || /^(?:shrink|grow)(?:$|-)/.test(b) || /^-?overflow-/.test(b) || /^-?object-/.test(b)) return "sizing";
|
|
110
|
+
if (/^-?(?:p|px|py|pt|pb|pl|pr|ps|pe|m|mx|my|mt|mb|ml|mr|ms|me)-/.test(b)) return "spacing";
|
|
111
|
+
if (/^-?(?:rounded|border|ring|divide|inset-ring)(?:-|\/|$)/.test(b) || /^(?:rounded|border|ring|divide|inset-ring)$/.test(b) || /^ring-offset-/.test(b)) return "shape";
|
|
112
|
+
if (/^-?bg-/.test(b) || /^(?:from|via|to)(?:-|\/|$)/.test(b) || /^bg-(?:linear|radial|conic)-/.test(b) || /^-?mask-/.test(b) || /^-?(?:fill|stroke)-/.test(b)) return "background";
|
|
113
|
+
if (/^-?(?:shadow|inset-shadow|drop-shadow|text-shadow)(?:-|\/|$)/.test(b) || /^(?:shadow|inset-shadow)$/.test(b) || /^-?outline(?:-|\/|$)/.test(b) || /^outline$/.test(b)) return "shadow";
|
|
114
|
+
if (/^-?(?:font|leading|tracking|list|indent|align|decoration|underline-offset|text-wrap|text-balance|text-pretty)-/.test(b) || /^-?(?:text-(?!shadow))/.test(b) || /^-?(?:whitespace|break|line-clamp|hyphens)-/.test(b) || /^(?:antialiased|subpixel-antialiased|italic|not-italic|overline|line-through|underline|no-underline|uppercase|lowercase|capitalize|normal-case|truncate|text-wrap|text-balance|text-pretty)$/.test(b) || /^text-wrap-/.test(b) || /^tabular-nums$|^slashed-zero$|^lining-nums$|^oldstyle-nums$|^proportional-nums$/.test(b)) return "typography";
|
|
115
|
+
if (/^opacity(?:-|$)/.test(b) || /^mix-blend-/.test(b) || /^isolation(?:-|$)/.test(b) || b === "isolate" || /^transform(?:-(?:gpu|cpu|none))?$/.test(b) || /^-?(?:translate|scale|rotate|skew)-/.test(b) || /^perspective(?:-|$)/.test(b) || b === "transform-3d" || /^blur(?:-|$)/.test(b) || /^backdrop-/.test(b) || /^filter(?:-|$)/.test(b) || /^brightness-/.test(b) || /^contrast-/.test(b) || /^grayscale(?:-|$)/.test(b) || /^hue-rotate-/.test(b) || /^invert(?:-|$)/.test(b) || /^saturate-/.test(b) || /^sepia(?:-|$)/.test(b) || /^will-change/.test(b)) return "composite";
|
|
116
|
+
if (/^(?:transition|duration|ease|delay|animate)(?:-|$)/.test(b) || /^ease-/.test(b)) return "motion";
|
|
117
|
+
if (/^(?:cursor|pointer-events|select|touch|scroll|scroll-m|scroll-p|overscroll|snap|resize|field-sizing|appearance|scheme|color-scheme|accent|caret)(?:-|$)/.test(b) || /^(?:cursor|select|resize)$/.test(b) || /^scroll-behavior/.test(b) || /^scroll-snap/.test(b) || /^touch-action/.test(b) || b === "inert") return "behavior";
|
|
118
|
+
return "other";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Arbitrary **parent** selectors (`[&…]`, `[&>a]:…`, slot forms with `&` inside the bracket)
|
|
122
|
+
* scope utilities to descendants — bucket as `selector` so they chunk apart from base layout/typography
|
|
123
|
+
* and are labeled distinctly from interactive `state` variants (`hover:`, `data-[…]:`, …).
|
|
124
|
+
* Pure arbitrary **properties** (`[--x]:`, `[color:red]`) have no `&` in the leading `[…]` segment.
|
|
125
|
+
*/
|
|
126
|
+
function isArbitraryParentSelectorStateToken(token) {
|
|
127
|
+
if (/^\[&/.test(token)) return true;
|
|
128
|
+
if (!token.startsWith("[")) return false;
|
|
129
|
+
const colonIdx = indexOfFirstVariantColon(token);
|
|
130
|
+
if (colonIdx === -1 || colonIdx === 0 || token[colonIdx - 1] !== "]") return false;
|
|
131
|
+
return token.slice(0, colonIdx).includes("&");
|
|
132
|
+
}
|
|
133
|
+
function classifyToken(token) {
|
|
134
|
+
if (isArbitraryParentSelectorStateToken(token)) return "selector";
|
|
135
|
+
if (isSelectorVariantToken(token)) return "selector";
|
|
136
|
+
if (/^@container(?:\/[a-z][a-z0-9-]*)?$/i.test(token)) return "existence";
|
|
137
|
+
if (token.startsWith("[")) {
|
|
138
|
+
const splitIdx = indexOfFirstVariantColon(token);
|
|
139
|
+
if (splitIdx === -1) return "arbitrary";
|
|
140
|
+
if (splitIdx === 0 || token[splitIdx - 1] !== "]") return "arbitrary";
|
|
141
|
+
}
|
|
142
|
+
if (firstLeadingVariantPrefix(token) === "starting") return "starting";
|
|
143
|
+
if (isStateToken(token)) return "state";
|
|
144
|
+
return classifyBareUtility(stripVariants(token));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Stable key for `data-[…]` variants so unrelated selectors stay split, while
|
|
148
|
+
* **branching on the full bracket** (e.g. `data-[vaul-drawer-direction=bottom]`
|
|
149
|
+
* vs `=left`) keeps direction-specific utilities in separate state groups.
|
|
150
|
+
*/
|
|
151
|
+
function dataAttributeStem(token) {
|
|
152
|
+
const match = token.match(/^data-\[([^\]]*)\]/);
|
|
153
|
+
return match ? `data-[${match[1]}]` : "data";
|
|
154
|
+
}
|
|
155
|
+
/** Same idea as {@link dataAttributeStem} for `aria-[…]:` variants. */
|
|
156
|
+
function ariaAttributeStem(token) {
|
|
157
|
+
const match = token.match(/^aria-\[([^\]]*)\]/);
|
|
158
|
+
return match ? `aria-[${match[1]}]` : "aria";
|
|
159
|
+
}
|
|
160
|
+
/** `in-data-[…]:` — normalize on the full bracket expression like {@link dataAttributeStem}. */
|
|
161
|
+
function inDataAttributeStem(token) {
|
|
162
|
+
const match = token.match(/^in-data-\[([^\]]*)\]/);
|
|
163
|
+
return match ? `in-data-[${match[1]}]` : "in-data";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Stable key for splitting adjacent `state` / `starting` tokens in {@link suggestCnGroups}
|
|
167
|
+
* (`selector` uses {@link selectorKey}).
|
|
168
|
+
* Uses the **full variant stack** (every `:` segment outside `[…]`), not only the
|
|
169
|
+
* outermost prefix, so `@md/foo:[&>*]:w-auto` and `@md/foo:has-[…]:mt-px` stay in
|
|
170
|
+
* separate groups while `hover:opacity` still keys as `hover` + `opacity`…
|
|
171
|
+
*
|
|
172
|
+
* `data-[…]` / `aria-[…]` normalize the first segment via {@link dataAttributeStem} /
|
|
173
|
+
* {@link ariaAttributeStem} (full token required for bracket capture).
|
|
174
|
+
*/
|
|
175
|
+
function stateKey(token) {
|
|
176
|
+
const layers = [];
|
|
177
|
+
let rest = token;
|
|
178
|
+
while (rest.length > 0) {
|
|
179
|
+
const colonIdx = indexOfFirstVariantColon(rest);
|
|
180
|
+
if (colonIdx === -1) break;
|
|
181
|
+
layers.push(rest.slice(0, colonIdx));
|
|
182
|
+
rest = rest.slice(colonIdx + 1);
|
|
183
|
+
}
|
|
184
|
+
if (layers.length === 0) return token;
|
|
185
|
+
if (layers[0].startsWith("data-[")) layers[0] = dataAttributeStem(token);
|
|
186
|
+
else if (layers[0].startsWith("aria-[")) layers[0] = ariaAttributeStem(token);
|
|
187
|
+
else if (layers[0].startsWith("in-data-[")) layers[0] = inDataAttributeStem(token);
|
|
188
|
+
return layers.join("");
|
|
189
|
+
}
|
|
190
|
+
const SELECTOR_KEY_SEP = "";
|
|
191
|
+
/**
|
|
192
|
+
* Variant key for {@link Bucket} `"selector"` tokens in {@link suggestCnGroups}.
|
|
193
|
+
* Reuses {@link stateKey} layer splitting, then normalizes common shadcn/Radix SVG patterns so
|
|
194
|
+
* `[&_svg]:…` and `[&_svg:not([class*='size-'])]:…` stay in one chunk.
|
|
195
|
+
*/
|
|
196
|
+
function selectorKey(token) {
|
|
197
|
+
return stateKey(token).split(SELECTOR_KEY_SEP).map(normalizeSelectorVariantLayer).join(SELECTOR_KEY_SEP);
|
|
198
|
+
}
|
|
199
|
+
function normalizeSelectorVariantLayer(layer) {
|
|
200
|
+
if (layer === "[&_svg:not([class*='size-'])]") return "[&_svg]";
|
|
201
|
+
return layer;
|
|
202
|
+
}
|
|
203
|
+
function bucketsCompatible(a, b) {
|
|
204
|
+
if (a === b) return true;
|
|
205
|
+
return COMPATIBLE_BUCKET_SETS.some((bucketSet) => bucketSet.has(a) && bucketSet.has(b));
|
|
206
|
+
}
|
|
207
|
+
/** Like {@link bucketsCompatible}, but never merge two distinct state / starting / selector variant blobs. */
|
|
208
|
+
function bucketsMergeCompatible(a, b) {
|
|
209
|
+
if (a === "state" && b === "state") return false;
|
|
210
|
+
if (a === "starting" && b === "starting") return false;
|
|
211
|
+
if (a === "selector" && b === "selector") return false;
|
|
212
|
+
if (a === "arbitrary" || b === "arbitrary") return false;
|
|
213
|
+
return bucketsCompatible(a, b);
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { bucketsCompatible, bucketsMergeCompatible, classifyBareUtility, classifyToken, compositeSecondaryOrder, indexOfFirstVariantColon, isArbitraryParentSelectorStateToken, selectorKey, stateKey, stripVariants, tokenizeClassString };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DEFAULT_ARRANGE_TARGET, LONG_STRING_TOKEN_THRESHOLD, MAX_STRIP_VARIANT_PASSES } from "./domain/constants.mjs";
|
|
2
|
+
import { bucketsCompatible, bucketsMergeCompatible, classifyToken, indexOfFirstVariantColon, selectorKey, stateKey, stripVariants, tokenizeClassString } from "./domain/tokenizer.mjs";
|
|
3
|
+
import { forEachStringLiteralInClassExpression, mergeCnUnconditionalLiteralPoolForTest } from "./domain/ast/collectors-cn.mjs";
|
|
4
|
+
import { KNOWN_CN_TV_MODULES, applyEditsDescending, buildKnownCnTvBindings, indentOfLineContaining, isCnOrTvIdentifier, lineOf, moduleLooksLikeCnTvReexport, unwrapCnInsideTvCallReplacement } from "./domain/ast/ast-helpers.mjs";
|
|
5
|
+
import { DEFAULT_SKIP_DIRS, walkTsxFiles } from "./infra/walk.mjs";
|
|
6
|
+
import { analyzeDirectory } from "./application/analyze.mjs";
|
|
7
|
+
import { printAnalyzeReport } from "./presentation/report.mjs";
|
|
8
|
+
import { areCnTailwindPartitionsEquivalent, capGroups, mergeEaseTimingIntoFollowingAnimatedState, mergeSingletons, suggestCnGroups, summarizeGroupBucketLabels } from "./domain/grouping.mjs";
|
|
9
|
+
import { escapeTsStringLiteralContent, formatArray, formatCnArguments, formatCnCall, formatJsxCnAttributeValue } from "./presentation/formatters.mjs";
|
|
10
|
+
import { cnModuleSpecifierForFile, ensureCnImport } from "./domain/imports.mjs";
|
|
11
|
+
import { groupFile } from "./application/group-file.mjs";
|
|
12
|
+
import { ArrangeError, ArrangeErrorCode } from "./domain/errors.mjs";
|
|
13
|
+
import { runArrangeSync, runOnTarget } from "./application/run-target.mjs";
|
|
14
|
+
import { createNodeCliFs, createNodeCliLogger } from "../infra/node-io.mjs";
|
|
15
|
+
export { ArrangeError, ArrangeErrorCode, DEFAULT_ARRANGE_TARGET, DEFAULT_SKIP_DIRS, KNOWN_CN_TV_MODULES, LONG_STRING_TOKEN_THRESHOLD, MAX_STRIP_VARIANT_PASSES, analyzeDirectory, applyEditsDescending, areCnTailwindPartitionsEquivalent, bucketsCompatible, bucketsMergeCompatible, buildKnownCnTvBindings, capGroups, classifyToken, cnModuleSpecifierForFile, createNodeCliFs, createNodeCliLogger, ensureCnImport, escapeTsStringLiteralContent, forEachStringLiteralInClassExpression, formatArray, formatCnArguments, formatCnCall, formatJsxCnAttributeValue, groupFile, indentOfLineContaining, indexOfFirstVariantColon, isCnOrTvIdentifier, lineOf, mergeCnUnconditionalLiteralPoolForTest, mergeEaseTimingIntoFollowingAnimatedState, mergeSingletons, moduleLooksLikeCnTvReexport, printAnalyzeReport, runArrangeSync, runOnTarget, selectorKey, stateKey, stripVariants, suggestCnGroups, summarizeGroupBucketLabels, tokenizeClassString, unwrapCnInsideTvCallReplacement, walkTsxFiles };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/lib/arrange/infra/walk.ts
|
|
3
|
+
const DEFAULT_SKIP_DIRS = new Set([
|
|
4
|
+
"node_modules",
|
|
5
|
+
"dist",
|
|
6
|
+
".git",
|
|
7
|
+
".turbo",
|
|
8
|
+
".next",
|
|
9
|
+
".cache",
|
|
10
|
+
"out",
|
|
11
|
+
"build",
|
|
12
|
+
"coverage",
|
|
13
|
+
".vercel",
|
|
14
|
+
".output"
|
|
15
|
+
]);
|
|
16
|
+
function walkTsxFiles(rootDirectoryPath, fs) {
|
|
17
|
+
const result = [];
|
|
18
|
+
const visit = (entryPath) => {
|
|
19
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
20
|
+
for (const childName of fs.readdirSync(entryPath)) {
|
|
21
|
+
if (DEFAULT_SKIP_DIRS.has(childName)) continue;
|
|
22
|
+
visit(path.join(entryPath, childName));
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (entryPath.endsWith(".d.ts")) return;
|
|
27
|
+
if (entryPath.endsWith(".tsx") || entryPath.endsWith(".ts")) result.push(entryPath);
|
|
28
|
+
};
|
|
29
|
+
visit(rootDirectoryPath);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { DEFAULT_SKIP_DIRS, walkTsxFiles };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { indentOfLineContaining } from "../domain/ast/ast-helpers.mjs";
|
|
2
|
+
//#region src/lib/arrange/presentation/formatters.ts
|
|
3
|
+
function escapeTsStringLiteralContent(group) {
|
|
4
|
+
return group.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Multiline fragment for use *inside* an existing `cn(...)` — replaces one
|
|
8
|
+
* string argument with several, without producing `cn(cn(...))`.
|
|
9
|
+
*/
|
|
10
|
+
function formatCnArguments(groups, options) {
|
|
11
|
+
const indent = options?.indent ?? " ";
|
|
12
|
+
const commaAfterLast = options?.commaAfterLastGroup ?? options?.trailingClassName ?? false;
|
|
13
|
+
const lines = [];
|
|
14
|
+
for (let i = 0; i < groups.length; i++) {
|
|
15
|
+
const group = groups[i];
|
|
16
|
+
const comma = i < groups.length - 1 || commaAfterLast ? "," : "";
|
|
17
|
+
lines.push(`${indent}"${escapeTsStringLiteralContent(group)}"${comma}`);
|
|
18
|
+
}
|
|
19
|
+
if (options?.trailingClassName) lines.push(`${indent}className,`);
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
|
22
|
+
function formatCnCall(groups, options) {
|
|
23
|
+
const lines = ["cn("];
|
|
24
|
+
const commaOnEachStringLine = groups.length > 1 || Boolean(options?.trailingClassName);
|
|
25
|
+
for (let i = 0; i < groups.length; i++) {
|
|
26
|
+
const group = groups[i];
|
|
27
|
+
const comma = commaOnEachStringLine ? "," : "";
|
|
28
|
+
lines.push(` "${escapeTsStringLiteralContent(group)}"${comma}`);
|
|
29
|
+
}
|
|
30
|
+
if (options?.trailingClassName) lines.push(" className,");
|
|
31
|
+
lines.push(")");
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
function formatArray(groups) {
|
|
35
|
+
const lines = ["["];
|
|
36
|
+
for (let i = 0; i < groups.length; i++) {
|
|
37
|
+
const group = groups[i];
|
|
38
|
+
const comma = i < groups.length - 1 || groups.length > 1 ? "," : "";
|
|
39
|
+
lines.push(` "${escapeTsStringLiteralContent(group)}"${comma}`);
|
|
40
|
+
}
|
|
41
|
+
lines.push("]");
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
function formatJsxCnAttributeValue(groups, source, valueNodeStart) {
|
|
45
|
+
const baseIndent = indentOfLineContaining(source, valueNodeStart);
|
|
46
|
+
return `{cn(\n${formatCnArguments(groups, {
|
|
47
|
+
indent: `${baseIndent} `,
|
|
48
|
+
commaAfterLastGroup: groups.length > 1
|
|
49
|
+
})}\n${baseIndent})}`;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { escapeTsStringLiteralContent, formatArray, formatCnArguments, formatCnCall, formatJsxCnAttributeValue };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import "../domain/constants.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/lib/arrange/presentation/report.ts
|
|
4
|
+
function printAnalyzeReport(analyzeRootDirectoryPath, analyzeReport, logger) {
|
|
5
|
+
const { out } = logger;
|
|
6
|
+
out(`Path: ${path.resolve(analyzeRootDirectoryPath)}`);
|
|
7
|
+
out(`.ts/.tsx files: ${analyzeReport.files}`);
|
|
8
|
+
out(`cn(...) call sites: ${analyzeReport.cnCallExpressions}`);
|
|
9
|
+
out(`tv(...) call sites: ${analyzeReport.tvCallExpressions}`);
|
|
10
|
+
out(`\nLong cn(...) string literals (≥18 tokens — consider splitting arguments): ${analyzeReport.longCnStringLiterals.length}`);
|
|
11
|
+
for (const findingEntry of analyzeReport.longCnStringLiterals.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
|
|
12
|
+
if (analyzeReport.longCnStringLiterals.length > 40) out(` … and ${analyzeReport.longCnStringLiterals.length - 40} more`);
|
|
13
|
+
out(`\nLong tv({...}) string literals in base/variants/… (≥18 tokens): ${analyzeReport.longTvStringLiterals.length}`);
|
|
14
|
+
for (const findingEntry of analyzeReport.longTvStringLiterals.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
|
|
15
|
+
if (analyzeReport.longTvStringLiterals.length > 40) out(` … and ${analyzeReport.longTvStringLiterals.length - 40} more`);
|
|
16
|
+
out(`\nJSX className="..." or className={'...'} (static strings, ≥18 tokens): ${analyzeReport.longJsxClassNameLiterals.length}`);
|
|
17
|
+
for (const findingEntry of analyzeReport.longJsxClassNameLiterals.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.tokenCount} tokens) ${findingEntry.preview}`);
|
|
18
|
+
if (analyzeReport.longJsxClassNameLiterals.length > 40) out(` … and ${analyzeReport.longJsxClassNameLiterals.length - 40} more`);
|
|
19
|
+
out(`\ncn(...) nested inside tv({...}) (prefer a string or array — preview/apply can rewrite): ${analyzeReport.cnInsideTvCalls.length}`);
|
|
20
|
+
for (const findingEntry of analyzeReport.cnInsideTvCalls.slice(0, 40)) out(` ${findingEntry.file}:${findingEntry.line} (${findingEntry.argCount} args) ${findingEntry.preview}`);
|
|
21
|
+
if (analyzeReport.cnInsideTvCalls.length > 40) out(` … and ${analyzeReport.cnInsideTvCalls.length - 40} more`);
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { printAnalyzeReport };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/lib/config/domain/schema.ts
|
|
3
|
+
const hookSchema = z.custom((value) => typeof value === "function", { message: "Expected a function" });
|
|
4
|
+
const mirrorConfigSchema = z.object({
|
|
5
|
+
skipPackages: z.array(z.string()).optional(),
|
|
6
|
+
pathTransformations: z.record(z.string(), z.object({ removePrefix: z.string().optional() }).strict()).optional(),
|
|
7
|
+
customExports: z.record(z.string(), z.record(z.string(), z.string())).optional(),
|
|
8
|
+
cssExports: z.record(z.string(), z.union([z.boolean(), z.object({
|
|
9
|
+
enabled: z.boolean().optional(),
|
|
10
|
+
customExports: z.record(z.string(), z.string()).optional(),
|
|
11
|
+
forceExportFiles: z.boolean().optional()
|
|
12
|
+
}).strict()])).optional()
|
|
13
|
+
}).strict();
|
|
14
|
+
const codefastTagConfigSchema = z.object({
|
|
15
|
+
skipPackages: z.array(z.string()).optional(),
|
|
16
|
+
onAfterWrite: hookSchema.optional()
|
|
17
|
+
}).strict();
|
|
18
|
+
const codefastArrangeConfigSchema = z.object({ onAfterWrite: hookSchema.optional() }).strict();
|
|
19
|
+
const codefastConfigSchema = z.object({
|
|
20
|
+
mirror: mirrorConfigSchema.optional(),
|
|
21
|
+
tag: codefastTagConfigSchema.optional(),
|
|
22
|
+
arrange: codefastArrangeConfigSchema.optional()
|
|
23
|
+
}).strict();
|
|
24
|
+
//#endregion
|
|
25
|
+
export { codefastArrangeConfigSchema, codefastConfigSchema, codefastTagConfigSchema, hookSchema, mirrorConfigSchema };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { codefastArrangeConfigSchema, codefastConfigSchema, codefastTagConfigSchema, hookSchema, mirrorConfigSchema } from "./domain/schema.mjs";
|
|
2
|
+
import { loadConfig, resetConfigLoaderCacheForTests } from "./infra/loader.mjs";
|
|
3
|
+
export { codefastArrangeConfigSchema, codefastConfigSchema, codefastTagConfigSchema, hookSchema, loadConfig, mirrorConfigSchema, resetConfigLoaderCacheForTests };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { codefastConfigSchema } from "../domain/schema.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import jiti from "jiti";
|
|
5
|
+
import { ZodError } from "zod";
|
|
6
|
+
//#region src/lib/config/infra/loader.ts
|
|
7
|
+
const CONFIG_JS_PRIORITY = [
|
|
8
|
+
"codefast.config.mjs",
|
|
9
|
+
"codefast.config.js",
|
|
10
|
+
"codefast.config.cjs"
|
|
11
|
+
];
|
|
12
|
+
const CONFIG_JSON = "codefast.config.json";
|
|
13
|
+
const cachedLoads = /* @__PURE__ */ new Map();
|
|
14
|
+
function formatZodError(error, filePath) {
|
|
15
|
+
const formatted = error.issues.map((issue) => {
|
|
16
|
+
return ` - ${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
17
|
+
}).join("\n");
|
|
18
|
+
return `Invalid config schema in ${path.basename(filePath)}:\n${formatted}`;
|
|
19
|
+
}
|
|
20
|
+
function parseLoadedConfig(raw, filePath) {
|
|
21
|
+
try {
|
|
22
|
+
return codefastConfigSchema.parse(raw);
|
|
23
|
+
} catch (caughtError) {
|
|
24
|
+
if (caughtError instanceof ZodError) throw new Error(formatZodError(caughtError, filePath));
|
|
25
|
+
throw caughtError;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function listConfigCandidates(startDir, fs) {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
let current = path.resolve(startDir);
|
|
31
|
+
while (true) {
|
|
32
|
+
for (const name of CONFIG_JS_PRIORITY) {
|
|
33
|
+
const candidate = path.join(current, name);
|
|
34
|
+
if (fs.existsSync(candidate)) candidates.push(candidate);
|
|
35
|
+
}
|
|
36
|
+
const jsonCandidate = path.join(current, CONFIG_JSON);
|
|
37
|
+
if (fs.existsSync(jsonCandidate)) candidates.push(jsonCandidate);
|
|
38
|
+
const parent = path.dirname(current);
|
|
39
|
+
if (parent === current) break;
|
|
40
|
+
current = parent;
|
|
41
|
+
}
|
|
42
|
+
return candidates;
|
|
43
|
+
}
|
|
44
|
+
async function readConfigFromPath(filePath, fs) {
|
|
45
|
+
if (path.extname(filePath) === ".json") {
|
|
46
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
47
|
+
return parseLoadedConfig(JSON.parse(content), filePath);
|
|
48
|
+
}
|
|
49
|
+
const moduleConfig = jiti(process.cwd(), {
|
|
50
|
+
interopDefault: true,
|
|
51
|
+
moduleCache: false
|
|
52
|
+
})(filePath);
|
|
53
|
+
return parseLoadedConfig(typeof moduleConfig === "object" && moduleConfig !== null && "default" in moduleConfig ? moduleConfig.default : moduleConfig, filePath);
|
|
54
|
+
}
|
|
55
|
+
async function loadOnce(fs, startDir) {
|
|
56
|
+
const warnings = [];
|
|
57
|
+
const configPaths = listConfigCandidates(startDir, fs);
|
|
58
|
+
if (configPaths.length === 0) return {
|
|
59
|
+
config: {},
|
|
60
|
+
warnings
|
|
61
|
+
};
|
|
62
|
+
const configPath = configPaths[0];
|
|
63
|
+
return {
|
|
64
|
+
config: await readConfigFromPath(configPath, fs),
|
|
65
|
+
warnings,
|
|
66
|
+
configPath
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function loadConfig(fs, startDir = process.cwd()) {
|
|
70
|
+
const cacheKey = path.resolve(startDir);
|
|
71
|
+
if (!cachedLoads.has(cacheKey)) cachedLoads.set(cacheKey, loadOnce(fs, cacheKey));
|
|
72
|
+
return cachedLoads.get(cacheKey);
|
|
73
|
+
}
|
|
74
|
+
function resetConfigLoaderCacheForTests() {
|
|
75
|
+
cachedLoads.clear();
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { loadConfig, resetConfigLoaderCacheForTests };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/lib/infra/caught-unknown-message.ts
|
|
2
|
+
/**
|
|
3
|
+
* Stable, user-facing text for values caught as `unknown`.
|
|
4
|
+
* Avoids `String(unknown)` / `[object Object]` and satisfies strict `no-base-to-string`.
|
|
5
|
+
*/
|
|
6
|
+
function messageFromCaughtUnknown(value) {
|
|
7
|
+
if (value instanceof Error) return value.message;
|
|
8
|
+
if (typeof value === "string") return value;
|
|
9
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
10
|
+
if (typeof value === "symbol") return String(value);
|
|
11
|
+
if (value === null) return "null";
|
|
12
|
+
if (value === void 0) return "undefined";
|
|
13
|
+
try {
|
|
14
|
+
const serialized = JSON.stringify(value);
|
|
15
|
+
if (typeof serialized === "string") return serialized;
|
|
16
|
+
} catch {}
|
|
17
|
+
return "[unserializable non-Error value]";
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { messageFromCaughtUnknown };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/lib/infra/config-reporter.ts
|
|
2
|
+
const YELLOW = "\x1B[33m";
|
|
3
|
+
const RESET = "\x1B[0m";
|
|
4
|
+
/** User-visible lines for non-fatal issues returned with a successfully parsed config. */
|
|
5
|
+
function printConfigSchemaWarnings(logger, warnings) {
|
|
6
|
+
const { out } = logger;
|
|
7
|
+
for (const warningMessage of warnings) out(`${YELLOW}\u26A0\uFE0F ${warningMessage}${RESET}`);
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { printConfigSchemaWarnings };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import fsSync from "node:fs";
|
|
3
|
+
import fsPromises from "node:fs/promises";
|
|
4
|
+
//#region src/lib/infra/node-io.ts
|
|
5
|
+
function createNodeCliFs() {
|
|
6
|
+
return {
|
|
7
|
+
existsSync: fsSync.existsSync,
|
|
8
|
+
statSync: fsSync.statSync,
|
|
9
|
+
readFileSync: fsSync.readFileSync,
|
|
10
|
+
writeFileSync: fsSync.writeFileSync,
|
|
11
|
+
readdirSync: fsSync.readdirSync,
|
|
12
|
+
readFile: (p, enc) => fsPromises.readFile(p, enc),
|
|
13
|
+
writeFile: (p, data, enc) => fsPromises.writeFile(p, data, enc),
|
|
14
|
+
readdir: (p, opts) => fsPromises.readdir(p, opts),
|
|
15
|
+
rename: (oldPath, newPath) => fsPromises.rename(oldPath, newPath),
|
|
16
|
+
unlink: (p) => fsPromises.unlink(p)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function createNodeCliLogger() {
|
|
20
|
+
return {
|
|
21
|
+
out: (line) => {
|
|
22
|
+
process.stdout.write(`${line}\n`);
|
|
23
|
+
},
|
|
24
|
+
err: (line) => {
|
|
25
|
+
process.stderr.write(`${line}\n`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { createNodeCliFs, createNodeCliLogger };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
//#region src/lib/infra/workspace/repo-root.ts
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the monorepo root (directory containing `pnpm-workspace.yaml`).
|
|
6
|
+
* Tries the compiled package location first, then `process.cwd()`.
|
|
7
|
+
*/
|
|
8
|
+
function findRepoRoot(fs) {
|
|
9
|
+
const candidates = [path.dirname(fileURLToPath(import.meta.url)), process.cwd()];
|
|
10
|
+
for (const start of candidates) {
|
|
11
|
+
let dir = path.resolve(start);
|
|
12
|
+
for (;;) {
|
|
13
|
+
if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return dir;
|
|
14
|
+
const parent = path.dirname(dir);
|
|
15
|
+
if (parent === dir) break;
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Could not locate monorepo root (missing pnpm-workspace.yaml). Searched from: ${candidates.join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { findRepoRoot };
|