@astryxdesign/cli 0.4.5-canary.4aacbbe → 0.4.5-canary.5d8ece1
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/assets/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +154 -0
- package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +111 -9
- package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +914 -0
- package/assets/templates/pages/dashboard-cohort-funnel/template.doc.mjs +12 -0
- package/assets/templates/pages/dashboard-data/page.tsx +894 -0
- package/assets/templates/pages/dashboard-data/template.doc.mjs +13 -0
- package/assets/templates/pages/dashboard-executive-summary/page.tsx +874 -0
- package/assets/templates/pages/dashboard-executive-summary/template.doc.mjs +13 -0
- package/assets/templates/pages/dashboard-project-status/page.tsx +1139 -0
- package/assets/templates/pages/dashboard-project-status/template.doc.mjs +13 -0
- package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1590 -0
- package/assets/templates/pages/dashboard-service-monitoring/template.doc.mjs +12 -0
- package/authoring/doctypes/template/type.ts +2 -0
- package/package.json +9 -9
|
@@ -303,4 +303,158 @@ describe('drop-xds-prefix-imports', () => {
|
|
|
303
303
|
expect(output).not.toContain('AstryxButton');
|
|
304
304
|
expect(output).not.toContain('XDSButton');
|
|
305
305
|
});
|
|
306
|
+
|
|
307
|
+
describe('JSX whitespace preservation on element-name rename', () => {
|
|
308
|
+
it('preserves the space between text and a following {expression} inside return (...)', async () => {
|
|
309
|
+
const input = [
|
|
310
|
+
`import {XDSText} from '@xds/core';`,
|
|
311
|
+
`export function App({name}) {`,
|
|
312
|
+
` return (`,
|
|
313
|
+
` <XDSText>hello {name} world</XDSText>`,
|
|
314
|
+
` );`,
|
|
315
|
+
`}`,
|
|
316
|
+
].join('\n');
|
|
317
|
+
const output = await applyTransform(input);
|
|
318
|
+
// Element is renamed...
|
|
319
|
+
expect(output).toContain('<Text>');
|
|
320
|
+
expect(output).toContain('</Text>');
|
|
321
|
+
expect(output).not.toContain('XDSText');
|
|
322
|
+
// ...and the whitespace around the {expression} survives (the bug
|
|
323
|
+
// collapsed `{name} world` to `{name}world`).
|
|
324
|
+
expect(output).toContain('hello {name} world');
|
|
325
|
+
expect(output).not.toContain('{name}world');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('preserves whitespace with multiple {expressions} adjacent to text', async () => {
|
|
329
|
+
const input = [
|
|
330
|
+
`import {XDSText} from '@xds/core';`,
|
|
331
|
+
`export function App({a, b}) {`,
|
|
332
|
+
` return (`,
|
|
333
|
+
` <XDSText>involving {a} in the {b} dataset</XDSText>`,
|
|
334
|
+
` );`,
|
|
335
|
+
`}`,
|
|
336
|
+
].join('\n');
|
|
337
|
+
const output = await applyTransform(input);
|
|
338
|
+
expect(output).toContain('involving {a} in the {b} dataset');
|
|
339
|
+
expect(output).toContain('<Text>');
|
|
340
|
+
expect(output).not.toContain('XDSText');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('preserves whitespace across a multi-line JSX body', async () => {
|
|
344
|
+
const input = [
|
|
345
|
+
`import {XDSText} from '@xds/core';`,
|
|
346
|
+
`export function App({a, b}) {`,
|
|
347
|
+
` return (`,
|
|
348
|
+
` <XDSText>`,
|
|
349
|
+
` involving {a} in the {b} dataset`,
|
|
350
|
+
` </XDSText>`,
|
|
351
|
+
` );`,
|
|
352
|
+
`}`,
|
|
353
|
+
].join('\n');
|
|
354
|
+
const output = await applyTransform(input);
|
|
355
|
+
expect(output).toContain('involving {a} in the {b} dataset');
|
|
356
|
+
expect(output).toContain('<Text>');
|
|
357
|
+
expect(output).toContain('</Text>');
|
|
358
|
+
expect(output).not.toContain('XDSText');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('renames both the opening and closing tag of the same element', async () => {
|
|
362
|
+
const input = [
|
|
363
|
+
`import {XDSCard} from '@xds/core';`,
|
|
364
|
+
`export const App = ({n}) => (<XDSCard>value {n} here</XDSCard>);`,
|
|
365
|
+
].join('\n');
|
|
366
|
+
const output = await applyTransform(input);
|
|
367
|
+
expect(output).toContain('<Card>');
|
|
368
|
+
expect(output).toContain('</Card>');
|
|
369
|
+
expect(output).toContain('value {n} here');
|
|
370
|
+
expect(output).not.toContain('XDSCard');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('preserves whitespace for multiple different elements in one return', async () => {
|
|
374
|
+
const input = [
|
|
375
|
+
`import {XDSCard, XDSText, XDSButton} from '@xds/core';`,
|
|
376
|
+
`export const App = ({n}) => (`,
|
|
377
|
+
` <XDSCard>`,
|
|
378
|
+
` <XDSText>hello {n} world</XDSText>`,
|
|
379
|
+
` <XDSButton>click {n} here</XDSButton>`,
|
|
380
|
+
` </XDSCard>`,
|
|
381
|
+
`);`,
|
|
382
|
+
].join('\n');
|
|
383
|
+
const output = await applyTransform(input);
|
|
384
|
+
expect(output).toContain('import {Card, Text, Button}');
|
|
385
|
+
expect(output).toContain('<Card>');
|
|
386
|
+
expect(output).toContain('hello {n} world');
|
|
387
|
+
expect(output).toContain('click {n} here');
|
|
388
|
+
expect(output).not.toContain('XDS');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('renames only the mapped segment of a member-expression tag name, preserving whitespace', async () => {
|
|
392
|
+
const input = [
|
|
393
|
+
`import {XDSMenu} from '@xds/core';`,
|
|
394
|
+
`export const App = ({x}) => (<XDSMenu.Item>pick {x} now</XDSMenu.Item>);`,
|
|
395
|
+
].join('\n');
|
|
396
|
+
const output = await applyTransform(input);
|
|
397
|
+
// Only the mapped `XDSMenu` segment is renamed; `.Item` is preserved.
|
|
398
|
+
expect(output).toContain('<Menu.Item>');
|
|
399
|
+
expect(output).toContain('</Menu.Item>');
|
|
400
|
+
expect(output).toContain('pick {x} now');
|
|
401
|
+
expect(output).not.toContain('XDSMenu');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('renames a self-closing element with attributes without altering the attributes', async () => {
|
|
405
|
+
const input = [
|
|
406
|
+
`import {XDSButton} from '@xds/core';`,
|
|
407
|
+
`export const App = () => <XDSButton label="x" onClick={fn} />;`,
|
|
408
|
+
].join('\n');
|
|
409
|
+
const output = await applyTransform(input);
|
|
410
|
+
expect(output).toContain('<Button label="x" onClick={fn} />');
|
|
411
|
+
expect(output).not.toContain('XDSButton');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('handles a JSX body with no whitespace to preserve', async () => {
|
|
415
|
+
const input = [
|
|
416
|
+
`import {XDSButton} from '@xds/core';`,
|
|
417
|
+
`export const App = ({label}) => <XDSButton>{label}</XDSButton>;`,
|
|
418
|
+
].join('\n');
|
|
419
|
+
const output = await applyTransform(input);
|
|
420
|
+
expect(output).toContain('<Button>{label}</Button>');
|
|
421
|
+
expect(output).not.toContain('XDSButton');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('preserves whitespace when the bare name is aliased on a collision', async () => {
|
|
425
|
+
const input = [
|
|
426
|
+
`import {XDSCodeBlock} from '@xds/core/CodeBlock';`,
|
|
427
|
+
`export function CodeBlock({code}) {`,
|
|
428
|
+
` return (<XDSCodeBlock>run {code} now</XDSCodeBlock>);`,
|
|
429
|
+
`}`,
|
|
430
|
+
].join('\n');
|
|
431
|
+
const output = await applyTransform(input);
|
|
432
|
+
expect(output).toContain('CodeBlock as AstryxCodeBlock');
|
|
433
|
+
expect(output).toContain('<AstryxCodeBlock>');
|
|
434
|
+
expect(output).toContain('</AstryxCodeBlock>');
|
|
435
|
+
expect(output).toContain('run {code} now');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('keeps a custom local alias on the element tag and preserves whitespace', async () => {
|
|
439
|
+
const input = [
|
|
440
|
+
`import {XDSText as Txt} from '@xds/core';`,
|
|
441
|
+
`export const App = ({n}) => (<Txt>hello {n} world</Txt>);`,
|
|
442
|
+
].join('\n');
|
|
443
|
+
const output = await applyTransform(input);
|
|
444
|
+
expect(output).toContain('Text as Txt');
|
|
445
|
+
expect(output).toContain('<Txt>hello {n} world</Txt>');
|
|
446
|
+
expect(output).not.toContain('XDSText');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('is idempotent: a second pass over already-migrated output is a no-op', async () => {
|
|
450
|
+
const input = [
|
|
451
|
+
`import {XDSText} from '@xds/core';`,
|
|
452
|
+
`export const App = ({n}) => (<XDSText>a {n} b</XDSText>);`,
|
|
453
|
+
].join('\n');
|
|
454
|
+
const once = await applyTransform(input);
|
|
455
|
+
const twice = await applyTransform(once);
|
|
456
|
+
expect(twice).toBe(once);
|
|
457
|
+
expect(once).toContain('a {n} b');
|
|
458
|
+
});
|
|
459
|
+
});
|
|
306
460
|
});
|
|
@@ -346,18 +346,27 @@ export default function transformer(file, api) {
|
|
|
346
346
|
}
|
|
347
347
|
// Don't double-rewrite the import specifier we already handled.
|
|
348
348
|
if (parent.type === 'ImportSpecifier') return;
|
|
349
|
+
// JSX element tag names are handled by a source-string splice after
|
|
350
|
+
// `toSource()` (see renameJsxElementNamesInSource). Mutating them here
|
|
351
|
+
// would dirty the enclosing JSXElement and make recast collapse whitespace
|
|
352
|
+
// around adjacent `{expressions}`. Under the tsx parser a JSX tag-name
|
|
353
|
+
// JSXIdentifier is a subtype of Identifier, so it surfaces in this
|
|
354
|
+
// `find(j.Identifier)` pass and must be explicitly skipped.
|
|
355
|
+
if (isJsxElementNameIdentifier(path)) return;
|
|
349
356
|
path.node.name = newName;
|
|
350
357
|
hasChanges = true;
|
|
351
358
|
});
|
|
352
359
|
|
|
353
|
-
// JSX element names
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
360
|
+
// JSX element names (`<XDSButton>` -> `<Button>`) are intentionally NOT
|
|
361
|
+
// renamed via AST mutation here. Mutating a JSXIdentifier in place dirties the
|
|
362
|
+
// enclosing JSXElement; when that element is the argument of a parenthesized
|
|
363
|
+
// `return ( <JSX/> )` (i.e. every React component body), recast declines a
|
|
364
|
+
// surgical child reprint and generically re-prints the whole return argument.
|
|
365
|
+
// Its generic JSXElement printer drops significant whitespace from JSXText
|
|
366
|
+
// that sits next to a `{expression}` -- so `hello {name} world` collapses to
|
|
367
|
+
// `hello {name}world`. Instead we splice the element-name renames directly
|
|
368
|
+
// into the emitted source string (below), which never re-prints the subtree.
|
|
369
|
+
// See renameJsxElementNamesInSource for the mechanism.
|
|
361
370
|
|
|
362
371
|
// 5. Rename TS type references in generic type-argument positions.
|
|
363
372
|
//
|
|
@@ -413,5 +422,98 @@ export default function transformer(file, api) {
|
|
|
413
422
|
};
|
|
414
423
|
renameTypeReferences(root.get().node);
|
|
415
424
|
|
|
416
|
-
|
|
425
|
+
if (!hasChanges) return undefined;
|
|
426
|
+
|
|
427
|
+
// Emit the AST-based renames, then splice the JSX element-name renames onto
|
|
428
|
+
// the emitted string (see the note in the JSX section above for why element
|
|
429
|
+
// names cannot be renamed through the AST without collapsing whitespace).
|
|
430
|
+
const emitted = root.toSource();
|
|
431
|
+
return renameJsxElementNamesInSource(j, emitted, localRenames);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Rename JSX element tag names (`<XDSButton>` / `</XDSButton>`) by splicing the
|
|
436
|
+
* new names directly into the already-emitted source, so recast never re-prints
|
|
437
|
+
* the surrounding JSX subtree (which would strip whitespace next to
|
|
438
|
+
* `{expressions}`).
|
|
439
|
+
*
|
|
440
|
+
* Spans are computed by re-parsing `source` itself, so the offsets are always
|
|
441
|
+
* valid against the string we splice into -- no dependency on the original
|
|
442
|
+
* pre-transform offsets. Splices are applied right-to-left (descending start
|
|
443
|
+
* offset) so earlier offsets stay valid as we go.
|
|
444
|
+
*
|
|
445
|
+
* Only the identifier that IS the element's tag name is renamed:
|
|
446
|
+
* - `<Foo attr />` -> the `Foo` opening-name only (not `attr`)
|
|
447
|
+
* - `<Ns.Foo />` / `<Foo.Bar>`-> the specific member-expression identifier that
|
|
448
|
+
* maps in `renames`; the other part of the member name is preserved
|
|
449
|
+
*
|
|
450
|
+
* @param {any} j jscodeshift instance (already bound to the tsx parser)
|
|
451
|
+
* @param {string} source emitted source to rewrite
|
|
452
|
+
* @param {Map<string, string>} renames localName -> newLocalName
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
455
|
+
function renameJsxElementNamesInSource(j, source, renames) {
|
|
456
|
+
if (renames.size === 0) return source;
|
|
457
|
+
|
|
458
|
+
const root = j(source);
|
|
459
|
+
|
|
460
|
+
/** @type {Array<{start: number, end: number, name: string}>} */
|
|
461
|
+
const edits = [];
|
|
462
|
+
|
|
463
|
+
root.find(j.JSXIdentifier).forEach((/** @type {any} */ path) => {
|
|
464
|
+
const node = path.node;
|
|
465
|
+
const newName = renames.get(node.name);
|
|
466
|
+
if (!newName || newName === node.name) return;
|
|
467
|
+
|
|
468
|
+
// The identifier must BE an element tag name -- the `.name` of a
|
|
469
|
+
// JSXOpeningElement/JSXClosingElement, or the matching part of a
|
|
470
|
+
// JSXMemberExpression tag name. Attribute names, attribute values, and the
|
|
471
|
+
// non-matching half of a member-expression name must be left alone.
|
|
472
|
+
if (!isJsxElementNameIdentifier(path)) return;
|
|
473
|
+
|
|
474
|
+
if (typeof node.start !== 'number' || typeof node.end !== 'number') return;
|
|
475
|
+
edits.push({start: node.start, end: node.end, name: newName});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
if (edits.length === 0) return source;
|
|
479
|
+
|
|
480
|
+
edits.sort((a, b) => b.start - a.start);
|
|
481
|
+
|
|
482
|
+
let out = source;
|
|
483
|
+
for (const {start, end, name} of edits) {
|
|
484
|
+
out = out.slice(0, start) + name + out.slice(end);
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Is this JSXIdentifier path the tag name of its element (opening or closing),
|
|
491
|
+
* as opposed to an attribute name or a namespace/member-expression segment we
|
|
492
|
+
* should not rename? For member-expression tag names (`<Foo.Bar>`) we accept
|
|
493
|
+
* the identifier regardless of which segment it is -- the caller only reaches
|
|
494
|
+
* here for identifiers whose name is in the rename map, and each renamed
|
|
495
|
+
* identifier maps independently, so `<Foo.Bar>` renames only the segment(s)
|
|
496
|
+
* that actually map.
|
|
497
|
+
*
|
|
498
|
+
* @param {any} path jscodeshift path to a JSXIdentifier
|
|
499
|
+
* @returns {boolean}
|
|
500
|
+
*/
|
|
501
|
+
function isJsxElementNameIdentifier(path) {
|
|
502
|
+
const parent = path.parent && path.parent.node;
|
|
503
|
+
if (!parent) return false;
|
|
504
|
+
|
|
505
|
+
if (
|
|
506
|
+
parent.type === 'JSXOpeningElement' ||
|
|
507
|
+
parent.type === 'JSXClosingElement'
|
|
508
|
+
) {
|
|
509
|
+
return parent.name === path.node;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// `<Foo.Bar>` / `<A.B.C>`: the tag name is a JSXMemberExpression whose
|
|
513
|
+
// segments are JSXIdentifiers. Rename the specific matching segment only.
|
|
514
|
+
if (parent.type === 'JSXMemberExpression') {
|
|
515
|
+
return parent.object === path.node || parent.property === path.node;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return false;
|
|
417
519
|
}
|