@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,120 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyTransform(source) {
|
|
6
|
+
const {default: transform} =
|
|
7
|
+
await import('../rename-text-color-active-to-accent.mjs');
|
|
8
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
9
|
+
const j = jscodeshift.withParser('tsx');
|
|
10
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
11
|
+
const file = {source, path: 'test.tsx'};
|
|
12
|
+
const result = transform(file, api);
|
|
13
|
+
return result ?? source;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('rename-text-color-active-to-accent', () => {
|
|
17
|
+
it('renames Text color="active" to color="accent"', async () => {
|
|
18
|
+
const input = `<Text color="active">Hi</Text>`;
|
|
19
|
+
const output = await applyTransform(input);
|
|
20
|
+
expect(output).toContain("'accent'");
|
|
21
|
+
expect(output).not.toContain('active');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renames Heading color="active" to color="accent"', async () => {
|
|
25
|
+
const input = `<Heading level={2} color="active">Title</Heading>`;
|
|
26
|
+
const output = await applyTransform(input);
|
|
27
|
+
expect(output).toContain("'accent'");
|
|
28
|
+
expect(output).not.toContain('active');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renames Link color="active" to color="accent"', async () => {
|
|
32
|
+
const input = `<Link href="/x" color="active">Go</Link>`;
|
|
33
|
+
const output = await applyTransform(input);
|
|
34
|
+
expect(output).toContain("'accent'");
|
|
35
|
+
expect(output).not.toContain('active');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renames Timestamp color="active" to color="accent"', async () => {
|
|
39
|
+
const input = `<Timestamp value={d} color="active" />`;
|
|
40
|
+
const output = await applyTransform(input);
|
|
41
|
+
expect(output).toContain("'accent'");
|
|
42
|
+
expect(output).not.toContain('active');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renames expression-container color={'active'}", async () => {
|
|
46
|
+
const input = `<Text color={'active'}>Hi</Text>`;
|
|
47
|
+
const output = await applyTransform(input);
|
|
48
|
+
expect(output).toContain("'accent'");
|
|
49
|
+
expect(output).not.toContain("'active'");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renames active inside a ternary color expression', async () => {
|
|
53
|
+
const input = `<Text color={isOn ? 'active' : 'secondary'}>Hi</Text>`;
|
|
54
|
+
const output = await applyTransform(input);
|
|
55
|
+
expect(output).toContain("'accent'");
|
|
56
|
+
expect(output).toContain("'secondary'");
|
|
57
|
+
expect(output).not.toContain("'active'");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does not change other color values', async () => {
|
|
61
|
+
const input = `<Text color="secondary">Hi</Text>`;
|
|
62
|
+
const output = await applyTransform(input);
|
|
63
|
+
expect(output).toBe(input);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not touch color="active" on non-target components', async () => {
|
|
67
|
+
const input = `<StatusDot color="active" />`;
|
|
68
|
+
const output = await applyTransform(input);
|
|
69
|
+
expect(output).toBe(input);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('does not touch unrelated "active" string literals', async () => {
|
|
73
|
+
const input = `const node = {id: 'active', label: 'Active Users'};`;
|
|
74
|
+
const output = await applyTransform(input);
|
|
75
|
+
expect(output).toBe(input);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not touch a status enum value named active', async () => {
|
|
79
|
+
const input = `const filters = {status: 'active'};`;
|
|
80
|
+
const output = await applyTransform(input);
|
|
81
|
+
expect(output).toBe(input);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renames object color prop in files importing a target component', async () => {
|
|
85
|
+
const input = `import {Text} from '@xds/core/Text';
|
|
86
|
+
const cfg = {color: 'active'};`;
|
|
87
|
+
const output = await applyTransform(input);
|
|
88
|
+
expect(output).toContain("color: 'accent'");
|
|
89
|
+
expect(output).not.toContain("'active'");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renames color: 'active' as const in files importing a target component", async () => {
|
|
93
|
+
const input = `import {Text} from '@xds/core/Text';
|
|
94
|
+
const COLORS = [{color: 'active' as const, description: 'Accent'}];`;
|
|
95
|
+
const output = await applyTransform(input);
|
|
96
|
+
expect(output).toContain("'accent' as const");
|
|
97
|
+
expect(output).not.toContain("'active'");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not rename object color prop when no target component is imported', async () => {
|
|
101
|
+
const input = `const cfg = {color: 'active'};`;
|
|
102
|
+
const output = await applyTransform(input);
|
|
103
|
+
expect(output).toBe(input);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('renames active in Storybook color argTypes options', async () => {
|
|
107
|
+
const input = `import {Text} from '@xds/core/Text';
|
|
108
|
+
const meta = {argTypes: {color: {control: 'select', options: ['primary', 'active', 'inherit']}}};`;
|
|
109
|
+
const output = await applyTransform(input);
|
|
110
|
+
expect(output).toContain("'accent'");
|
|
111
|
+
expect(output).not.toContain("'active'");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns undefined (no change) when nothing matches', async () => {
|
|
115
|
+
const input = `<Text color="primary">Hi</Text>`;
|
|
116
|
+
const output = await applyTransform(input);
|
|
117
|
+
// applyTransform falls back to source when transform returns undefined
|
|
118
|
+
expect(output).toBe(input);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file v0.1.2 transform manifest
|
|
5
|
+
*
|
|
6
|
+
* Lists all codemods for the v0.1.2 release in the order they should run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import renameTextColorActiveToAccent, {
|
|
10
|
+
meta as renameTextColorActiveToAccentMeta,
|
|
11
|
+
} from './rename-text-color-active-to-accent.mjs';
|
|
12
|
+
|
|
13
|
+
export default [
|
|
14
|
+
{
|
|
15
|
+
name: 'rename-text-color-active-to-accent',
|
|
16
|
+
transform: renameTextColorActiveToAccent,
|
|
17
|
+
meta: renameTextColorActiveToAccentMeta,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: Rename Text/Heading/Link/Timestamp color "active" to "accent"
|
|
5
|
+
* @see https://github.com/facebook/astryx/issues/2863
|
|
6
|
+
*
|
|
7
|
+
* The `active` text-color value is renamed to `accent` and now maps to the
|
|
8
|
+
* dedicated `--color-text-accent` token (legible accent ink) instead of
|
|
9
|
+
* `--color-accent` (the interactive/base accent surface). This gives a
|
|
10
|
+
* first-class, type-safe way to render accent-colored text and resolves the
|
|
11
|
+
* gap reported in #2863.
|
|
12
|
+
*
|
|
13
|
+
* Transforms (scoped to Text, Heading, Link, and Timestamp):
|
|
14
|
+
* - <Text color="active" /> → <Text color="accent" />
|
|
15
|
+
* - <Heading color={'active'} /> → <Heading color={'accent'} />
|
|
16
|
+
* - <Text color={cond ? 'active' : x}> → <Text color={cond ? 'accent' : x}>
|
|
17
|
+
* - Storybook argTypes: color: { options: [..., 'active', ...] }
|
|
18
|
+
* → replaces 'active' with 'accent' (files importing a target component)
|
|
19
|
+
* - Object properties: { color: 'active' } / { color: 'active' as const }
|
|
20
|
+
* → { color: 'accent' } (files importing a target component)
|
|
21
|
+
*
|
|
22
|
+
* The transform deliberately does NOT touch bare `'active'` strings that are
|
|
23
|
+
* not a `color` value (e.g. status enums, ids, filter values), so it is safe
|
|
24
|
+
* to run across an entire codebase.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const meta = {
|
|
28
|
+
title: 'Rename Text/Heading/Link/Timestamp color "active" to "accent"',
|
|
29
|
+
description:
|
|
30
|
+
'Renames the `color="active"` value to `color="accent"` on Text, Heading, ' +
|
|
31
|
+
'Link, and Timestamp. The `accent` value maps to `--color-text-accent` ' +
|
|
32
|
+
'(the dedicated accent text ink). Also updates Storybook color argTypes ' +
|
|
33
|
+
'options and object-literal `color` props in files that import a target component.',
|
|
34
|
+
pr: '#2863',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const OLD_VALUE = 'active';
|
|
38
|
+
const NEW_VALUE = 'accent';
|
|
39
|
+
|
|
40
|
+
/** Components whose `color` prop accepts a TextColor value. */
|
|
41
|
+
const TARGET_COMPONENTS = new Set(['Text', 'Heading', 'Link', 'Timestamp']);
|
|
42
|
+
|
|
43
|
+
export default function transformer(file, api) {
|
|
44
|
+
const j = api.jscodeshift;
|
|
45
|
+
const root = j(file.source);
|
|
46
|
+
let hasChanges = false;
|
|
47
|
+
|
|
48
|
+
/** Rewrite a string-literal node when it equals the old value. */
|
|
49
|
+
function renameStringLiteral(node) {
|
|
50
|
+
if (!node) return false;
|
|
51
|
+
if (
|
|
52
|
+
(node.type === 'StringLiteral' || node.type === 'Literal') &&
|
|
53
|
+
node.value === OLD_VALUE
|
|
54
|
+
) {
|
|
55
|
+
node.value = NEW_VALUE;
|
|
56
|
+
if (node.raw) node.raw = `'${NEW_VALUE}'`;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Recursively rewrite the old value inside ternary/logical expressions. */
|
|
63
|
+
function renameInExpression(node) {
|
|
64
|
+
if (!node) return false;
|
|
65
|
+
let changed = false;
|
|
66
|
+
if (node.type === 'StringLiteral' || node.type === 'Literal') {
|
|
67
|
+
changed = renameStringLiteral(node) || changed;
|
|
68
|
+
} else if (node.type === 'ConditionalExpression') {
|
|
69
|
+
changed = renameInExpression(node.consequent) || changed;
|
|
70
|
+
changed = renameInExpression(node.alternate) || changed;
|
|
71
|
+
} else if (node.type === 'LogicalExpression') {
|
|
72
|
+
changed = renameInExpression(node.left) || changed;
|
|
73
|
+
changed = renameInExpression(node.right) || changed;
|
|
74
|
+
} else if (node.type === 'TSAsExpression') {
|
|
75
|
+
// `'active' as const` → rewrite the inner expression
|
|
76
|
+
changed = renameInExpression(node.expression) || changed;
|
|
77
|
+
}
|
|
78
|
+
return changed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- 1. JSX attribute: color="active" / color={'active'} on target components ---
|
|
82
|
+
root.find(j.JSXOpeningElement).forEach(path => {
|
|
83
|
+
const name = path.node.name;
|
|
84
|
+
const componentName = name.type === 'JSXIdentifier' ? name.name : null;
|
|
85
|
+
if (!componentName || !TARGET_COMPONENTS.has(componentName)) return;
|
|
86
|
+
|
|
87
|
+
path.node.attributes.forEach(attr => {
|
|
88
|
+
if (attr.type !== 'JSXAttribute') return;
|
|
89
|
+
if (!attr.name || attr.name.name !== 'color') return;
|
|
90
|
+
|
|
91
|
+
const value = attr.value;
|
|
92
|
+
if (!value) return;
|
|
93
|
+
|
|
94
|
+
if (value.type === 'StringLiteral' || value.type === 'Literal') {
|
|
95
|
+
if (renameStringLiteral(value)) hasChanges = true;
|
|
96
|
+
} else if (value.type === 'JSXExpressionContainer') {
|
|
97
|
+
if (renameInExpression(value.expression)) hasChanges = true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Object-property / argTypes transforms only run in files that use a target
|
|
103
|
+
// component — keeps unrelated `{ color: 'active' }` / `'active'` strings safe.
|
|
104
|
+
const importsTarget =
|
|
105
|
+
root
|
|
106
|
+
.find(j.ImportSpecifier)
|
|
107
|
+
.filter(p => TARGET_COMPONENTS.has(p.node.imported?.name))
|
|
108
|
+
.size() > 0;
|
|
109
|
+
|
|
110
|
+
if (importsTarget) {
|
|
111
|
+
const PropertyType = j.ObjectProperty ?? j.Property;
|
|
112
|
+
|
|
113
|
+
// --- 2. Object property: { color: 'active' } / { color: 'active' as const } ---
|
|
114
|
+
root.find(PropertyType, {key: {name: 'color'}}).forEach(path => {
|
|
115
|
+
if (renameInExpression(path.node.value)) hasChanges = true;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// --- 3. Storybook argTypes: color: { options: [..., 'active', ...] } ---
|
|
119
|
+
root.find(PropertyType, {key: {name: 'color'}}).forEach(path => {
|
|
120
|
+
const value = path.node.value;
|
|
121
|
+
if (!value || value.type !== 'ObjectExpression') return;
|
|
122
|
+
|
|
123
|
+
const optionsProp = value.properties.find(
|
|
124
|
+
p => p.key && (p.key.name === 'options' || p.key.value === 'options'),
|
|
125
|
+
);
|
|
126
|
+
if (optionsProp && optionsProp.value.type === 'ArrayExpression') {
|
|
127
|
+
optionsProp.value.elements.forEach(el => {
|
|
128
|
+
if (renameStringLiteral(el)) hasChanges = true;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!hasChanges) return undefined;
|
|
135
|
+
return root.toSource({quote: 'single'});
|
|
136
|
+
}
|
package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Invoke the transform directly with a configured jscodeshift api, mirroring
|
|
9
|
+
* how other transform tests (and `runConfigCodemod`) drive transforms via the
|
|
10
|
+
* unified `(file, api) => string | null | undefined` contract.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} source
|
|
13
|
+
* @param {string} [filePath]
|
|
14
|
+
* @returns {Promise<string|null>} the rewritten source, or null for no-op
|
|
15
|
+
*/
|
|
16
|
+
async function applyTransform(source, filePath = 'astryx.config.mjs') {
|
|
17
|
+
const {default: transform} = await import(
|
|
18
|
+
'../migrate-layout-components-to-experimental.mjs'
|
|
19
|
+
);
|
|
20
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
21
|
+
const j = jscodeshift.withParser('babel');
|
|
22
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
23
|
+
return transform({source, path: filePath}, api);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('migrate-layout-components-to-experimental — meta', () => {
|
|
27
|
+
it('declares itself as a config codemod', async () => {
|
|
28
|
+
const {meta} = await import(
|
|
29
|
+
'../migrate-layout-components-to-experimental.mjs'
|
|
30
|
+
);
|
|
31
|
+
expect(meta.codemodType).toBe('config');
|
|
32
|
+
expect(meta.title).toMatch(/layout\.components/);
|
|
33
|
+
expect(typeof meta.description).toBe('string');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('migrate-layout-components-to-experimental — string → {from}', () => {
|
|
38
|
+
it('normalizes a string entry to { from } (named import semantics)', async () => {
|
|
39
|
+
const input = `export default {
|
|
40
|
+
layout: {
|
|
41
|
+
components: {
|
|
42
|
+
KpiCard: '@/components/KpiCard',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
`;
|
|
47
|
+
const output = await applyTransform(input);
|
|
48
|
+
expect(output).not.toBeNull();
|
|
49
|
+
// No longer under layout.
|
|
50
|
+
expect(output).not.toMatch(/layout\s*:/);
|
|
51
|
+
// Relocated under experimental.xle.components as { from }.
|
|
52
|
+
expect(output).toContain('experimental');
|
|
53
|
+
expect(output).toContain('xle');
|
|
54
|
+
expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('normalizes multiple string entries', async () => {
|
|
58
|
+
const input = `export default {
|
|
59
|
+
layout: {
|
|
60
|
+
components: {
|
|
61
|
+
KpiCard: '@/components/KpiCard',
|
|
62
|
+
Banner: '@/components/Banner',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
`;
|
|
67
|
+
const output = await applyTransform(input);
|
|
68
|
+
expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
|
|
69
|
+
expect(output).toMatch(/Banner:\s*\{\s*from:\s*['"]@\/components\/Banner['"]/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('migrate-layout-components-to-experimental — object entries carried unchanged', () => {
|
|
74
|
+
it('preserves default: true (default import semantics)', async () => {
|
|
75
|
+
const input = `export default {
|
|
76
|
+
layout: {
|
|
77
|
+
components: {
|
|
78
|
+
Chart: { from: '@/c/Chart', default: true },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
`;
|
|
83
|
+
const output = await applyTransform(input);
|
|
84
|
+
expect(output).toContain("from: '@/c/Chart'");
|
|
85
|
+
expect(output).toContain('default: true');
|
|
86
|
+
expect(output).toContain('experimental');
|
|
87
|
+
expect(output).toContain('xle');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('preserves description', async () => {
|
|
91
|
+
const input = `export default {
|
|
92
|
+
layout: {
|
|
93
|
+
components: {
|
|
94
|
+
Chart: { from: '@/c/Chart', description: 'A chart' },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
`;
|
|
99
|
+
const output = await applyTransform(input);
|
|
100
|
+
expect(output).toContain("from: '@/c/Chart'");
|
|
101
|
+
expect(output).toContain("description: 'A chart'");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('preserves from/description/default and key order', async () => {
|
|
105
|
+
const input = `export default {
|
|
106
|
+
layout: {
|
|
107
|
+
components: {
|
|
108
|
+
Chart: { from: '@/c/Chart', description: 'A chart', default: true },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
`;
|
|
113
|
+
const output = await applyTransform(input);
|
|
114
|
+
// Order: from, then description, then default.
|
|
115
|
+
expect(output).toMatch(
|
|
116
|
+
/from:\s*'@\/c\/Chart',\s*description:\s*'A chart',\s*default:\s*true/,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('migrates a mix of string and object entries', async () => {
|
|
121
|
+
const input = `export default {
|
|
122
|
+
layout: {
|
|
123
|
+
components: {
|
|
124
|
+
KpiCard: '@/components/KpiCard',
|
|
125
|
+
Chart: { from: '@/c/Chart', default: true },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
`;
|
|
130
|
+
const output = await applyTransform(input);
|
|
131
|
+
expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
|
|
132
|
+
expect(output).toContain("from: '@/c/Chart'");
|
|
133
|
+
expect(output).toContain('default: true');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('migrate-layout-components-to-experimental — wrapper forms', () => {
|
|
138
|
+
it('handles bare object literal default export', async () => {
|
|
139
|
+
const input = `export default {
|
|
140
|
+
layout: { components: { KpiCard: '@/components/KpiCard' } },
|
|
141
|
+
};
|
|
142
|
+
`;
|
|
143
|
+
const output = await applyTransform(input);
|
|
144
|
+
expect(output).toContain('experimental');
|
|
145
|
+
expect(output).toContain('xle');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('handles createConfig({ ... }) wrapper', async () => {
|
|
149
|
+
const input = `import { createConfig } from '@astryxdesign/cli';
|
|
150
|
+
|
|
151
|
+
export default createConfig({
|
|
152
|
+
layout: { components: { KpiCard: '@/components/KpiCard' } },
|
|
153
|
+
});
|
|
154
|
+
`;
|
|
155
|
+
const output = await applyTransform(input);
|
|
156
|
+
expect(output).not.toBeNull();
|
|
157
|
+
expect(output).toContain('createConfig(');
|
|
158
|
+
expect(output).toContain('experimental');
|
|
159
|
+
expect(output).toContain('xle');
|
|
160
|
+
expect(output).toMatch(/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/);
|
|
161
|
+
expect(output).not.toMatch(/layout\s*:/);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('migrate-layout-components-to-experimental — placement', () => {
|
|
166
|
+
it('adds xle to a pre-existing experimental (no xle)', async () => {
|
|
167
|
+
const input = `export default {
|
|
168
|
+
experimental: { foo: true },
|
|
169
|
+
layout: { components: { KpiCard: '@/components/KpiCard' } },
|
|
170
|
+
};
|
|
171
|
+
`;
|
|
172
|
+
const output = await applyTransform(input);
|
|
173
|
+
expect(output).toContain('foo: true');
|
|
174
|
+
expect(output).toContain('xle');
|
|
175
|
+
expect(output).toMatch(/KpiCard:\s*\{\s*from:/);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('migrate-layout-components-to-experimental — no-op cases', () => {
|
|
180
|
+
it('returns null when there is no layout', async () => {
|
|
181
|
+
const input = `export default { theme: 'dark' };\n`;
|
|
182
|
+
const output = await applyTransform(input);
|
|
183
|
+
expect(output).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('returns null when layout has no components', async () => {
|
|
187
|
+
const input = `export default { layout: { foo: 1 } };\n`;
|
|
188
|
+
// layout exists but has no components → no-op (return null) BEFORE the
|
|
189
|
+
// "extra key" check, because there is nothing to migrate.
|
|
190
|
+
const output = await applyTransform(input);
|
|
191
|
+
expect(output).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns null on an already-migrated config (idempotence-ish)', async () => {
|
|
195
|
+
const input = `export default {
|
|
196
|
+
experimental: { xle: { components: { KpiCard: { from: '@/components/KpiCard' } } } },
|
|
197
|
+
};
|
|
198
|
+
`;
|
|
199
|
+
const output = await applyTransform(input);
|
|
200
|
+
expect(output).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('migrate-layout-components-to-experimental — bail (throw) cases', () => {
|
|
205
|
+
it('throws when experimental.xle already exists', async () => {
|
|
206
|
+
const input = `export default {
|
|
207
|
+
experimental: { xle: { components: {} } },
|
|
208
|
+
layout: { components: { KpiCard: '@/components/KpiCard' } },
|
|
209
|
+
};
|
|
210
|
+
`;
|
|
211
|
+
await expect(applyTransform(input)).rejects.toThrow(/experimental\.xle/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('throws when layout has an extra key besides components', async () => {
|
|
215
|
+
const input = `export default {
|
|
216
|
+
layout: {
|
|
217
|
+
components: { KpiCard: '@/components/KpiCard' },
|
|
218
|
+
spacing: 8,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
`;
|
|
222
|
+
await expect(applyTransform(input)).rejects.toThrow(/other than/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('throws when an entry value is an identifier', async () => {
|
|
226
|
+
const input = `const Kpi = '@/components/KpiCard';
|
|
227
|
+
export default {
|
|
228
|
+
layout: { components: { KpiCard: Kpi } },
|
|
229
|
+
};
|
|
230
|
+
`;
|
|
231
|
+
await expect(applyTransform(input)).rejects.toThrow(/cannot be safely migrated/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('throws when an entry value is a call expression', async () => {
|
|
235
|
+
const input = `export default {
|
|
236
|
+
layout: { components: { KpiCard: resolve('@/components/KpiCard') } },
|
|
237
|
+
};
|
|
238
|
+
`;
|
|
239
|
+
await expect(applyTransform(input)).rejects.toThrow(/cannot be safely migrated/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('throws on a non-object default export (variable reference)', async () => {
|
|
243
|
+
const input = `const config = { layout: { components: {} } };
|
|
244
|
+
export default config;
|
|
245
|
+
`;
|
|
246
|
+
await expect(applyTransform(input)).rejects.toThrow(/statically analyze/);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('migrate-layout-components-to-experimental — end-to-end via runConfigCodemod', () => {
|
|
251
|
+
it('rewrites astryx.config.mjs on disk through the unified runner', async () => {
|
|
252
|
+
const {runConfigCodemod} = await import('../../../run-codemod.mjs');
|
|
253
|
+
const {default: transform, meta} = await import(
|
|
254
|
+
'../migrate-layout-components-to-experimental.mjs'
|
|
255
|
+
);
|
|
256
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
257
|
+
|
|
258
|
+
const originalCwd = process.cwd();
|
|
259
|
+
const tmpDir = fs.mkdtempSync(
|
|
260
|
+
path.join(process.cwd(), '.astryx-v013-codemod-test-'),
|
|
261
|
+
);
|
|
262
|
+
try {
|
|
263
|
+
process.chdir(tmpDir);
|
|
264
|
+
fs.writeFileSync(
|
|
265
|
+
path.join(tmpDir, 'package.json'),
|
|
266
|
+
JSON.stringify({name: 'consumer'}),
|
|
267
|
+
);
|
|
268
|
+
fs.writeFileSync(
|
|
269
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
270
|
+
`export default {
|
|
271
|
+
layout: { components: { KpiCard: '@/components/KpiCard' } },
|
|
272
|
+
};
|
|
273
|
+
`,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const entry = {
|
|
277
|
+
id: 'migrate-layout-components-to-experimental',
|
|
278
|
+
type: 'config',
|
|
279
|
+
codemod: {title: meta.title, transform},
|
|
280
|
+
package: 'core',
|
|
281
|
+
version: '0.1.3',
|
|
282
|
+
};
|
|
283
|
+
const log = {
|
|
284
|
+
step() {},
|
|
285
|
+
info() {},
|
|
286
|
+
success() {},
|
|
287
|
+
warn() {},
|
|
288
|
+
error() {},
|
|
289
|
+
message() {},
|
|
290
|
+
};
|
|
291
|
+
const result = runConfigCodemod(entry, {apply: true, log, jscodeshift});
|
|
292
|
+
|
|
293
|
+
expect(result.errors).toHaveLength(0);
|
|
294
|
+
expect(result.filesChanged).toBe(1);
|
|
295
|
+
const rewritten = fs.readFileSync(
|
|
296
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
297
|
+
'utf-8',
|
|
298
|
+
);
|
|
299
|
+
expect(rewritten).toContain('experimental');
|
|
300
|
+
expect(rewritten).toContain('xle');
|
|
301
|
+
expect(rewritten).toMatch(/KpiCard:\s*\{\s*from:/);
|
|
302
|
+
expect(rewritten).not.toMatch(/layout\s*:/);
|
|
303
|
+
} finally {
|
|
304
|
+
process.chdir(originalCwd);
|
|
305
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('surfaces a thrown bail as a structured error through runConfigCodemod', async () => {
|
|
310
|
+
const {runConfigCodemod} = await import('../../../run-codemod.mjs');
|
|
311
|
+
const {default: transform, meta} = await import(
|
|
312
|
+
'../migrate-layout-components-to-experimental.mjs'
|
|
313
|
+
);
|
|
314
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
315
|
+
|
|
316
|
+
const originalCwd = process.cwd();
|
|
317
|
+
const tmpDir = fs.mkdtempSync(
|
|
318
|
+
path.join(process.cwd(), '.astryx-v013-codemod-test-'),
|
|
319
|
+
);
|
|
320
|
+
try {
|
|
321
|
+
process.chdir(tmpDir);
|
|
322
|
+
fs.writeFileSync(
|
|
323
|
+
path.join(tmpDir, 'package.json'),
|
|
324
|
+
JSON.stringify({name: 'consumer'}),
|
|
325
|
+
);
|
|
326
|
+
fs.writeFileSync(
|
|
327
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
328
|
+
`export default {
|
|
329
|
+
experimental: { xle: { components: {} } },
|
|
330
|
+
layout: { components: { KpiCard: '@/components/KpiCard' } },
|
|
331
|
+
};
|
|
332
|
+
`,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const entry = {
|
|
336
|
+
id: 'migrate-layout-components-to-experimental',
|
|
337
|
+
type: 'config',
|
|
338
|
+
codemod: {title: meta.title, transform},
|
|
339
|
+
package: 'core',
|
|
340
|
+
version: '0.1.3',
|
|
341
|
+
};
|
|
342
|
+
const log = {
|
|
343
|
+
step() {},
|
|
344
|
+
info() {},
|
|
345
|
+
success() {},
|
|
346
|
+
warn() {},
|
|
347
|
+
error() {},
|
|
348
|
+
message() {},
|
|
349
|
+
};
|
|
350
|
+
const result = runConfigCodemod(entry, {apply: true, log, jscodeshift});
|
|
351
|
+
|
|
352
|
+
expect(result.filesChanged).toBe(0);
|
|
353
|
+
expect(result.errors).toHaveLength(1);
|
|
354
|
+
expect(result.errors[0].error).toMatch(/experimental\.xle/);
|
|
355
|
+
} finally {
|
|
356
|
+
process.chdir(originalCwd);
|
|
357
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file v0.1.3 transform manifest
|
|
5
|
+
*
|
|
6
|
+
* Lists all codemods for the v0.1.3 release in the order they should run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import migrateLayoutComponentsToExperimental, {
|
|
10
|
+
meta as migrateLayoutComponentsToExperimentalMeta,
|
|
11
|
+
} from './migrate-layout-components-to-experimental.mjs';
|
|
12
|
+
|
|
13
|
+
export default [
|
|
14
|
+
{
|
|
15
|
+
name: 'migrate-layout-components-to-experimental',
|
|
16
|
+
transform: migrateLayoutComponentsToExperimental,
|
|
17
|
+
meta: migrateLayoutComponentsToExperimentalMeta,
|
|
18
|
+
},
|
|
19
|
+
];
|