@maggioli-design-system/magma-codemods 2.0.0-beta.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.
Files changed (44) hide show
  1. package/README.md +193 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +77 -0
  4. package/dist/generate/diff-docs.d.ts +36 -0
  5. package/dist/generate/diff-docs.js +180 -0
  6. package/dist/index.d.ts +39 -0
  7. package/dist/index.js +166 -0
  8. package/dist/manifest/manifest.d.ts +3 -0
  9. package/dist/manifest/manifest.generated.d.ts +2 -0
  10. package/dist/manifest/manifest.generated.js +1901 -0
  11. package/dist/manifest/manifest.js +546 -0
  12. package/dist/manifest/registry.d.ts +32 -0
  13. package/dist/manifest/registry.js +102 -0
  14. package/dist/manifest/schema.d.ts +245 -0
  15. package/dist/manifest/schema.js +9 -0
  16. package/dist/report/diff.d.ts +2 -0
  17. package/dist/report/diff.js +95 -0
  18. package/dist/report/reporter.d.ts +27 -0
  19. package/dist/report/reporter.js +118 -0
  20. package/dist/report/types.d.ts +51 -0
  21. package/dist/report/types.js +2 -0
  22. package/dist/surfaces/angular.d.ts +3 -0
  23. package/dist/surfaces/angular.js +489 -0
  24. package/dist/surfaces/css.d.ts +5 -0
  25. package/dist/surfaces/css.js +287 -0
  26. package/dist/surfaces/html.d.ts +3 -0
  27. package/dist/surfaces/html.js +402 -0
  28. package/dist/surfaces/inline-templates.d.ts +3 -0
  29. package/dist/surfaces/inline-templates.js +98 -0
  30. package/dist/surfaces/react.d.ts +3 -0
  31. package/dist/surfaces/react.js +496 -0
  32. package/dist/surfaces/shared/attribute-ops.d.ts +56 -0
  33. package/dist/surfaces/shared/attribute-ops.js +62 -0
  34. package/dist/surfaces/shared/class-ops.d.ts +55 -0
  35. package/dist/surfaces/shared/class-ops.js +91 -0
  36. package/dist/surfaces/shared/edits.d.ts +19 -0
  37. package/dist/surfaces/shared/edits.js +30 -0
  38. package/dist/surfaces/shared/negate.d.ts +13 -0
  39. package/dist/surfaces/shared/negate.js +51 -0
  40. package/dist/surfaces/shared/transform.d.ts +17 -0
  41. package/dist/surfaces/shared/transform.js +8 -0
  42. package/dist/surfaces/shared/value-model.d.ts +49 -0
  43. package/dist/surfaces/shared/value-model.js +18 -0
  44. package/package.json +61 -0
