@astryxdesign/cli 0.1.1-canary.a514b99 → 0.1.1-canary.afb6240
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/package.json +19 -8
- package/src/api/discover.mjs +78 -26
- package/src/api/layout.mjs +301 -0
- package/src/api/layout.test.mjs +238 -0
- package/src/api/template.mjs +191 -50
- package/src/api/template.test.mjs +2 -0
- package/src/commands/gap-report.mjs +17 -9
- package/src/commands/gap-report.test.mjs +21 -16
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +51 -23
- package/src/commands/upgrade.mjs +1 -70
- package/src/config.mjs +31 -0
- package/src/config.test.mjs +24 -0
- package/src/index.mjs +4 -0
- package/src/lib/config-schema.mjs +119 -0
- package/src/lib/config.mjs +34 -7
- package/src/lib/config.test.mjs +51 -2
- package/src/lib/error-codes.mjs +8 -0
- package/src/lib/integrations.mjs +155 -0
- package/src/lib/integrations.test.mjs +154 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +6 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +91 -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/types/config.d.ts +99 -0
- package/src/utils/github.mjs +12 -27
- 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/Slider/SliderShowcase.tsx +10 -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/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** User config exported from astryx.config.mjs. */
|
|
4
|
+
export interface AstryxConfig {
|
|
5
|
+
/** External package directories to scan for package.json astryx.docs metadata. */
|
|
6
|
+
packages?: string | string[];
|
|
7
|
+
/** Integration package names or manifest paths. */
|
|
8
|
+
integrations?: string | string[];
|
|
9
|
+
/** Gap report delivery override. Omit for default GitHub issue filing. */
|
|
10
|
+
gapReport?: false | {command: string};
|
|
11
|
+
/** Template hooks. */
|
|
12
|
+
template?: {
|
|
13
|
+
get?: (id: string) => string | Promise<string>;
|
|
14
|
+
};
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AstryxIntegrationCodemod {
|
|
19
|
+
name: string;
|
|
20
|
+
from?: string;
|
|
21
|
+
to?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
pr?: string;
|
|
25
|
+
optional?: boolean;
|
|
26
|
+
fileExtensions?: string[];
|
|
27
|
+
transform:
|
|
28
|
+
| string
|
|
29
|
+
| ((file: unknown, api: unknown) => string | undefined | null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AstryxPostCodemodContext {
|
|
33
|
+
packageDir: string;
|
|
34
|
+
codemodDir: string;
|
|
35
|
+
changedFiles: string[];
|
|
36
|
+
absoluteChangedFiles: string[];
|
|
37
|
+
packageChangedFiles: string[];
|
|
38
|
+
apply: boolean;
|
|
39
|
+
run: (
|
|
40
|
+
command: string,
|
|
41
|
+
args?: string[],
|
|
42
|
+
options?: {
|
|
43
|
+
cwd?: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
env?: Record<string, string>;
|
|
46
|
+
},
|
|
47
|
+
) => Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AstryxPostCodemodHook {
|
|
51
|
+
name?: string;
|
|
52
|
+
run?: (context: AstryxPostCodemodContext) => void | Promise<void>;
|
|
53
|
+
command?: (context: AstryxPostCodemodContext) =>
|
|
54
|
+
| {
|
|
55
|
+
command: string;
|
|
56
|
+
args?: string[];
|
|
57
|
+
cwd?: string;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
env?: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
| null
|
|
62
|
+
| undefined
|
|
63
|
+
| Promise<
|
|
64
|
+
| {
|
|
65
|
+
command: string;
|
|
66
|
+
args?: string[];
|
|
67
|
+
cwd?: string;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
env?: Record<string, string>;
|
|
70
|
+
}
|
|
71
|
+
| null
|
|
72
|
+
| undefined
|
|
73
|
+
>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Integration manifest exported from an astryx.integration.mjs file. */
|
|
77
|
+
export interface AstryxIntegration {
|
|
78
|
+
name: string;
|
|
79
|
+
version?: string;
|
|
80
|
+
displayName?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
/** Relative docs root containing *.doc.mjs files. */
|
|
83
|
+
docs?: string;
|
|
84
|
+
category?: string;
|
|
85
|
+
/** Relative block-template root. */
|
|
86
|
+
blocks?: string;
|
|
87
|
+
gapReport?: false | {command: string};
|
|
88
|
+
template?: {
|
|
89
|
+
get?: string | ((id: string) => string | Promise<string>);
|
|
90
|
+
};
|
|
91
|
+
codemods?: AstryxIntegrationCodemod[];
|
|
92
|
+
postCodemod?: AstryxPostCodemodHook[];
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export declare function createConfig<T extends AstryxConfig>(config: T): T;
|
|
97
|
+
export declare function createIntegration<T extends AstryxIntegration>(
|
|
98
|
+
integration: T,
|
|
99
|
+
): T;
|
package/src/utils/github.mjs
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import {execFileSync} from 'node:child_process';
|
|
19
|
-
import * as fs from 'node:fs';
|
|
20
19
|
import * as path from 'node:path';
|
|
20
|
+
import {loadConfig} from '../lib/config.mjs';
|
|
21
21
|
|
|
22
22
|
/** Default target repository for gap report issues. */
|
|
23
23
|
const DEFAULT_REPO = 'facebookexperimental/xds';
|
|
@@ -44,7 +44,7 @@ export const GAP_CATEGORIES = [
|
|
|
44
44
|
* - command: string → run this script instead of creating a GitHub issue
|
|
45
45
|
* - neither → default behavior (GitHub issue to facebookexperimental/xds)
|
|
46
46
|
*/
|
|
47
|
-
export function loadGapReportConfig() {
|
|
47
|
+
export async function loadGapReportConfig() {
|
|
48
48
|
// 1. Check environment variable
|
|
49
49
|
const envVar = process.env.ASTRYX_GAP_REPORT;
|
|
50
50
|
if (envVar) {
|
|
@@ -55,27 +55,12 @@ export function loadGapReportConfig() {
|
|
|
55
55
|
return {enabled: true, command: envVar};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// 2. Check astryx.config.mjs
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (match) {
|
|
65
|
-
const value = match[1].trim();
|
|
66
|
-
if (value === 'false') {
|
|
67
|
-
return {enabled: false};
|
|
68
|
-
}
|
|
69
|
-
// Look for command property in object literal
|
|
70
|
-
const cmdMatch = content.match(
|
|
71
|
-
/gapReport\s*:\s*\{[^}]*command\s*:\s*['"](.+?)['"]/s,
|
|
72
|
-
);
|
|
73
|
-
if (cmdMatch) {
|
|
74
|
-
return {enabled: true, command: cmdMatch[1]};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
// Config parse error — fall through to defaults
|
|
58
|
+
// 2. Check astryx.config.mjs + integration manifests
|
|
59
|
+
const config = await loadConfig(process.cwd());
|
|
60
|
+
if (config.gapReport !== undefined) {
|
|
61
|
+
if (config.gapReport === false) return {enabled: false};
|
|
62
|
+
if (config.gapReport?.command) {
|
|
63
|
+
return {enabled: true, command: config.gapReport.command};
|
|
79
64
|
}
|
|
80
65
|
}
|
|
81
66
|
|
|
@@ -172,14 +157,14 @@ function runCustomCommand(command, report) {
|
|
|
172
157
|
* repo: string,
|
|
173
158
|
* }}
|
|
174
159
|
*/
|
|
175
|
-
export function buildGapReportPreview({
|
|
160
|
+
export async function buildGapReportPreview({
|
|
176
161
|
component,
|
|
177
162
|
category,
|
|
178
163
|
intention,
|
|
179
164
|
detail,
|
|
180
165
|
source = 'cli',
|
|
181
166
|
}) {
|
|
182
|
-
const config = loadGapReportConfig();
|
|
167
|
+
const config = await loadGapReportConfig();
|
|
183
168
|
const categoryLabel =
|
|
184
169
|
GAP_CATEGORIES.find(c => c.value === category)?.label ?? category;
|
|
185
170
|
|
|
@@ -238,8 +223,8 @@ export function buildGapReportPreview({
|
|
|
238
223
|
* @param {string} [opts.source] Who filed it: "interactive", "llm-auto", "cli"
|
|
239
224
|
* @returns {string|null} URL of the created issue (or custom command output), null if disabled
|
|
240
225
|
*/
|
|
241
|
-
export function createGapReport(opts) {
|
|
242
|
-
const preview = buildGapReportPreview(opts);
|
|
226
|
+
export async function createGapReport(opts) {
|
|
227
|
+
const preview = await buildGapReportPreview(opts);
|
|
243
228
|
|
|
244
229
|
if (!preview.enabled) {
|
|
245
230
|
return null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'CommandPaletteEmpty',
|
|
7
|
+
name: 'CommandPaletteEmpty',
|
|
8
|
+
displayName: 'Command Palette Empty',
|
|
9
|
+
description:
|
|
10
|
+
'Command palette empty state shown when no commands are available.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
isShowcase: true,
|
|
13
|
+
aspectRatio: 4 / 3,
|
|
14
|
+
componentsUsed: ['CommandPalette', 'CommandPaletteEmpty', 'Text'],
|
|
15
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useMemo} from 'react';
|
|
6
|
+
import {CommandPalette} from '@astryxdesign/core/CommandPalette';
|
|
7
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
8
|
+
import {createStaticSource} from '@astryxdesign/core/Typeahead';
|
|
9
|
+
|
|
10
|
+
export default function CommandPaletteEmptyShowcase() {
|
|
11
|
+
const emptySource = useMemo(() => createStaticSource([]), []);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<CommandPalette
|
|
15
|
+
isOpen
|
|
16
|
+
isInline
|
|
17
|
+
onOpenChange={() => {}}
|
|
18
|
+
searchSource={emptySource}
|
|
19
|
+
emptyBootstrapText={
|
|
20
|
+
<Text type="supporting" color="secondary">
|
|
21
|
+
No commands available yet
|
|
22
|
+
</Text>
|
|
23
|
+
}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
export const doc = {
|
|
5
5
|
type: 'block',
|
|
6
6
|
exampleFor: 'DateInput',
|
|
7
|
-
name: 'DateInput —
|
|
8
|
-
displayName: 'DateInput —
|
|
7
|
+
name: 'DateInput — Min/Max Constraints',
|
|
8
|
+
displayName: 'DateInput — Min/Max Constraints',
|
|
9
9
|
description:
|
|
10
10
|
'Date input constrained to a min/max window. Use when only certain dates are valid, like booking availability or a fiscal quarter.',
|
|
11
11
|
isReady: true,
|
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
'use client';
|
|
4
4
|
|
|
5
|
+
import {useState} from 'react';
|
|
5
6
|
import {Slider} from '@astryxdesign/core/Slider';
|
|
6
7
|
|
|
7
8
|
export default function SliderShowcase() {
|
|
8
|
-
|
|
9
|
+
const [value, setValue] = useState(50);
|
|
10
|
+
return (
|
|
11
|
+
<Slider
|
|
12
|
+
label="Volume"
|
|
13
|
+
value={value}
|
|
14
|
+
onChange={setValue}
|
|
15
|
+
style={{width: 300}}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
9
18
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'useTableColumnResize',
|
|
7
|
+
name: 'useTableColumnResize — Draggable Columns',
|
|
8
|
+
displayName: 'useTableColumnResize — Draggable Columns',
|
|
9
|
+
description:
|
|
10
|
+
'A Table using useTableColumnResize. Drag the right edge of any column header to resize; widths are committed on release. The last proportional column flexes to fill remaining space.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 16 / 9,
|
|
13
|
+
componentsUsed: ['Table'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {
|
|
7
|
+
Table,
|
|
8
|
+
useTableColumnResize,
|
|
9
|
+
proportional,
|
|
10
|
+
pixel,
|
|
11
|
+
} from '@astryxdesign/core/Table';
|
|
12
|
+
|
|
13
|
+
interface User extends Record<string, unknown> {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
role: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const users: User[] = [
|
|
21
|
+
{
|
|
22
|
+
id: '1',
|
|
23
|
+
name: 'Alice Johnson',
|
|
24
|
+
email: 'alice@example.com',
|
|
25
|
+
role: 'Engineer',
|
|
26
|
+
},
|
|
27
|
+
{id: '2', name: 'Bob Smith', email: 'bob@example.com', role: 'Designer'},
|
|
28
|
+
{id: '3', name: 'Charlie Brown', email: 'charlie@example.com', role: 'PM'},
|
|
29
|
+
{id: '4', name: 'Diana Prince', email: 'diana@example.com', role: 'Engineer'},
|
|
30
|
+
{id: '5', name: 'Eve Davis', email: 'eve@example.com', role: 'Analyst'},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const columns = [
|
|
34
|
+
{key: 'name', header: 'Name'},
|
|
35
|
+
{key: 'email', header: 'Email', width: proportional(2)},
|
|
36
|
+
{key: 'role', header: 'Role', width: pixel(120)},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default function ColumnResizeHookUsage() {
|
|
40
|
+
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
41
|
+
|
|
42
|
+
const columnResize = useTableColumnResize({
|
|
43
|
+
columnWidths,
|
|
44
|
+
columns,
|
|
45
|
+
onColumnResizeEnd: updates => {
|
|
46
|
+
setColumnWidths(prev => ({...prev, ...updates}));
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Table
|
|
52
|
+
data={users}
|
|
53
|
+
columns={columns}
|
|
54
|
+
idKey="id"
|
|
55
|
+
hasHover
|
|
56
|
+
plugins={{columnResize}}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
|
|
4
|
+
export const doc = {
|
|
5
|
+
type: 'block',
|
|
6
|
+
exampleFor: 'useTableStickyColumns',
|
|
7
|
+
name: 'useTableStickyColumns — Pinned Columns',
|
|
8
|
+
displayName: 'useTableStickyColumns — Pinned Columns',
|
|
9
|
+
description:
|
|
10
|
+
'A Table using useTableStickyColumns to pin the Name column to the start edge and Status to the end edge. Scroll horizontally — pinned columns stay in view with a soft shadow over the scrolling content.',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 16 / 9,
|
|
13
|
+
componentsUsed: ['Table'],
|
|
14
|
+
};
|