@astryxdesign/cli 0.1.1 → 0.1.2-canary.043f3fd
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/CHANGELOG.md +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +31 -8
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +87 -28
- package/src/api/doctor.mjs +7 -22
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +302 -0
- package/src/api/layout.test.mjs +241 -0
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +421 -122
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +2 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +2 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +81 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +15 -6
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +241 -224
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -239
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +18 -0
- package/src/config.test.mjs +100 -0
- package/src/index.mjs +13 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +105 -0
- package/src/lib/error-codes.mjs +18 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +132 -0
- package/src/lib/integrations.test.mjs +135 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +15 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +70 -0
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -252
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -456
- package/src/commands/gap-report.test.mjs +0 -163
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -86
- package/src/lib/config.test.mjs +0 -42
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Unit tests for the XLE/XLO parser, printers, and validator.
|
|
5
|
+
* Parser/printer tests are registry-free; validator tests use a small
|
|
6
|
+
* synthetic registry so they don't depend on @xds/core docs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect} from 'vitest';
|
|
10
|
+
import {parse, parseCompact, parseOutline, detectForm, XLEParseError} from './parse.mjs';
|
|
11
|
+
import {toCompact, toOutline} from './print.mjs';
|
|
12
|
+
import {validate} from './validate.mjs';
|
|
13
|
+
import {parseEnumValues, SPACING_STEPS} from './registry.mjs';
|
|
14
|
+
|
|
15
|
+
// ─── parser: compact ───────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe('parseCompact', () => {
|
|
18
|
+
it('parses child and sibling operators', () => {
|
|
19
|
+
const doc = parseCompact('A > B + C');
|
|
20
|
+
expect(doc.roots).toHaveLength(1);
|
|
21
|
+
expect(doc.roots[0].name).toBe('A');
|
|
22
|
+
expect(doc.roots[0].children.map(c => c.name)).toEqual(['B', 'C']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parses groups with repeat', () => {
|
|
26
|
+
const doc = parseCompact('V > (C + D)*3');
|
|
27
|
+
const group = doc.roots[0].children[0];
|
|
28
|
+
expect(group.kind).toBe('group');
|
|
29
|
+
expect(group.repeat).toBe(3);
|
|
30
|
+
expect(group.children.map(c => c.name)).toEqual(['C', 'D']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('parses node anatomy: id, enum mods, payload, attrs, hint, repeat, selected', () => {
|
|
34
|
+
const doc = parseCompact('Tab#main.lg"Overview"[divider sz=md]{card-callout}!*2');
|
|
35
|
+
const node = doc.roots[0];
|
|
36
|
+
expect(node.id).toBe('main');
|
|
37
|
+
expect(node.enumMods).toEqual(['lg']);
|
|
38
|
+
expect(node.payload).toBe('Overview');
|
|
39
|
+
expect(node.attrs).toEqual([
|
|
40
|
+
expect.objectContaining({kind: 'flag', key: 'divider'}),
|
|
41
|
+
expect.objectContaining({kind: 'kv', key: 'sz', value: 'md'}),
|
|
42
|
+
]);
|
|
43
|
+
expect(node.hint.name).toBe('card-callout');
|
|
44
|
+
expect(node.selected).toBe(true);
|
|
45
|
+
expect(node.repeat).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('parses fused shorthands, objects, lists, and negation', () => {
|
|
49
|
+
const doc = parseCompact('G[c{min:340,fit} g4 dv=[top,bottom] !scroll]');
|
|
50
|
+
const attrs = doc.roots[0].attrs;
|
|
51
|
+
expect(attrs[0]).toEqual(expect.objectContaining({key: 'c', value: {min: 340, fit: true}}));
|
|
52
|
+
expect(attrs[1]).toEqual(expect.objectContaining({key: 'g', value: 4}));
|
|
53
|
+
expect(attrs[2]).toEqual(expect.objectContaining({key: 'dv', value: ['top', 'bottom']}));
|
|
54
|
+
expect(attrs[3]).toEqual(expect.objectContaining({kind: 'neg', key: 'scroll'}));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('parses slot attrs with nested sub-expressions', () => {
|
|
58
|
+
const doc = parseCompact('A[@topNav=(TN > TNI"Home" + TNI"Docs") @banner=\'hello\']');
|
|
59
|
+
const [topNav, banner] = doc.roots[0].slots;
|
|
60
|
+
expect(topNav.key).toBe('topNav');
|
|
61
|
+
expect(topNav.value.subexpr[0].name).toBe('TN');
|
|
62
|
+
expect(topNav.value.subexpr[0].children).toHaveLength(2);
|
|
63
|
+
expect(banner.value).toBe('hello');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('parses detached overlays after ;;', () => {
|
|
67
|
+
const doc = parseCompact('V > B"Delete"[opens=#confirm] ;; AD#confirm"Sure?"');
|
|
68
|
+
expect(doc.roots).toHaveLength(1);
|
|
69
|
+
expect(doc.overlays).toHaveLength(1);
|
|
70
|
+
expect(doc.overlays[0].id).toBe('confirm');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects ^ with a correction toward groups', () => {
|
|
74
|
+
expect(() => parseCompact('A > B ^ C')).toThrow(/group siblings with \(\.\.\.\)/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rejects brace content that is not a block name, teaching the payload form', () => {
|
|
78
|
+
try {
|
|
79
|
+
parseCompact('C{Total Revenue}');
|
|
80
|
+
expect.unreachable();
|
|
81
|
+
} catch (e) {
|
|
82
|
+
expect(e).toBeInstanceOf(XLEParseError);
|
|
83
|
+
expect(e.message).toMatch(/not a block reference/);
|
|
84
|
+
expect(e.message).toMatch(/quoted payload/);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reports unterminated constructs with positions', () => {
|
|
89
|
+
expect(() => parseCompact('V[g6 > C')).toThrow(XLEParseError);
|
|
90
|
+
expect(() => parseCompact('Tx"unclosed')).toThrow(/Unterminated/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── parser: outline ───────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const OUTLINE = `
|
|
97
|
+
AppShell
|
|
98
|
+
topNav: TN "Acme"
|
|
99
|
+
Layout > LC !scroll
|
|
100
|
+
V g=6
|
|
101
|
+
Tx.lg "Hello"
|
|
102
|
+
repeat 3:
|
|
103
|
+
C {card-callout}
|
|
104
|
+
Tbar "Actions"
|
|
105
|
+
B "Delete" opens=#confirm
|
|
106
|
+
|
|
107
|
+
overlays:
|
|
108
|
+
AD#confirm "Sure?"
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
describe('parseOutline', () => {
|
|
112
|
+
it('parses indentation, slot lines, inline chains, repeats, overlays', () => {
|
|
113
|
+
const doc = parseOutline(OUTLINE);
|
|
114
|
+
const shell = doc.roots[0];
|
|
115
|
+
expect(shell.name).toBe('AppShell');
|
|
116
|
+
expect(shell.slots[0].key).toBe('topNav');
|
|
117
|
+
expect(shell.slots[0].value.subexpr[0].payload).toBe('Acme');
|
|
118
|
+
|
|
119
|
+
const layout = shell.children[0];
|
|
120
|
+
expect(layout.name).toBe('Layout');
|
|
121
|
+
const lc = layout.children[0];
|
|
122
|
+
expect(lc.name).toBe('LC');
|
|
123
|
+
expect(lc.attrs[0]).toEqual(expect.objectContaining({kind: 'neg', key: 'scroll'}));
|
|
124
|
+
|
|
125
|
+
const v = lc.children[0];
|
|
126
|
+
expect(v.children[0].enumMods).toEqual(['lg']);
|
|
127
|
+
const repeat = v.children[1];
|
|
128
|
+
expect(repeat.kind).toBe('group');
|
|
129
|
+
expect(repeat.repeat).toBe(3);
|
|
130
|
+
expect(repeat.children[0].hint.name).toBe('card-callout');
|
|
131
|
+
|
|
132
|
+
// Tbar is a sibling of V (child of LC), not swallowed by the repeat block
|
|
133
|
+
expect(lc.children.map(c => c.name ?? 'group')).toEqual(['V', 'Tbar']);
|
|
134
|
+
expect(doc.overlays).toHaveLength(1);
|
|
135
|
+
expect(doc.overlays[0].id).toBe('confirm');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('parses block-form slots', () => {
|
|
139
|
+
const doc = parseOutline('ChL\n composer:\n ChC\n ChML');
|
|
140
|
+
const chl = doc.roots[0];
|
|
141
|
+
expect(chl.slots[0].key).toBe('composer');
|
|
142
|
+
expect(chl.slots[0].value.subexpr[0].name).toBe('ChC');
|
|
143
|
+
expect(chl.children.map(c => c.name)).toEqual(['ChML']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('errors on a slot line with no parent component', () => {
|
|
147
|
+
expect(() => parseOutline('topNav: TN')).toThrow(/no parent component/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ─── form detection ────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
describe('detectForm', () => {
|
|
154
|
+
it('single line → compact', () => {
|
|
155
|
+
expect(detectForm('A > B + C')).toBe('compact');
|
|
156
|
+
});
|
|
157
|
+
it('multi-line ending in operators → compact', () => {
|
|
158
|
+
expect(detectForm('A > B +\n C > D')).toBe('compact');
|
|
159
|
+
});
|
|
160
|
+
it('indented multi-line → outline', () => {
|
|
161
|
+
expect(detectForm('A\n B\n C')).toBe('outline');
|
|
162
|
+
});
|
|
163
|
+
it('repeat/overlays markers → outline', () => {
|
|
164
|
+
expect(detectForm('V\nrepeat 2:\n C')).toBe('outline');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── printers: round-trips ─────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Strip positions for structural comparison. */
|
|
171
|
+
function shape(doc) {
|
|
172
|
+
return JSON.parse(JSON.stringify(doc, (key, value) =>
|
|
173
|
+
key === 'line' || key === 'col' || key === 'form' || key === 'raw' ? undefined : value));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const ROUND_TRIP_CASES = [
|
|
177
|
+
'A > B + C',
|
|
178
|
+
'V[g6] > (C{card-callout}*4) + T[striped]',
|
|
179
|
+
'Ctr[h=100dvh] > C[w=400 p8] > B.primary"Sign in"',
|
|
180
|
+
'A[@topNav=(TN > TNI"Home")] > L > LC[!scroll] > Tx"hi"',
|
|
181
|
+
'V > B"Del"[opens=#confirm] ;; AD#confirm"Sure?"',
|
|
182
|
+
'TL > Tab"Overview"! + Tab"Activity"',
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
describe('print round-trips', () => {
|
|
186
|
+
for (const source of ROUND_TRIP_CASES) {
|
|
187
|
+
it(`compact → compact: ${source}`, () => {
|
|
188
|
+
const doc = parse(source, {form: 'compact'});
|
|
189
|
+
const reparsed = parse(toCompact(doc), {form: 'compact'});
|
|
190
|
+
expect(shape(reparsed)).toEqual(shape(doc));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it(`compact → outline → parse: ${source}`, () => {
|
|
194
|
+
const doc = parse(source, {form: 'compact'});
|
|
195
|
+
const outline = toOutline(doc);
|
|
196
|
+
const reparsed = parse(outline, {form: 'outline'});
|
|
197
|
+
// Outline flattens repeat-less disambiguation groups — compare the
|
|
198
|
+
// outline printed from both, which is canonical.
|
|
199
|
+
expect(toOutline(reparsed)).toEqual(outline);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
it('outline → compact → parse → outline is stable', () => {
|
|
204
|
+
const doc = parseOutline(OUTLINE);
|
|
205
|
+
const compact = toCompact(doc);
|
|
206
|
+
const reparsed = parseCompact(compact);
|
|
207
|
+
expect(toOutline(reparsed)).toEqual(toOutline(doc));
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ─── validator (synthetic registry) ────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
function prop(name, type, required = false) {
|
|
214
|
+
return {
|
|
215
|
+
name, type, required,
|
|
216
|
+
enumValues: parseEnumValues(type),
|
|
217
|
+
isBoolean: type === 'boolean',
|
|
218
|
+
isFunction: /^\(/.test(type),
|
|
219
|
+
isNode: /ReactNode/.test(type),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function makeTestRegistry() {
|
|
224
|
+
const make = (name, props) => ({
|
|
225
|
+
name, exportName: `XDS${name}`, dirName: name, importPath: `@xds/core/${name}`,
|
|
226
|
+
props: new Map(props.map(p => [p.name, p])),
|
|
227
|
+
});
|
|
228
|
+
const components = new Map([
|
|
229
|
+
['AppShell', make('AppShell', [prop('contentPadding', 'SpacingStep'), prop('children', 'ReactNode'), prop('topNav', 'ReactNode')])],
|
|
230
|
+
['Stack', make('Stack', [prop('gap', 'SpacingStep'), prop('hAlign', "'start' | 'center' | 'end' | 'between'"), prop('vAlign', "'start' | 'center' | 'end'")])],
|
|
231
|
+
['Chip', make('Chip', [prop('variant', "'neutral' | 'success' | 'error'"), prop('label', 'string', true)])],
|
|
232
|
+
['Box', make('Box', [prop('isScrollable', 'boolean'), prop('children', 'ReactNode')])],
|
|
233
|
+
]);
|
|
234
|
+
const aliases = new Map([['Sh', 'AppShell'], ['St', 'Stack'], ['Ch', 'Chip'], ['Bx', 'Box']]);
|
|
235
|
+
return {components, aliases, componentNames: [...components.keys()]};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const BLOCKS = [{dirName: 'CardCallout', description: '', category: 'components/Card'}];
|
|
239
|
+
|
|
240
|
+
describe('validate', () => {
|
|
241
|
+
const registry = makeTestRegistry();
|
|
242
|
+
|
|
243
|
+
it('binds aliases, full names, and XDS-prefixed names identically', () => {
|
|
244
|
+
for (const name of ['Sh', 'AppShell', 'XDSAppShell']) {
|
|
245
|
+
const doc = parse(`${name}[cp6]`);
|
|
246
|
+
const {errors} = validate(doc, registry, BLOCKS);
|
|
247
|
+
expect(errors).toEqual([]);
|
|
248
|
+
expect(doc.roots[0].bound.props.get('contentPadding')).toBe(6);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('rejects unknown components with ranked suggestions', () => {
|
|
253
|
+
const doc = parse('AppShel');
|
|
254
|
+
const {errors} = validate(doc, registry, BLOCKS);
|
|
255
|
+
expect(errors[0].message).toMatch(/Unknown component/);
|
|
256
|
+
expect(errors[0].suggestions).toContain('AppShell');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('rejects per-component shorthand misuse and suggests the right prop', () => {
|
|
260
|
+
const doc = parse('Sh[p6]');
|
|
261
|
+
const {errors} = validate(doc, registry, BLOCKS);
|
|
262
|
+
expect(errors[0].message).toMatch(/AppShell has no prop 'padding'/);
|
|
263
|
+
expect(errors[0].suggestions).toContain('contentPadding');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('rejects out-of-enum values, listing the legal set', () => {
|
|
267
|
+
const doc = parse('St[g7]');
|
|
268
|
+
const {errors} = validate(doc, registry, BLOCKS);
|
|
269
|
+
expect(errors[0].message).toMatch(/must be one of 0 \| 0\.5/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('resolves axis-neutral j= per component axis', () => {
|
|
273
|
+
const doc = parse('St[j=between]');
|
|
274
|
+
const {errors} = validate(doc, registry, BLOCKS);
|
|
275
|
+
expect(errors).toEqual([]);
|
|
276
|
+
// synthetic Stack has no `justify`, so j= falls back to hAlign
|
|
277
|
+
expect(doc.roots[0].bound.props.get('hAlign')).toBe('between');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('resolves dot-modifiers against unique enum values, with typo help', () => {
|
|
281
|
+
const ok = parse('Ch.success"Active"');
|
|
282
|
+
expect(validate(ok, registry, BLOCKS).errors).toEqual([]);
|
|
283
|
+
expect(ok.roots[0].bound.props.get('variant')).toBe('success');
|
|
284
|
+
|
|
285
|
+
const bad = parse('Ch.sucess"Active"');
|
|
286
|
+
const {errors} = validate(bad, registry, BLOCKS);
|
|
287
|
+
expect(errors[0].suggestions).toContain('success');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('resolves negation flags to boolean props', () => {
|
|
291
|
+
const doc = parse('Bx[!scroll]');
|
|
292
|
+
expect(validate(doc, registry, BLOCKS).errors).toEqual([]);
|
|
293
|
+
expect(doc.roots[0].bound.props.get('isScrollable')).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('rejects unknown blocks unless loose', () => {
|
|
297
|
+
const doc = parse('Bx{nope-not-real}');
|
|
298
|
+
expect(validate(doc, registry, BLOCKS).errors[0].message).toMatch(/Unknown block/);
|
|
299
|
+
|
|
300
|
+
const loose = parse('Bx{nope-not-real}');
|
|
301
|
+
const result = validate(loose, registry, BLOCKS, {loose: true});
|
|
302
|
+
expect(result.errors).toEqual([]);
|
|
303
|
+
expect(result.warnings[0].message).toMatch(/TODO placeholder/);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('resolves block hints case/kebab-insensitively', () => {
|
|
307
|
+
const doc = parse('Bx{card-callout}');
|
|
308
|
+
expect(validate(doc, registry, BLOCKS).errors).toEqual([]);
|
|
309
|
+
expect(doc.roots[0].hint.block.name).toBe('CardCallout');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('enforces structural pairings', () => {
|
|
313
|
+
const doc = parse('Bx > Sh');
|
|
314
|
+
const {errors} = validate(doc, registry, BLOCKS);
|
|
315
|
+
expect(errors[0].message).toMatch(/outermost/);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── registry helpers ──────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe('parseEnumValues', () => {
|
|
322
|
+
it('parses quoted string unions', () => {
|
|
323
|
+
expect(parseEnumValues("'a' | 'b' | 'c'")).toEqual(['a', 'b', 'c']);
|
|
324
|
+
});
|
|
325
|
+
it('parses numeric unions and SpacingStep', () => {
|
|
326
|
+
expect(parseEnumValues('1|2|3')).toEqual([1, 2, 3]);
|
|
327
|
+
expect(parseEnumValues('SpacingStep')).toEqual(SPACING_STEPS);
|
|
328
|
+
});
|
|
329
|
+
it('returns null for non-enums', () => {
|
|
330
|
+
expect(parseEnumValues('number | string')).toBeNull();
|
|
331
|
+
expect(parseEnumValues('boolean')).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
});
|
package/src/template.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Static template authoring API.
|
|
5
|
+
*
|
|
6
|
+
* Helpers for authoring Astryx page/block template docs. Like
|
|
7
|
+
* `createConfig`/`createIntegration`, these are tiny runtime identity helpers
|
|
8
|
+
* whose real value is the exported TypeScript surface from
|
|
9
|
+
* `@astryxdesign/cli/template`. They inject the discriminant `type` so a
|
|
10
|
+
* discovered doc always knows whether it is a page or a block. They do NOT
|
|
11
|
+
* validate — validation happens at the load boundary, where integration
|
|
12
|
+
* template discovery runs the module's default export through
|
|
13
|
+
* {@link TemplateEnvelopeSchema} (see `loadModuleWithSchema`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {z} from 'zod';
|
|
17
|
+
|
|
18
|
+
const PreviewSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
image: z.string().optional(),
|
|
21
|
+
aspectRatio: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shared authored-template shape. `type` is injected by the create* helpers,
|
|
27
|
+
* so authors never write it. Inline source/sourceFile are intentionally NOT
|
|
28
|
+
* part of v1 — a template's source is the required same-stem sibling file.
|
|
29
|
+
* Exported so integration template discovery can validate the stamped result.
|
|
30
|
+
*/
|
|
31
|
+
export const BaseTemplateSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
name: z.string().min(1, 'name is required'),
|
|
34
|
+
description: z.string().min(1, 'description is required'),
|
|
35
|
+
category: z.string().optional(),
|
|
36
|
+
componentsUsed: z.array(z.string()).optional(),
|
|
37
|
+
preview: PreviewSchema.optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The metadata envelope integration template discovery validates: a stamped
|
|
43
|
+
* template doc. This is the LOAD-boundary contract — a hand-written plain
|
|
44
|
+
* object that matches this shape is accepted (discovery does not check "was it
|
|
45
|
+
* made by the factory", only the shape).
|
|
46
|
+
*/
|
|
47
|
+
export const TemplateEnvelopeSchema = BaseTemplateSchema.extend({
|
|
48
|
+
type: z.enum(['page', 'block']),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Author an Astryx page template doc. Stamp-only: returns the def with
|
|
53
|
+
* `type: 'page'` injected. Validation happens at the load boundary.
|
|
54
|
+
*
|
|
55
|
+
* @template {import('./types/template-api').AstryxPageTemplateInput} T
|
|
56
|
+
* @param {T} def
|
|
57
|
+
* @returns {T & {type: 'page'}}
|
|
58
|
+
*/
|
|
59
|
+
export function createPageTemplate(def) {
|
|
60
|
+
return /** @type {T & {type: 'page'}} */ ({...def, type: 'page'});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Author an Astryx block template doc. Stamp-only: returns the def with
|
|
65
|
+
* `type: 'block'` injected. Validation happens at the load boundary.
|
|
66
|
+
*
|
|
67
|
+
* @template {import('./types/template-api').AstryxBlockTemplateInput} T
|
|
68
|
+
* @param {T} def
|
|
69
|
+
* @returns {T & {type: 'block'}}
|
|
70
|
+
*/
|
|
71
|
+
export function createBlockTemplate(def) {
|
|
72
|
+
return /** @type {T & {type: 'block'}} */ ({...def, type: 'block'});
|
|
73
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
createPageTemplate,
|
|
6
|
+
createBlockTemplate,
|
|
7
|
+
TemplateEnvelopeSchema,
|
|
8
|
+
} from './template.mjs';
|
|
9
|
+
|
|
10
|
+
// createPageTemplate/createBlockTemplate are now stamp-only: they inject the
|
|
11
|
+
// `type` discriminant and return the def otherwise unchanged, with NO runtime
|
|
12
|
+
// validation. Validation happens at the LOAD boundary (integration template
|
|
13
|
+
// discovery runs the default export through TemplateEnvelopeSchema), so the
|
|
14
|
+
// rejection cases that used to assert factory throws now assert the envelope
|
|
15
|
+
// schema rejects the same shapes.
|
|
16
|
+
|
|
17
|
+
describe('createPageTemplate (stamp-only)', () => {
|
|
18
|
+
it('returns the def with type "page" injected', () => {
|
|
19
|
+
const t = createPageTemplate({
|
|
20
|
+
name: 'Landing',
|
|
21
|
+
description: 'A landing page.',
|
|
22
|
+
});
|
|
23
|
+
expect(t).toEqual({
|
|
24
|
+
name: 'Landing',
|
|
25
|
+
description: 'A landing page.',
|
|
26
|
+
type: 'page',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('preserves optional fields', () => {
|
|
31
|
+
const t = createPageTemplate({
|
|
32
|
+
name: 'Landing',
|
|
33
|
+
description: 'A landing page.',
|
|
34
|
+
category: 'Marketing',
|
|
35
|
+
componentsUsed: ['Button', 'Card'],
|
|
36
|
+
preview: {image: './preview.png', aspectRatio: '16 / 9'},
|
|
37
|
+
});
|
|
38
|
+
expect(t.category).toBe('Marketing');
|
|
39
|
+
expect(t.componentsUsed).toEqual(['Button', 'Card']);
|
|
40
|
+
expect(t.preview).toEqual({image: './preview.png', aspectRatio: '16 / 9'});
|
|
41
|
+
expect(t.type).toBe('page');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('does NOT validate — returns an invalid def stamped, unchanged', () => {
|
|
45
|
+
const t = createPageTemplate({description: 'x'});
|
|
46
|
+
expect(t.type).toBe('page');
|
|
47
|
+
expect(t.name).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('createBlockTemplate (stamp-only)', () => {
|
|
52
|
+
it('returns the def with type "block" injected', () => {
|
|
53
|
+
const t = createBlockTemplate({
|
|
54
|
+
name: 'Hero',
|
|
55
|
+
description: 'A hero block.',
|
|
56
|
+
});
|
|
57
|
+
expect(t.type).toBe('block');
|
|
58
|
+
expect(t.name).toBe('Hero');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('TemplateEnvelopeSchema (load-boundary validation)', () => {
|
|
63
|
+
it('accepts a stamped page template', () => {
|
|
64
|
+
const parsed = TemplateEnvelopeSchema.parse(
|
|
65
|
+
createPageTemplate({name: 'Landing', description: 'A landing page.'}),
|
|
66
|
+
);
|
|
67
|
+
expect(parsed.type).toBe('page');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('accepts a PLAIN OBJECT envelope (no factory required)', () => {
|
|
71
|
+
const parsed = TemplateEnvelopeSchema.parse({
|
|
72
|
+
type: 'block',
|
|
73
|
+
name: 'Hero',
|
|
74
|
+
description: 'A hero block.',
|
|
75
|
+
});
|
|
76
|
+
expect(parsed.name).toBe('Hero');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects a missing name', () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
TemplateEnvelopeSchema.parse({type: 'page', description: 'x'}),
|
|
82
|
+
).toThrow(/name/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects a missing description', () => {
|
|
86
|
+
expect(() =>
|
|
87
|
+
TemplateEnvelopeSchema.parse({type: 'page', name: 'x'}),
|
|
88
|
+
).toThrow(/description/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects an empty-string name', () => {
|
|
92
|
+
expect(() =>
|
|
93
|
+
TemplateEnvelopeSchema.parse({type: 'page', name: '', description: 'x'}),
|
|
94
|
+
).toThrow(/name/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects a missing/invalid type', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
TemplateEnvelopeSchema.parse({name: 'x', description: 'y'}),
|
|
100
|
+
).toThrow();
|
|
101
|
+
expect(() =>
|
|
102
|
+
TemplateEnvelopeSchema.parse({type: 'bogus', name: 'x', description: 'y'}),
|
|
103
|
+
).toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects unknown keys (strict)', () => {
|
|
107
|
+
expect(() =>
|
|
108
|
+
TemplateEnvelopeSchema.parse({
|
|
109
|
+
type: 'page',
|
|
110
|
+
name: 'x',
|
|
111
|
+
description: 'y',
|
|
112
|
+
source: './x.tsx',
|
|
113
|
+
}),
|
|
114
|
+
).toThrow();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('rejects inline sourceFile (not supported in v1)', () => {
|
|
118
|
+
expect(() =>
|
|
119
|
+
TemplateEnvelopeSchema.parse({
|
|
120
|
+
type: 'page',
|
|
121
|
+
name: 'x',
|
|
122
|
+
description: 'y',
|
|
123
|
+
sourceFile: './x.tsx',
|
|
124
|
+
}),
|
|
125
|
+
).toThrow();
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/types/api.d.ts
CHANGED
|
@@ -33,7 +33,6 @@ import type {
|
|
|
33
33
|
TemplateShowResponse,
|
|
34
34
|
TemplateSkeletonResponse,
|
|
35
35
|
TemplateCopyResponse,
|
|
36
|
-
TemplateGetResponse,
|
|
37
36
|
} from './template';
|
|
38
37
|
import type {
|
|
39
38
|
HookListResponse,
|
|
@@ -138,6 +137,8 @@ export interface TemplateOptions {
|
|
|
138
137
|
show?: boolean;
|
|
139
138
|
/** Filter templates by kind: 'page' or 'block'. Only applies to list views. */
|
|
140
139
|
type?: 'page' | 'block';
|
|
140
|
+
/** Narrow to templates from a specific package (id-only lookups across packages are ambiguous). */
|
|
141
|
+
package?: string;
|
|
141
142
|
targetPath?: string;
|
|
142
143
|
cwd?: string;
|
|
143
144
|
}
|
|
@@ -153,11 +154,6 @@ export declare function template(
|
|
|
153
154
|
options?: TemplateOptions,
|
|
154
155
|
): Promise<TemplateResult>;
|
|
155
156
|
|
|
156
|
-
export declare function getTemplateById(
|
|
157
|
-
id: string,
|
|
158
|
-
options?: {cwd?: string},
|
|
159
|
-
): Promise<TemplateGetResponse>;
|
|
160
|
-
|
|
161
157
|
// ── Hook ─────────────────────────────────────────────────────────────
|
|
162
158
|
|
|
163
159
|
export interface HookOptions {
|
package/src/types/base.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ import type {
|
|
|
32
32
|
TemplateShowResponse,
|
|
33
33
|
TemplateSkeletonResponse,
|
|
34
34
|
TemplateCopyResponse,
|
|
35
|
-
TemplateGetResponse,
|
|
36
35
|
} from './template';
|
|
37
36
|
import type {
|
|
38
37
|
HookListResponse,
|
|
@@ -44,14 +43,11 @@ import type {
|
|
|
44
43
|
import type {SwizzleListResponse, SwizzleCopyResponse} from './swizzle';
|
|
45
44
|
import type {ThemeBuildResponse} from './theme';
|
|
46
45
|
import type {UpgradeListResponse, UpgradeRunResponse} from './upgrade';
|
|
47
|
-
import type {
|
|
48
|
-
GapReportCategoriesResponse,
|
|
49
|
-
GapReportFileResponse,
|
|
50
|
-
} from './gap-report';
|
|
51
46
|
import type {SearchResponse} from './search';
|
|
52
47
|
import type {ErrorCode} from './error-codes';
|
|
53
48
|
import type {ManifestResponse} from './manifest';
|
|
54
49
|
import type {DoctorResponse} from './doctor';
|
|
50
|
+
import type {ValidateIntegrationResponse} from './validate-integration';
|
|
55
51
|
|
|
56
52
|
/**
|
|
57
53
|
* Structured error. Check `'error' in result` to discriminate.
|
|
@@ -93,7 +89,6 @@ export type CLIAnyResponse =
|
|
|
93
89
|
| TemplateShowResponse
|
|
94
90
|
| TemplateSkeletonResponse
|
|
95
91
|
| TemplateCopyResponse
|
|
96
|
-
| TemplateGetResponse
|
|
97
92
|
| HookListResponse
|
|
98
93
|
| HookBriefResponse
|
|
99
94
|
| HookFullResponse
|
|
@@ -104,11 +99,10 @@ export type CLIAnyResponse =
|
|
|
104
99
|
| ThemeBuildResponse
|
|
105
100
|
| UpgradeListResponse
|
|
106
101
|
| UpgradeRunResponse
|
|
107
|
-
| GapReportCategoriesResponse
|
|
108
|
-
| GapReportFileResponse
|
|
109
102
|
| SearchResponse
|
|
110
103
|
| ManifestResponse
|
|
111
|
-
| DoctorResponse
|
|
104
|
+
| DoctorResponse
|
|
105
|
+
| ValidateIntegrationResponse;
|
|
112
106
|
|
|
113
107
|
/** Union of all type discriminator string literals. */
|
|
114
108
|
export type CLIResponseType = CLIAnyResponse['type'];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public type surface for the file-based codemod-authoring API exported from
|
|
5
|
+
* `@astryxdesign/cli/codemod`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A single source file presented to a codemod's transform. */
|
|
9
|
+
export interface AstryxCodemodFile {
|
|
10
|
+
/** Absolute path to the file being transformed. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** The current source contents of the file. */
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Helpers and context passed to a codemod's transform as the second argument. */
|
|
17
|
+
export interface AstryxCodemodApi {
|
|
18
|
+
/** A jscodeshift instance configured with a parser for the file. */
|
|
19
|
+
jscodeshift: unknown;
|
|
20
|
+
/** Report a statistic (no-op-friendly; provided for jscodeshift parity). */
|
|
21
|
+
stats: (...args: unknown[]) => void;
|
|
22
|
+
/** Report progress (no-op-friendly; provided for jscodeshift parity). */
|
|
23
|
+
report: (...args: unknown[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A codemod's transform. Return the new source to rewrite the file, or
|
|
28
|
+
* `null`/`undefined` to leave the file unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export type AstryxCodemodTransform = (
|
|
31
|
+
file: AstryxCodemodFile,
|
|
32
|
+
api: AstryxCodemodApi,
|
|
33
|
+
) => string | null | undefined;
|
|
34
|
+
|
|
35
|
+
/** Definition accepted by {@link createCodemod}. */
|
|
36
|
+
export interface AstryxCodemodDef {
|
|
37
|
+
/** Short, human-readable title shown in upgrade output. */
|
|
38
|
+
title: string;
|
|
39
|
+
/** Optional longer description. */
|
|
40
|
+
description?: string;
|
|
41
|
+
/** When true, the codemod runs only when explicitly requested. */
|
|
42
|
+
isOptional?: boolean;
|
|
43
|
+
/** File extensions this codemod applies to (e.g. ['.tsx', '.ts']). */
|
|
44
|
+
fileExtensions?: string[];
|
|
45
|
+
/** The transform function. */
|
|
46
|
+
transform: AstryxCodemodTransform;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Result of {@link createCodemod}. */
|
|
50
|
+
export interface AstryxCodemod extends AstryxCodemodDef {
|
|
51
|
+
isOptional: boolean;
|
|
52
|
+
type: 'code';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Definition accepted by {@link createConfigCodemod}. */
|
|
56
|
+
export interface AstryxConfigCodemodDef {
|
|
57
|
+
/** Short, human-readable title shown in upgrade output. */
|
|
58
|
+
title: string;
|
|
59
|
+
/** Optional longer description. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/** When true, the codemod runs only when explicitly requested. */
|
|
62
|
+
isOptional?: boolean;
|
|
63
|
+
/** The transform function applied to the astryx.config.* file. */
|
|
64
|
+
transform: AstryxCodemodTransform;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Result of {@link createConfigCodemod}. */
|
|
68
|
+
export interface AstryxConfigCodemod extends AstryxConfigCodemodDef {
|
|
69
|
+
isOptional: boolean;
|
|
70
|
+
type: 'config';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Define a file-transforming codemod. */
|
|
74
|
+
export declare function createCodemod<T extends AstryxCodemodDef>(
|
|
75
|
+
def: T,
|
|
76
|
+
): AstryxCodemod;
|
|
77
|
+
|
|
78
|
+
/** Define a codemod that targets the consumer's astryx.config.* file. */
|
|
79
|
+
export declare function createConfigCodemod<T extends AstryxConfigCodemodDef>(
|
|
80
|
+
def: T,
|
|
81
|
+
): AstryxConfigCodemod;
|