@@ -0,0 +1,102 @@
1
+ /** camelCase prop → kebab-case attribute: `autoPlacement` → `auto-placement`. */
2
+ export const propToAttr = (prop) => prop.replace(/([A-Z])/g, '-$1').toLowerCase();
3
+ /** kebab-case attribute → camelCase prop: `auto-placement` → `autoPlacement`. */
4
+ export const attrToProp = (attr) => attr.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
5
+ /** Custom-element tag → React component name: `mds-dropdown` → `MdsDropdown`. */
6
+ export const tagToReactName = (tag) => tag
7
+ .split('-')
8
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
9
+ .join('');
10
+ /** Stencil event name → React handler prop: `mdsChange` → `onMdsChange`. */
11
+ export const eventToReactProp = (event) => `on${event.charAt(0).toUpperCase()}${event.slice(1)}`;
12
+ /** A stable identifier for a rule, used by `--only`/`--skip` and the report. */
13
+ export const ruleId = (tag, rule) => {
14
+ switch (rule.kind) {
15
+ case 'propRename':
16
+ case 'booleanInvert':
17
+ return `${tag}/${rule.kind}/${rule.from.prop}`;
18
+ case 'propRemove':
19
+ return `${tag}/${rule.kind}/${rule.prop.prop}`;
20
+ case 'enumRemap':
21
+ return `${tag}/${rule.kind}/${rule.prop.prop}`;
22
+ case 'slotRemove':
23
+ case 'slotRename':
24
+ return `${tag}/${rule.kind}/${rule.from}`;
25
+ case 'slotToAttr':
26
+ return `${tag}/${rule.kind}/${rule.slot}`;
27
+ case 'ensureAttr':
28
+ return `${tag}/${rule.kind}/${rule.attr.prop}`;
29
+ case 'tagRename':
30
+ return `${tag}/${rule.kind}`;
31
+ case 'cssVarRemove':
32
+ case 'classReport':
33
+ return `${tag}/${rule.kind}/${rule.name}`;
34
+ case 'cssVarRename':
35
+ case 'cssVarSurfaceReport':
36
+ case 'classRename':
37
+ case 'partRename':
38
+ case 'eventRename':
39
+ return `${tag}/${rule.kind}/${rule.from}`;
40
+ }
41
+ };
42
+ export const getByTag = (manifest, tag) => manifest.components[tag];
43
+ /** Lazily-built React-name → component index. */
44
+ const reactIndexCache = new WeakMap();
45
+ export const getByReactName = (manifest, react) => {
46
+ let index = reactIndexCache.get(manifest);
47
+ if (!index) {
48
+ index = new Map();
49
+ for (const component of Object.values(manifest.components))
50
+ index.set(component.react, component);
51
+ reactIndexCache.set(manifest, index);
52
+ }
53
+ return index.get(react);
54
+ };
55
+ /**
56
+ * The effective rule list for a component: the global per-component rules
57
+ * (currently `tone`) resolved against this component, followed by its own
58
+ * rules. Global `tone` only materialises for components that declare the
59
+ * referenced v2 enum set.
60
+ *
61
+ * Note: `removeDefaultSlot` is intentionally *not* expanded here — `slot="default"`
62
+ * lives on the projected child (any tag), not on the `mds-*` element, so each
63
+ * surface applies it globally by reading `manifest.global.removeDefaultSlot`.
64
+ */
65
+ export const rulesForComponent = (manifest, component) => {
66
+ const rules = [];
67
+ const tone = manifest.global.tone;
68
+ if (tone && component.v2EnumSets?.[tone.toneSet]) {
69
+ const toneRule = {
70
+ kind: 'enumRemap',
71
+ prop: tone.prop,
72
+ map: tone.overrides?.[component.tag] ?? tone.map,
73
+ v2set: tone.toneSet,
74
+ confidence: 'review',
75
+ };
76
+ rules.push(toneRule);
77
+ }
78
+ rules.push(...component.rules);
79
+ return rules;
80
+ };
81
+ /** Resolve the v2 enum set referenced by an `enumRemap` rule, if any. */
82
+ export const v2SetFor = (component, rule) => (rule.v2set ? component.v2EnumSets?.[rule.v2set] : undefined);
83
+ /**
84
+ * The v1 → v2 tag renames of the manifest, keyed by v1 tag. Read in one lookup
85
+ * per element so the renames are simultaneous: `mds-pref-theme` becomes
86
+ * `mds-pref-mode` while `mds-pref-theme-variant` becomes `mds-pref-theme`, and
87
+ * neither result is looked up again.
88
+ */
89
+ const tagRenameCache = new WeakMap();
90
+ export const tagRenamesOf = (manifest) => {
91
+ let renames = tagRenameCache.get(manifest);
92
+ if (!renames) {
93
+ renames = new Map();
94
+ for (const component of Object.values(manifest.components)) {
95
+ const rule = component.rules.find((r) => r.kind === 'tagRename');
96
+ if (rule)
97
+ renames.set(component.tag, rule);
98
+ }
99
+ tagRenameCache.set(manifest, renames);
100
+ }
101
+ return renames;
102
+ };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Manifest schema: the curated, per-component description of every v1 → v2
3
+ * breaking change. The manifest is the single runtime artifact the transformers
4
+ * consume; it is self-contained (carries attr↔prop pairs, the React component
5
+ * name and the v2 enum sets) so a surface never has to look anything up
6
+ * elsewhere. It is produced semi-automatically by the generator (see
7
+ * `generate/diff-docs.ts`) and ratified by hand.
8
+ */
9
+ /** How confident we are that a rule can be applied without human review. */
10
+ export type Confidence = 'safe' | 'review' | 'manual';
11
+ /** A prop identified by both of its surface spellings. */
12
+ export interface PropId {
13
+ /** kebab-case attribute name, e.g. `auto-placement` (HTML). */
14
+ attr: string;
15
+ /** camelCase property / JSX prop name, e.g. `autoPlacement` (React, Angular). */
16
+ prop: string;
17
+ }
18
+ /** Category D — rename a prop, value preserved. */
19
+ export interface PropRenameRule {
20
+ kind: 'propRename';
21
+ from: PropId;
22
+ to: PropId;
23
+ confidence: Confidence;
24
+ note?: string;
25
+ }
26
+ /** Category C — prop removed with no replacement; warn the consumer. */
27
+ export interface PropRemoveRule {
28
+ kind: 'propRemove';
29
+ prop: PropId;
30
+ /** `comment`: inject an inline note next to the attribute. `delete`: drop it. */
31
+ strategy: 'comment' | 'delete';
32
+ message: string;
33
+ }
34
+ /** Categories A and E — remap enum literals (validated against the v2 set). */
35
+ export interface EnumRemapRule {
36
+ kind: 'enumRemap';
37
+ prop: PropId;
38
+ /** `value → newValue`; a `null` target means "no v2 equivalent, migrate manually". */
39
+ map: Record<string, string | null>;
40
+ /** Name of the entry in the component's `v2EnumSets` to validate targets/literals against. */
41
+ v2set?: string;
42
+ confidence: Confidence;
43
+ }
44
+ /** Category B — rename a boolean prop and negate its value. */
45
+ export interface BooleanInvertRule {
46
+ kind: 'booleanInvert';
47
+ from: PropId;
48
+ to: PropId;
49
+ oldDefault: boolean;
50
+ newDefault: boolean;
51
+ confidence: Confidence;
52
+ }
53
+ /** Category F — remove or rename a named slot. */
54
+ export interface SlotRule {
55
+ kind: 'slotRemove' | 'slotRename';
56
+ from: string;
57
+ to?: string;
58
+ }
59
+ /**
60
+ * Category F (preferred form) — lift the text content of a slot into an
61
+ * attribute. v2 `mds-button` keeps reading slotted text for backward compat,
62
+ * but the preferred shape moves it to `label`:
63
+ * `<mds-button>Save</mds-button>` → `<mds-button label="Save"></mds-button>`.
64
+ *
65
+ * Only pure-text / single-expression content can be lifted; content with
66
+ * element children is reported for manual migration instead.
67
+ */
68
+ export interface SlotToAttrRule {
69
+ kind: 'slotToAttr';
70
+ /** Slot whose content is lifted; `default` = the unnamed slot (plain text content). */
71
+ slot: string;
72
+ to: PropId;
73
+ confidence: Confidence;
74
+ }
75
+ /** Category G — rename a CSS custom property (optionally flagging a value-format change). */
76
+ export interface CssVarRenameRule {
77
+ kind: 'cssVarRename';
78
+ /** Without the leading `--`. */
79
+ from: string;
80
+ to: string;
81
+ /** e.g. hex → `R G B` channels; the value cannot be migrated automatically. */
82
+ valueFormatChanged?: boolean;
83
+ /**
84
+ * Extra context surfaced as a flag on the definition site: the value-format
85
+ * details, or the fact that the v1 name was documented but never shipped
86
+ * (renaming it activates an override that was silently inert).
87
+ */
88
+ note?: string;
89
+ }
90
+ /** Category G2 — a CSS custom property was removed with no v2 replacement; usages are reported. */
91
+ export interface CssVarRemoveRule {
92
+ kind: 'cssVarRemove';
93
+ /** Without the leading `--`. */
94
+ name: string;
95
+ message: string;
96
+ }
97
+ /**
98
+ * Category G3 (report-only). A neutral tone/primitive used as a *background* is
99
+ * a surface under the semantic color system, but the exact role (default /
100
+ * raised / overlay / sunken / muted) is contextual and often the component's own
101
+ * default (C2 territory), so the codemod REPORTS the site for manual migration
102
+ * to a `--magma-surface-*` role instead of rewriting it. "Background context" =
103
+ * a `background` / `background-color` property, OR a custom property whose name
104
+ * contains `background` (component `--mds-*-background*` tokens). CSS-only; the
105
+ * value is never rewritten.
106
+ */
107
+ export interface CssVarSurfaceReportRule {
108
+ kind: 'cssVarSurfaceReport';
109
+ /** Without the leading `--`: the token that is a surface candidate as a background. */
110
+ from: string;
111
+ /** Optional extra guidance appended to the report message. */
112
+ note?: string;
113
+ }
114
+ /**
115
+ * Category J — rename a utility class of the styles package (the Tailwind
116
+ * design-token contract: `shadow-*`, `rounded-*`, `border-*`, `gap-*`), value
117
+ * preserved. `from`/`to` are the bare utility names; the surfaces match them
118
+ * under any variant prefixes (`hover:`, `md:`, arbitrary variants) and the
119
+ * important marker, and rewrite only the utility segment.
120
+ */
121
+ export interface ClassRenameRule {
122
+ kind: 'classRename';
123
+ /** Bare utility name, without variant prefixes: `shadow-outline-light`. */
124
+ from: string;
125
+ to: string;
126
+ /** Extra context surfaced as a flag next to the rename. */
127
+ note?: string;
128
+ /**
129
+ * Also rename the class in CSS class selectors (`.pref-theme-dark .x`). Off
130
+ * for the utility classes, which a stylesheet applies with `@apply` rather
131
+ * than selects; on for the state classes the design system writes on
132
+ * `<html>`, which consumer stylesheets select.
133
+ */
134
+ selectors?: boolean;
135
+ }
136
+ /** Category J (report-only) — a v1 utility class with no exact v2 equivalent; usages are reported. */
137
+ export interface ClassReportRule {
138
+ kind: 'classReport';
139
+ /** Bare utility name, without variant prefixes. */
140
+ name: string;
141
+ message: string;
142
+ }
143
+ /** Category H — rename a shadow part referenced in `::part()`. */
144
+ export interface PartRenameRule {
145
+ kind: 'partRename';
146
+ from: string;
147
+ to: string;
148
+ }
149
+ /**
150
+ * Category K — rename the element itself. The component keeps its v1 tag as its
151
+ * manifest key, so every other rule of the component still matches the source
152
+ * as written; the surfaces rename the tag (HTML / Angular start and end tags,
153
+ * CSS type selectors) and the React component name (JSX tags and the named
154
+ * import) in the same single pass. All renames are applied at once, so a v1
155
+ * name that another component takes in v2 (`mds-pref-theme`) is never renamed
156
+ * twice.
157
+ */
158
+ export interface TagRenameRule {
159
+ kind: 'tagRename';
160
+ /** v2 tag, `mds-pref-mode`. */
161
+ to: string;
162
+ /** v2 React component name, `MdsPrefMode`. */
163
+ toReact: string;
164
+ }
165
+ /** Category I — rename an event (raw event name, e.g. `mdsChange`). */
166
+ export interface EventRenameRule {
167
+ kind: 'eventRename';
168
+ from: string;
169
+ to: string;
170
+ }
171
+ /**
172
+ * Behavior-preservation guard. Adds `attr` (as a shorthand boolean) to the
173
+ * element **only when none of the `unless` props are already present**. Used
174
+ * when a v2 default flips relative to v1: e.g. `mds-dropdown` auto-placement was
175
+ * off by default in v1 but is on in v2, so adding `disable-auto-placement` to
176
+ * dropdowns that never set it preserves the v1 behavior.
177
+ */
178
+ export interface EnsureAttrRule {
179
+ kind: 'ensureAttr';
180
+ /** Attribute/prop to add (shorthand boolean unless `value` is set). */
181
+ attr: PropId;
182
+ /** Literal value to set (`variant="light"`); omitted → boolean shorthand. */
183
+ value?: string;
184
+ /** Skip the insertion when any of these props is already on the element. */
185
+ unless: PropId[];
186
+ confidence: Confidence;
187
+ /** Human-readable reason, shown in the report. */
188
+ reason: string;
189
+ }
190
+ export type Rule = PropRenameRule | PropRemoveRule | EnumRemapRule | BooleanInvertRule | SlotRule | SlotToAttrRule | CssVarRenameRule | CssVarRemoveRule | CssVarSurfaceReportRule | ClassRenameRule | ClassReportRule | PartRenameRule | EventRenameRule | TagRenameRule | EnsureAttrRule;
191
+ export type RuleKind = Rule['kind'];
192
+ export interface ComponentManifest {
193
+ /** `mds-dropdown`. */
194
+ tag: string;
195
+ /** React component name, `MdsDropdown`. */
196
+ react: string;
197
+ /** v2 enum sets referenced by this component's `enumRemap` rules, keyed by set name. */
198
+ v2EnumSets?: Record<string, readonly string[]>;
199
+ rules: Rule[];
200
+ }
201
+ /** Rules applied to every component (still validated per-component). */
202
+ export interface GlobalRules {
203
+ /**
204
+ * `tone` enum remap applied to any component that has a `tone` prop. Targets
205
+ * are validated against each component's own v2 enum set named `toneSet`.
206
+ */
207
+ tone?: {
208
+ prop: PropId;
209
+ map: Record<string, string | null>;
210
+ /** Name of the per-component `v2EnumSets` entry holding that component's valid tone values. */
211
+ toneSet: string;
212
+ /**
213
+ * Per-tag replacement maps for components whose v2 tone set supports a
214
+ * closer target than the global one (e.g. `quiet → text` where `text`
215
+ * exists). A tag listed here uses its map *instead of* `map`.
216
+ */
217
+ overrides?: Record<string, Record<string, string | null>>;
218
+ };
219
+ /** Remove `slot="default"` everywhere (v2 uses the unnamed default slot). */
220
+ removeDefaultSlot?: boolean;
221
+ /**
222
+ * CSS custom-property migrations that are not tied to a single component:
223
+ * primitive-token renames (the `--tone-*` -> `--tone-*-seed` seed rename from
224
+ * A2) and report-only surface candidates (a neutral tone used as a background,
225
+ * migrated by hand to a `--magma-surface-*` role). CSS-only; the
226
+ * HTML/React/Angular surfaces ignore them.
227
+ */
228
+ cssVars?: Array<CssVarRenameRule | CssVarSurfaceReportRule>;
229
+ /**
230
+ * Utility-class migrations of the styles package (category J): the Tailwind
231
+ * design-token contract that changed between v1 and v2 (`shadow-*` ring
232
+ * family, retuned `rounded-*` scale, `border-*` widths, named `gap-*`
233
+ * steps). Applied to `class` / `className` values on ANY element (the
234
+ * classes are global, not tied to an `mds-*` component) and to `@apply` in
235
+ * CSS/SCSS.
236
+ */
237
+ classes?: Array<ClassRenameRule | ClassReportRule>;
238
+ }
239
+ export interface Manifest {
240
+ fromVersion: string;
241
+ toVersion: string;
242
+ global: GlobalRules;
243
+ /** Keyed by tag. */
244
+ components: Record<string, ComponentManifest>;
245
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Manifest schema: the curated, per-component description of every v1 → v2
3
+ * breaking change. The manifest is the single runtime artifact the transformers
4
+ * consume; it is self-contained (carries attr↔prop pairs, the React component
5
+ * name and the v2 enum sets) so a surface never has to look anything up
6
+ * elsewhere. It is produced semi-automatically by the generator (see
7
+ * `generate/diff-docs.ts`) and ratified by hand.
8
+ */
9
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Produce a unified diff for two strings, or `''` when they are identical. */
2
+ export declare const unifiedDiff: (fileName: string, before: string, after: string, context?: number) => string;
@@ -0,0 +1,95 @@
1
+ const LARGE = 5000;
2
+ const diffLines = (a, b) => {
3
+ if (a.length > LARGE || b.length > LARGE) {
4
+ return [
5
+ ...a.map((line) => ({ type: 'del', line })),
6
+ ...b.map((line) => ({ type: 'add', line })),
7
+ ];
8
+ }
9
+ const n = a.length;
10
+ const m = b.length;
11
+ // lcs[i][j] = length of LCS of a[i:] and b[j:]
12
+ const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
13
+ for (let i = n - 1; i >= 0; i--) {
14
+ for (let j = m - 1; j >= 0; j--) {
15
+ lcs[i][j] =
16
+ a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
17
+ }
18
+ }
19
+ const ops = [];
20
+ let i = 0;
21
+ let j = 0;
22
+ while (i < n && j < m) {
23
+ if (a[i] === b[j]) {
24
+ ops.push({ type: 'eq', line: a[i] });
25
+ i++;
26
+ j++;
27
+ }
28
+ else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
29
+ ops.push({ type: 'del', line: a[i] });
30
+ i++;
31
+ }
32
+ else {
33
+ ops.push({ type: 'add', line: b[j] });
34
+ j++;
35
+ }
36
+ }
37
+ while (i < n)
38
+ ops.push({ type: 'del', line: a[i++] });
39
+ while (j < m)
40
+ ops.push({ type: 'add', line: b[j++] });
41
+ return ops;
42
+ };
43
+ /** Produce a unified diff for two strings, or `''` when they are identical. */
44
+ export const unifiedDiff = (fileName, before, after, context = 3) => {
45
+ if (before === after)
46
+ return '';
47
+ const ops = diffLines(before.split('\n'), after.split('\n'));
48
+ // Indices of ops that are changes (non-eq).
49
+ const changeIdx = ops.map((op, idx) => (op.type === 'eq' ? -1 : idx)).filter((idx) => idx >= 0);
50
+ if (changeIdx.length === 0)
51
+ return '';
52
+ const ranges = [];
53
+ for (const idx of changeIdx) {
54
+ const start = Math.max(0, idx - context);
55
+ const end = Math.min(ops.length - 1, idx + context);
56
+ const last = ranges[ranges.length - 1];
57
+ if (last && start <= last.end + 1)
58
+ last.end = Math.max(last.end, end);
59
+ else
60
+ ranges.push({ start, end });
61
+ }
62
+ const lines = [`--- a/${fileName}`, `+++ b/${fileName}`];
63
+ for (const range of ranges) {
64
+ let aStart = 0;
65
+ let bStart = 0;
66
+ for (let k = 0; k < range.start; k++) {
67
+ if (ops[k].type !== 'add')
68
+ aStart++;
69
+ if (ops[k].type !== 'del')
70
+ bStart++;
71
+ }
72
+ let aLen = 0;
73
+ let bLen = 0;
74
+ const body = [];
75
+ for (let k = range.start; k <= range.end; k++) {
76
+ const op = ops[k];
77
+ if (op.type === 'eq') {
78
+ body.push(` ${op.line}`);
79
+ aLen++;
80
+ bLen++;
81
+ }
82
+ else if (op.type === 'del') {
83
+ body.push(`-${op.line}`);
84
+ aLen++;
85
+ }
86
+ else {
87
+ body.push(`+${op.line}`);
88
+ bLen++;
89
+ }
90
+ }
91
+ lines.push(`@@ -${aStart + 1},${aLen} +${bStart + 1},${bLen} @@`);
92
+ lines.push(...body);
93
+ }
94
+ return lines.join('\n');
95
+ };
@@ -0,0 +1,27 @@
1
+ import { type FileReport, type Report, type Surface } from './types.js';
2
+ export interface ParseError {
3
+ file: string;
4
+ surface: Surface;
5
+ message: string;
6
+ }
7
+ export interface ReporterMeta {
8
+ fromVersion: string;
9
+ toVersion: string;
10
+ dryRun: boolean;
11
+ }
12
+ export declare class Reporter {
13
+ private readonly meta;
14
+ private readonly files;
15
+ private readonly errors;
16
+ constructor(meta: ReporterMeta);
17
+ addFile(report: FileReport): void;
18
+ addError(error: ParseError): void;
19
+ build(): Report;
20
+ private countKind;
21
+ /** Plain JSON report (errors included under a top-level key). */
22
+ toJSON(report: Report): string;
23
+ renderHuman(report: Report, options?: {
24
+ showDiff: boolean;
25
+ }): string;
26
+ }
27
+ export declare const exitCode: (report: Report) => number;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Collects per-file results and renders them for humans (diff + grouped
3
+ * findings + summary table) and for machines (JSON). Exit-code policy: `0` on
4
+ * success (with or without warnings/flags/dynamic notes), `2` if any file
5
+ * failed to parse.
6
+ */
7
+ import chalk from 'chalk';
8
+ export class Reporter {
9
+ meta;
10
+ files = [];
11
+ errors = [];
12
+ constructor(meta) {
13
+ this.meta = meta;
14
+ }
15
+ addFile(report) {
16
+ this.files.push(report);
17
+ }
18
+ addError(error) {
19
+ this.errors.push(error);
20
+ }
21
+ build() {
22
+ const summary = {
23
+ filesScanned: this.files.length + this.errors.length,
24
+ filesChanged: this.files.filter((f) => f.changed).length,
25
+ changes: this.countKind('change'),
26
+ warnings: this.countKind('warn'),
27
+ flags: this.countKind('flag'),
28
+ dynamic: this.countKind('dynamic'),
29
+ errors: this.errors.length,
30
+ };
31
+ return {
32
+ fromVersion: this.meta.fromVersion,
33
+ toVersion: this.meta.toVersion,
34
+ dryRun: this.meta.dryRun,
35
+ files: this.files,
36
+ summary,
37
+ };
38
+ }
39
+ countKind(kind) {
40
+ return this.files.reduce((total, file) => total + file.findings.filter((f) => f.kind === kind).length, 0);
41
+ }
42
+ /** Plain JSON report (errors included under a top-level key). */
43
+ toJSON(report) {
44
+ return JSON.stringify({ ...report, parseErrors: this.errors }, null, 2);
45
+ }
46
+ renderHuman(report, options = { showDiff: true }) {
47
+ const out = [];
48
+ for (const file of report.files) {
49
+ if (!file.changed && file.findings.length === 0)
50
+ continue;
51
+ out.push(chalk.bold.underline(file.file));
52
+ if (options.showDiff && file.diff)
53
+ out.push(colorizeDiff(file.diff));
54
+ for (const finding of file.findings)
55
+ out.push(` ${renderFinding(finding)}`);
56
+ out.push('');
57
+ }
58
+ for (const error of this.errors) {
59
+ out.push(`${chalk.red('✖ parse error')} ${chalk.bold(error.file)}: ${error.message}`);
60
+ }
61
+ if (this.errors.length)
62
+ out.push('');
63
+ out.push(renderSummary(report));
64
+ return out.join('\n');
65
+ }
66
+ }
67
+ export const exitCode = (report) => (report.summary.errors > 0 ? 2 : 0);
68
+ const KIND_LABEL = {
69
+ change: (s) => chalk.green(s),
70
+ warn: (s) => chalk.yellow(s),
71
+ flag: (s) => chalk.yellow(s),
72
+ dynamic: (s) => chalk.magenta(s),
73
+ };
74
+ const KIND_GLYPH = {
75
+ change: '✓',
76
+ warn: '⚠',
77
+ flag: '⚠',
78
+ dynamic: '✋',
79
+ };
80
+ const renderFinding = (finding) => {
81
+ const color = KIND_LABEL[finding.kind];
82
+ const loc = finding.line ? chalk.dim(`:${finding.line}`) : '';
83
+ const rule = finding.ruleId ? chalk.dim(` [${finding.ruleId}]`) : '';
84
+ const head = `${color(KIND_GLYPH[finding.kind])}${loc} ${finding.message}${rule}`;
85
+ if (finding.before !== undefined && finding.after !== undefined) {
86
+ return `${head}\n ${chalk.red(finding.before)} ${chalk.dim('→')} ${chalk.green(finding.after)}`;
87
+ }
88
+ return head;
89
+ };
90
+ const colorizeDiff = (diff) => diff
91
+ .split('\n')
92
+ .map((line) => {
93
+ if (line.startsWith('+++') || line.startsWith('---'))
94
+ return chalk.bold(line);
95
+ if (line.startsWith('@@'))
96
+ return chalk.cyan(line);
97
+ if (line.startsWith('+'))
98
+ return chalk.green(line);
99
+ if (line.startsWith('-'))
100
+ return chalk.red(line);
101
+ return chalk.dim(line);
102
+ })
103
+ .join('\n');
104
+ const renderSummary = (report) => {
105
+ const s = report.summary;
106
+ const mode = report.dryRun ? chalk.yellow('dry-run (no files written)') : chalk.green('write');
107
+ const parts = [
108
+ `${chalk.bold('Magma codemods')} ${report.fromVersion} → ${report.toVersion} ${chalk.dim(`[${mode}]`)}`,
109
+ ` files scanned : ${s.filesScanned}`,
110
+ ` files changed : ${s.filesChanged}`,
111
+ ` changes : ${chalk.green(String(s.changes))}`,
112
+ ` warnings : ${s.warnings ? chalk.yellow(String(s.warnings)) : '0'}`,
113
+ ` flags : ${s.flags ? chalk.yellow(String(s.flags)) : '0'}`,
114
+ ` dynamic (manual): ${s.dynamic ? chalk.magenta(String(s.dynamic)) : '0'}`,
115
+ ` parse errors : ${s.errors ? chalk.red(String(s.errors)) : '0'}`,
116
+ ];
117
+ return parts.join('\n');
118
+ };
@@ -0,0 +1,51 @@
1
+ /** Report data model shared by the transformers, the reporter and the JSON output. */
2
+ export type Surface = 'html' | 'react' | 'angular' | 'css';
3
+ export type FindingKind =
4
+ /** A transformation that was applied (or would be, in dry-run). */
5
+ 'change'
6
+ /** A removed prop: an inline comment is injected and the consumer warned. */
7
+ | 'warn'
8
+ /** Applied but needs a look: enum literal outside the v2 set, CSS value-format change. */
9
+ | 'flag'
10
+ /** Dynamic / unanalyzable usage the tool cannot rewrite safely — manual action required. */
11
+ | 'dynamic';
12
+ export interface Finding {
13
+ kind: FindingKind;
14
+ surface: Surface;
15
+ file: string;
16
+ /** 1-based line, when known. */
17
+ line?: number;
18
+ column?: number;
19
+ /** The rule that produced (or would have produced) this finding. */
20
+ ruleId?: string;
21
+ /** Component tag, e.g. `mds-dropdown`. */
22
+ component?: string;
23
+ message: string;
24
+ before?: string;
25
+ after?: string;
26
+ }
27
+ export interface FileReport {
28
+ file: string;
29
+ surface: Surface;
30
+ changed: boolean;
31
+ findings: Finding[];
32
+ /** Unified diff of the file, present when `changed` and source/output are available. */
33
+ diff?: string;
34
+ }
35
+ export interface ReportSummary {
36
+ filesScanned: number;
37
+ filesChanged: number;
38
+ changes: number;
39
+ warnings: number;
40
+ flags: number;
41
+ dynamic: number;
42
+ /** Files that failed to parse. */
43
+ errors: number;
44
+ }
45
+ export interface Report {
46
+ fromVersion: string;
47
+ toVersion: string;
48
+ dryRun: boolean;
49
+ files: FileReport[];
50
+ summary: ReportSummary;
51
+ }
@@ -0,0 +1,2 @@
1
+ /** Report data model shared by the transformers, the reporter and the JSON output. */
2
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type Manifest } from '../manifest/schema.js';
2
+ import { type TransformContext, type TransformResult } from './shared/transform.js';
3
+ export declare const transformAngular: (source: string, manifest: Manifest, ctx: TransformContext) => TransformResult;