@astryxdesign/cli 0.1.0-canary.f94dd07 → 0.1.1-canary.a514b99
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 +68 -0
- package/README.md +117 -75
- package/bin/astryx.mjs +22 -7
- package/docs/icons.doc.mjs +1 -1
- package/docs/migration.doc.mjs +2 -2
- package/docs/shape.doc.mjs +1 -1
- package/docs/styling.doc.mjs +2 -2
- package/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +14 -0
- package/docs/theme.doc.zh.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +4 -4
- package/package.json +8 -8
- package/src/api/search.mjs +207 -13
- package/src/api/template.mjs +2 -1
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/runner.mjs +105 -51
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +116 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +51 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +230 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +84 -0
- package/src/commands/agent-docs.mjs +92 -56
- package/src/commands/agent-docs.path-safety.test.mjs +1 -1
- package/src/commands/agent-docs.test.mjs +66 -10
- package/src/commands/build-theme.import-path.test.mjs +1 -1
- package/src/commands/build-theme.path-safety.test.mjs +1 -1
- package/src/commands/build-theme.prose.test.mjs +1 -1
- package/src/commands/build.mjs +196 -0
- package/src/commands/component-package.test.mjs +1 -1
- package/src/commands/component.test.mjs +1 -1
- package/src/commands/docs.test.mjs +1 -1
- package/src/commands/doctor.test.mjs +1 -1
- package/src/commands/external-showcase.test.mjs +1 -1
- package/src/commands/init.mjs +9 -1
- package/src/commands/interactive-guard.test.mjs +1 -1
- package/src/commands/json-contract.test.mjs +10 -3
- package/src/commands/swizzle-gap-safety.test.mjs +1 -1
- package/src/commands/swizzle.path-safety.test.mjs +1 -1
- package/src/commands/template.path-safety.test.mjs +1 -1
- package/src/commands/template.test.mjs +1 -1
- package/src/commands/upgrade.mjs +353 -169
- package/src/commands/upgrade.test.mjs +41 -27
- package/src/index.mjs +1 -0
- package/src/lib/config.mjs +12 -0
- package/src/lib/config.test.mjs +42 -0
- package/src/lib/error-codes.mjs +3 -0
- package/src/types/error-codes.d.ts +1 -0
- package/src/utils/interactive.mjs +1 -1
- package/src/utils/interactive.test.mjs +2 -0
- package/src/utils/package-manager.test.mjs +1 -1
- package/src/utils/path-safety.test.mjs +1 -1
- package/src/utils/paths.test.mjs +8 -8
- package/src/utils/update-check.mjs +4 -26
- package/src/utils/update-check.test.mjs +2 -64
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +1 -10
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +1 -9
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +1 -9
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +12 -19
- package/templates/blocks/components/Banner/BannerShowcase.tsx +1 -8
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +1 -8
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +2 -12
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +6 -9
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +10 -12
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -9
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +1 -9
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +1 -8
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +1 -8
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +1 -8
- package/templates/blocks/components/CodeBlock/CodeBlockBashCommand.tsx +1 -1
- package/templates/blocks/components/CodeBlock/CodeBlockJSONConfig.tsx +1 -1
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemShowcase.tsx +9 -12
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +13 -15
- package/templates/blocks/components/Divider/DividerShowcase.tsx +1 -8
- package/templates/blocks/components/Divider/DividerVertical.tsx +7 -9
- package/templates/blocks/components/Field/FieldShowcase.tsx +1 -8
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +1 -6
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +1 -9
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +4 -6
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +1 -6
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +2 -8
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -8
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +2 -12
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +8 -11
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +9 -12
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +4 -17
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +15 -16
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +5 -21
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -14
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +12 -14
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +1 -8
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +2 -14
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +2 -7
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +1 -6
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +4 -9
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +1 -8
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +1 -10
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +8 -11
- package/templates/pages/ai-chat/page.tsx +71 -64
- package/templates/pages/ai-chat-landing/page.tsx +8 -12
- package/templates/pages/centered-hero/page.tsx +13 -15
- package/templates/pages/classic-gallery/page.tsx +27 -34
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +11 -14
- package/templates/pages/documentation-design/page.tsx +10 -13
- package/templates/pages/documentation-technical/page.tsx +15 -16
- package/templates/pages/editor/page.tsx +42 -54
- package/templates/pages/file-explorer/page.tsx +13 -16
- package/templates/pages/form-two-column/page.tsx +13 -17
- package/templates/pages/gallery-hero/page.tsx +13 -15
- package/templates/pages/ide/page.tsx +32 -39
- package/templates/pages/library/page.tsx +16 -23
- package/templates/pages/login/page.tsx +14 -18
- package/templates/pages/login-card/page.tsx +14 -18
- package/templates/pages/login-split/page.tsx +50 -48
- package/templates/pages/login-sso/page.tsx +9 -13
- package/templates/pages/mixed-gallery/page.tsx +51 -45
- package/templates/pages/payment-form/page.tsx +56 -70
- package/templates/pages/product-detail/page.tsx +27 -33
- package/templates/pages/product-gallery/page.tsx +7 -13
- package/templates/pages/settings-dialog/page.tsx +35 -43
- package/templates/pages/settings-sidebar/page.tsx +39 -47
- package/templates/pages/side-gallery/page.tsx +6 -9
- package/templates/pages/table-grouped/page.tsx +11 -15
- package/templates/pages/theme-showcase/page.tsx +33 -37
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file v0.1.0 transform manifest
|
|
5
|
+
*
|
|
6
|
+
* Lists all codemods for the v0.1.0 release in the order they should run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import migrateXdsConfigSurfaces, {
|
|
10
|
+
meta as migrateXdsConfigSurfacesMeta,
|
|
11
|
+
} from './migrate-xds-config-surfaces.mjs';
|
|
12
|
+
|
|
13
|
+
import migrateXdsModuleSpecifiers, {
|
|
14
|
+
meta as migrateXdsModuleSpecifiersMeta,
|
|
15
|
+
} from './migrate-xds-module-specifiers.mjs';
|
|
16
|
+
|
|
17
|
+
export default [
|
|
18
|
+
{
|
|
19
|
+
name: 'migrate-xds-config-surfaces',
|
|
20
|
+
transform: migrateXdsConfigSurfaces,
|
|
21
|
+
meta: migrateXdsConfigSurfacesMeta,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'migrate-xds-module-specifiers',
|
|
25
|
+
transform: migrateXdsModuleSpecifiers,
|
|
26
|
+
meta: migrateXdsModuleSpecifiersMeta,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: migrate XDS config surfaces to Astryx v0.1.0
|
|
5
|
+
*
|
|
6
|
+
* The v0.1.0 CLI reads astryx.config.mjs. This config codemod migrates
|
|
7
|
+
* legacy xds.config.mjs and package.json's top-level `xds` config without
|
|
8
|
+
* changing dependency names or versions.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const meta = {
|
|
12
|
+
title: 'Migrate xds config surfaces to astryx',
|
|
13
|
+
description:
|
|
14
|
+
'Moves legacy package.json xds config into astryx.config.mjs, or renames ' +
|
|
15
|
+
'xds.config.mjs when no package.json config exists. Bails out if both exist.',
|
|
16
|
+
pr: '#3092',
|
|
17
|
+
codemodType: 'config',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function findTopLevelKey(source, targetKey) {
|
|
21
|
+
let depth = 0;
|
|
22
|
+
let inString = false;
|
|
23
|
+
let escaped = false;
|
|
24
|
+
let stringStart = -1;
|
|
25
|
+
|
|
26
|
+
for (let i = 0; i < source.length; i++) {
|
|
27
|
+
const ch = source[i];
|
|
28
|
+
|
|
29
|
+
if (inString) {
|
|
30
|
+
if (escaped) {
|
|
31
|
+
escaped = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (ch === '\\') {
|
|
35
|
+
escaped = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch !== '"') continue;
|
|
39
|
+
|
|
40
|
+
inString = false;
|
|
41
|
+
const rawKey = source.slice(stringStart + 1, i);
|
|
42
|
+
if (depth !== 1 || rawKey !== targetKey) continue;
|
|
43
|
+
|
|
44
|
+
let j = i + 1;
|
|
45
|
+
while (/\s/.test(source[j] ?? '')) j++;
|
|
46
|
+
if (source[j] !== ':') continue;
|
|
47
|
+
|
|
48
|
+
const valueStart = j + 1;
|
|
49
|
+
const value = readJsonValue(source, valueStart);
|
|
50
|
+
if (!value) return null;
|
|
51
|
+
return {keyStart: stringStart, keyEnd: i + 1, colon: j, ...value};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (ch === '"') {
|
|
55
|
+
inString = true;
|
|
56
|
+
stringStart = i;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '{' || ch === '[') depth++;
|
|
60
|
+
else if (ch === '}' || ch === ']') depth--;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readJsonValue(source, start) {
|
|
66
|
+
let i = start;
|
|
67
|
+
while (/\s/.test(source[i] ?? '')) i++;
|
|
68
|
+
const valueStart = i;
|
|
69
|
+
let inString = false;
|
|
70
|
+
let escaped = false;
|
|
71
|
+
let depth = 0;
|
|
72
|
+
|
|
73
|
+
for (; i < source.length; i++) {
|
|
74
|
+
const ch = source[i];
|
|
75
|
+
if (inString) {
|
|
76
|
+
if (escaped) {
|
|
77
|
+
escaped = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (ch === '\\') {
|
|
81
|
+
escaped = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === '"') inString = false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === '"') {
|
|
88
|
+
inString = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === '{' || ch === '[') {
|
|
92
|
+
depth++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === '}' || ch === ']') {
|
|
96
|
+
if (depth === 0) break;
|
|
97
|
+
depth--;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (depth === 0 && ch === ',') break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const valueEnd = i;
|
|
104
|
+
const rawValue = source.slice(valueStart, valueEnd).trim();
|
|
105
|
+
try {
|
|
106
|
+
JSON.parse(rawValue);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {valueStart, valueEnd, rawValue, terminator: source[i]};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removeTopLevelProperty(source, property) {
|
|
114
|
+
let removeStart = property.keyStart;
|
|
115
|
+
let removeEnd = property.valueEnd;
|
|
116
|
+
|
|
117
|
+
if (source[removeEnd] === ',') {
|
|
118
|
+
removeEnd++;
|
|
119
|
+
if (source[removeEnd] === '\n') removeEnd++;
|
|
120
|
+
return source.slice(0, removeStart) + source.slice(removeEnd);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let i = removeStart - 1;
|
|
124
|
+
while (/\s/.test(source[i] ?? '')) i--;
|
|
125
|
+
if (source[i] === ',') {
|
|
126
|
+
const whitespaceBetweenCommaAndKey = source.slice(i + 1, removeStart);
|
|
127
|
+
const preservedWhitespace = whitespaceBetweenCommaAndKey.includes('\n')
|
|
128
|
+
? '\n'
|
|
129
|
+
: '';
|
|
130
|
+
return source.slice(0, i) + preservedWhitespace + source.slice(removeEnd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return source.slice(0, removeStart) + source.slice(removeEnd);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatConfigSource(rawConfig) {
|
|
137
|
+
const parsed = JSON.parse(rawConfig);
|
|
138
|
+
return `export default ${JSON.stringify(parsed, null, 2)};\n`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function migratePackageJsonConfig(config) {
|
|
142
|
+
if (!config.packageJson) return {changes: []};
|
|
143
|
+
|
|
144
|
+
const packageJson = config.packageJson.source;
|
|
145
|
+
const xdsConfig = findTopLevelKey(packageJson, 'xds');
|
|
146
|
+
const astryxConfig = findTopLevelKey(packageJson, 'astryx');
|
|
147
|
+
if (!xdsConfig && !astryxConfig) return {changes: []};
|
|
148
|
+
|
|
149
|
+
if (config.astryxConfig) {
|
|
150
|
+
return {
|
|
151
|
+
errors: [
|
|
152
|
+
{
|
|
153
|
+
file: 'package.json',
|
|
154
|
+
error:
|
|
155
|
+
'package.json contains xds/astryx config and astryx.config.mjs already exists; migrate the config manually.',
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (xdsConfig && astryxConfig) {
|
|
162
|
+
return {
|
|
163
|
+
errors: [
|
|
164
|
+
{
|
|
165
|
+
file: 'package.json',
|
|
166
|
+
error:
|
|
167
|
+
'package.json contains both xds and astryx config keys; migrate the config manually.',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const configProperty = astryxConfig ?? xdsConfig;
|
|
174
|
+
const packageJsonWithoutConfig = removeTopLevelProperty(
|
|
175
|
+
packageJson,
|
|
176
|
+
configProperty,
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
changes: [
|
|
180
|
+
{path: 'package.json', source: packageJsonWithoutConfig},
|
|
181
|
+
{
|
|
182
|
+
path: 'astryx.config.mjs',
|
|
183
|
+
source: formatConfigSource(configProperty.rawValue),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function migrateXdsConfigFile(config) {
|
|
190
|
+
if (!config.xdsConfig) return {changes: []};
|
|
191
|
+
if (config.astryxConfig) {
|
|
192
|
+
return {
|
|
193
|
+
errors: [
|
|
194
|
+
{
|
|
195
|
+
file: 'xds.config.mjs',
|
|
196
|
+
error:
|
|
197
|
+
'xds.config.mjs and astryx.config.mjs both exist; migrate the config manually.',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
changes: [
|
|
204
|
+
{path: 'astryx.config.mjs', source: config.xdsConfig.source},
|
|
205
|
+
{path: 'xds.config.mjs', delete: true},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export default function transformer(_file, api) {
|
|
211
|
+
const config = api.config ?? {};
|
|
212
|
+
const packageResult = migratePackageJsonConfig(config);
|
|
213
|
+
if (packageResult.errors?.length) return packageResult;
|
|
214
|
+
if (packageResult.changes?.length) {
|
|
215
|
+
if (config.xdsConfig) {
|
|
216
|
+
return {
|
|
217
|
+
errors: [
|
|
218
|
+
{
|
|
219
|
+
file: 'package.json',
|
|
220
|
+
error:
|
|
221
|
+
'package.json contains xds/astryx config and xds.config.mjs exists; migrate the config manually.',
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return packageResult;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return migrateXdsConfigFile(config);
|
|
230
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: migrate module specifiers from @xds/* to @astryxdesign/*
|
|
5
|
+
*
|
|
6
|
+
* The v0.1.0 release moved the public package scope from @xds to
|
|
7
|
+
* @astryxdesign. Consumers update dependencies first, install, then run
|
|
8
|
+
* `astryx upgrade`; this transform only updates JS/TS module specifiers in
|
|
9
|
+
* source code.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const meta = {
|
|
13
|
+
title: 'Migrate module specifiers from @xds/* to @astryxdesign/*',
|
|
14
|
+
description:
|
|
15
|
+
'Updates JS/TS import, export, dynamic import, and require module ' +
|
|
16
|
+
'specifiers from @xds/* to the @astryxdesign/* packages used by Astryx v0.1.0.',
|
|
17
|
+
pr: '#3092',
|
|
18
|
+
fileExtensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PACKAGE_RENAMES = new Map([
|
|
22
|
+
['@xds/build', '@astryxdesign/build'],
|
|
23
|
+
['@xds/cli', '@astryxdesign/cli'],
|
|
24
|
+
['@xds/core', '@astryxdesign/core'],
|
|
25
|
+
['@xds/lab', '@astryxdesign/lab'],
|
|
26
|
+
['@xds/theme-butter', '@astryxdesign/theme-butter'],
|
|
27
|
+
['@xds/theme-chocolate', '@astryxdesign/theme-chocolate'],
|
|
28
|
+
['@xds/theme-daily', '@astryxdesign/theme-neutral'],
|
|
29
|
+
['@xds/theme-default', '@astryxdesign/theme-neutral'],
|
|
30
|
+
['@xds/theme-gothic', '@astryxdesign/theme-gothic'],
|
|
31
|
+
['@xds/theme-matcha', '@astryxdesign/theme-matcha'],
|
|
32
|
+
['@xds/theme-neutral', '@astryxdesign/theme-neutral'],
|
|
33
|
+
['@xds/theme-stone', '@astryxdesign/theme-stone'],
|
|
34
|
+
['@xds/theme-y2k', '@astryxdesign/theme-y2k'],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function renamePackageSpecifier(value) {
|
|
38
|
+
if (typeof value !== 'string') return value;
|
|
39
|
+
for (const [from, to] of PACKAGE_RENAMES) {
|
|
40
|
+
if (value === from) return to;
|
|
41
|
+
if (value.startsWith(from + '/')) return to + value.slice(from.length);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rewriteLiteral(node) {
|
|
47
|
+
if (!node || typeof node.value !== 'string') return false;
|
|
48
|
+
const next = renamePackageSpecifier(node.value);
|
|
49
|
+
if (next === node.value) return false;
|
|
50
|
+
node.value = next;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function transformer(file, api) {
|
|
55
|
+
const j = api.jscodeshift;
|
|
56
|
+
const root = j(file.source);
|
|
57
|
+
let hasChanges = false;
|
|
58
|
+
|
|
59
|
+
root.find(j.ImportDeclaration).forEach(path => {
|
|
60
|
+
hasChanges = rewriteLiteral(path.node.source) || hasChanges;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
root.find(j.ExportNamedDeclaration).forEach(path => {
|
|
64
|
+
hasChanges = rewriteLiteral(path.node.source) || hasChanges;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
root.find(j.ExportAllDeclaration).forEach(path => {
|
|
68
|
+
hasChanges = rewriteLiteral(path.node.source) || hasChanges;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
root.find(j.CallExpression).forEach(path => {
|
|
72
|
+
const isDynamicImport = path.node.callee.type === 'Import';
|
|
73
|
+
const isRequire =
|
|
74
|
+
path.node.callee.type === 'Identifier' &&
|
|
75
|
+
path.node.callee.name === 'require';
|
|
76
|
+
if (!isDynamicImport && !isRequire) return;
|
|
77
|
+
const [arg] = path.node.arguments;
|
|
78
|
+
if (arg?.type === 'StringLiteral' || arg?.type === 'Literal') {
|
|
79
|
+
hasChanges = rewriteLiteral(arg) || hasChanges;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return hasChanges ? root.toSource() : undefined;
|
|
84
|
+
}
|
|
@@ -93,14 +93,55 @@ export function resolveAgentPaths(targetDir, agent) {
|
|
|
93
93
|
return {inject: [], create: [searchPaths[searchPaths.length - 1]]};
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Detect which styling system the consumer project has wired up, so the agent
|
|
98
|
+
* docs recommend a path that actually compiles in THIS project.
|
|
99
|
+
*
|
|
100
|
+
* `xstyle`/StyleX needs the StyleX compiler (the `@stylexjs/stylex` runtime
|
|
101
|
+
* alone throws at runtime → blank page); Tailwind utilities need Tailwind.
|
|
102
|
+
* Recommending either when it isn't configured yields unstyled or blank output.
|
|
103
|
+
* Plain CSS variables (via `style`/`className`) always work, so they're the
|
|
104
|
+
* safe default. Precedence: stylex (compiler wired) → tailwind → css.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} targetDir
|
|
107
|
+
* @returns {'stylex' | 'tailwind' | 'css'}
|
|
108
|
+
*/
|
|
109
|
+
export function detectStylingSystem(targetDir) {
|
|
110
|
+
try {
|
|
111
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
112
|
+
if (!fs.existsSync(pkgPath)) return 'css';
|
|
113
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
const deps = {...pkg.dependencies, ...pkg.devDependencies};
|
|
115
|
+
// Key off a StyleX *compiler* plugin — the runtime alone won't render.
|
|
116
|
+
const stylexCompilers = [
|
|
117
|
+
'@stylexjs/babel-plugin',
|
|
118
|
+
'vite-plugin-stylex',
|
|
119
|
+
'unplugin-stylex',
|
|
120
|
+
'@stylexswc/unplugin',
|
|
121
|
+
'@stylexswc/nextjs-plugin',
|
|
122
|
+
'stylex-webpack',
|
|
123
|
+
];
|
|
124
|
+
if (stylexCompilers.some(d => d in deps)) return 'stylex';
|
|
125
|
+
if ('tailwindcss' in deps) return 'tailwind';
|
|
126
|
+
return 'css';
|
|
127
|
+
} catch {
|
|
128
|
+
// Best-effort: default to the universally-safe CSS-variable path.
|
|
129
|
+
return 'css';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
96
133
|
/**
|
|
97
134
|
* Generate the agent cheat sheet from live CLI metadata.
|
|
98
135
|
*
|
|
99
136
|
* Structured as: workflow (behavioral) → rules (error prevention) → CLI reference.
|
|
100
137
|
* Templates are positioned first in the workflow to teach agents the
|
|
101
138
|
* "look at reference code" reflex before writing any UI.
|
|
139
|
+
*
|
|
140
|
+
* `stylingSystem` tailors the custom-styling guidance to what the project has
|
|
141
|
+
* configured (see {@link detectStylingSystem}) so the agent never reaches for a
|
|
142
|
+
* styling path that isn't compiled here.
|
|
102
143
|
*/
|
|
103
|
-
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix()} = {}) {
|
|
144
|
+
export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPrefix(), stylingSystem = 'css'} = {}) {
|
|
104
145
|
const run = `${runPrefix} astryx`;
|
|
105
146
|
const lines = [MARKER_START];
|
|
106
147
|
|
|
@@ -117,69 +158,57 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
|
|
|
117
158
|
}
|
|
118
159
|
}
|
|
119
160
|
|
|
120
|
-
// Header
|
|
121
|
-
lines.push(`Astryx v${version}
|
|
161
|
+
// Header — state the CLI prefix once; commands below are shown as `astryx <cmd>`.
|
|
162
|
+
lines.push(`Astryx v${version} · ${componentCount} components`);
|
|
163
|
+
lines.push(`CLI: run every command as \`${run} <cmd>\` (shown below as \`astryx ...\`).`);
|
|
122
164
|
lines.push('');
|
|
123
165
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
lines.push(
|
|
127
|
-
lines.push(
|
|
128
|
-
lines.push(
|
|
129
|
-
lines.push('');
|
|
130
|
-
lines.push('Templates are reference code — read them for composition patterns, not just scaffolding.');
|
|
131
|
-
lines.push('Full pages → dashboard (uses AppShell). Forms → contact-form. Tables → data-table. Settings → settings-sidebar.');
|
|
166
|
+
// Required setup — components ship precompiled CSS; without these imports
|
|
167
|
+
// everything renders unstyled. Theme is optional (a default ships in astryx.css).
|
|
168
|
+
lines.push('SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled:');
|
|
169
|
+
lines.push(' import "@astryxdesign/core/reset.css";');
|
|
170
|
+
lines.push(' import "@astryxdesign/core/astryx.css";');
|
|
132
171
|
lines.push('');
|
|
133
172
|
|
|
134
|
-
//
|
|
135
|
-
lines.push(
|
|
136
|
-
lines.push('
|
|
137
|
-
lines.push('
|
|
138
|
-
lines.push('
|
|
139
|
-
lines.push(`No magic values — run \`${run} docs tokens\` for spacing/color/radius.`);
|
|
140
|
-
lines.push(`To change accent/brand colors: \`${run} theme\` — never override --astryx-color-* in :root.`);
|
|
173
|
+
// Workflow — `build` is the front door; discover before writing UI.
|
|
174
|
+
lines.push("WORKFLOW — discover, don't guess. Before writing UI:");
|
|
175
|
+
lines.push('1. `astryx build "<idea>"` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.');
|
|
176
|
+
lines.push('2. `astryx template <name> [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.');
|
|
177
|
+
lines.push('3. `astryx component <Name>` — props + examples for every component you use.');
|
|
141
178
|
lines.push('');
|
|
142
179
|
|
|
143
|
-
//
|
|
144
|
-
lines.push(
|
|
145
|
-
lines.push(
|
|
180
|
+
// Rules — the top error-preventers.
|
|
181
|
+
lines.push('RULES:');
|
|
182
|
+
lines.push('- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.');
|
|
183
|
+
// Styling guidance tailored to the project's configured system — never
|
|
184
|
+
// recommend a path that isn't compiled here (xstyle needs the StyleX compiler;
|
|
185
|
+
// utilities need Tailwind). Tokens are always the source of truth.
|
|
186
|
+
if (stylingSystem === 'stylex') {
|
|
187
|
+
lines.push('- Custom styling: component props first; else the xstyle prop / StyleX tokens (@astryxdesign/core/theme/tokens.stylex). No raw hex/px.');
|
|
188
|
+
} else if (stylingSystem === 'tailwind') {
|
|
189
|
+
lines.push('- Custom styling: component props first; else Tailwind utilities backed by tokens (bg-surface, text-primary, rounded-lg) via tailwind-theme.css. No raw hex/px.');
|
|
190
|
+
} else {
|
|
191
|
+
lines.push("- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)");
|
|
192
|
+
}
|
|
193
|
+
lines.push('- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.');
|
|
194
|
+
lines.push('');
|
|
146
195
|
|
|
147
|
-
//
|
|
196
|
+
// Command reference — build/template/component are covered in WORKFLOW above.
|
|
197
|
+
lines.push('MORE CLI:');
|
|
198
|
+
lines.push(' search "<query>" find any component / hook / doc / template / block');
|
|
199
|
+
lines.push(` component --list ${componentCount} components by category`);
|
|
200
|
+
lines.push(' template --list page + block recipes');
|
|
148
201
|
const docsDir = path.join(CLI_ROOT, 'docs');
|
|
149
202
|
if (fs.existsSync(docsDir)) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
let desc = topic;
|
|
155
|
-
try {
|
|
156
|
-
const fileContent = fs.readFileSync(path.join(docsDir, file), 'utf-8');
|
|
157
|
-
const descMatch = fileContent.match(/description:\s*['"](.+?)['"]/);
|
|
158
|
-
if (descMatch) desc = descMatch[1];
|
|
159
|
-
} catch {
|
|
160
|
-
// Best-effort: fall back to the topic name if the file is unreadable.
|
|
161
|
-
}
|
|
162
|
-
if (desc.length > 50) desc = desc.slice(0, 47) + '...';
|
|
163
|
-
lines.push(`${run} docs ${topic.padEnd(20)} ${desc}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Templates
|
|
168
|
-
const templatesDir = path.join(CLI_ROOT, 'templates');
|
|
169
|
-
if (fs.existsSync(templatesDir)) {
|
|
170
|
-
const templates = fs.readdirSync(templatesDir, {withFileTypes: true})
|
|
171
|
-
.filter(e => e.isDirectory())
|
|
172
|
-
.map(e => e.name)
|
|
203
|
+
const topics = fs.readdirSync(docsDir)
|
|
204
|
+
.map(f => f.match(/^(\w+)\.doc\.mjs$/))
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map(m => m[1])
|
|
173
207
|
.sort();
|
|
174
|
-
if (
|
|
175
|
-
lines.push(`${run} template --list page recipes with component lists`);
|
|
176
|
-
lines.push(`${run} template <name> [path] scaffold from template`);
|
|
177
|
-
}
|
|
208
|
+
if (topics.length > 0) lines.push(` docs <topic> ${topics.join(', ')}`);
|
|
178
209
|
}
|
|
179
|
-
|
|
180
|
-
lines.push(
|
|
181
|
-
lines.push(`${run} upgrade --apply codemods after version bump`);
|
|
182
|
-
lines.push(`after @astryxdesign/core bump, always run ${run} upgrade --apply`);
|
|
210
|
+
lines.push(' swizzle <Name> eject component source (--gap reports why)');
|
|
211
|
+
lines.push(' upgrade --apply run after any @astryxdesign/core bump');
|
|
183
212
|
lines.push(MARKER_END);
|
|
184
213
|
|
|
185
214
|
return lines.join('\n');
|
|
@@ -258,7 +287,10 @@ export function injectXdsBlock(filePath, compressedIndex, {createIfMissing = fal
|
|
|
258
287
|
*/
|
|
259
288
|
export function injectAgentsMd(targetDir, version) {
|
|
260
289
|
const agentsPath = path.join(targetDir, AGENTS_MD);
|
|
261
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
290
|
+
const compressedIndex = generateCompressedIndex(version, {
|
|
291
|
+
coreDir: findCoreDir(targetDir),
|
|
292
|
+
stylingSystem: detectStylingSystem(targetDir),
|
|
293
|
+
});
|
|
262
294
|
injectXdsBlock(agentsPath, compressedIndex, {
|
|
263
295
|
createIfMissing: true,
|
|
264
296
|
header: `# AGENTS.md\n\nProject-specific guidance for AI coding agents.`,
|
|
@@ -273,7 +305,10 @@ export function injectAgentsMd(targetDir, version) {
|
|
|
273
305
|
*/
|
|
274
306
|
export function injectClaudeMd(targetDir, version) {
|
|
275
307
|
const claudePath = path.join(targetDir, CLAUDE_MD);
|
|
276
|
-
const compressedIndex = generateCompressedIndex(version, {
|
|
308
|
+
const compressedIndex = generateCompressedIndex(version, {
|
|
309
|
+
coreDir: findCoreDir(targetDir),
|
|
310
|
+
stylingSystem: detectStylingSystem(targetDir),
|
|
311
|
+
});
|
|
277
312
|
return injectXdsBlock(claudePath, compressedIndex);
|
|
278
313
|
}
|
|
279
314
|
|
|
@@ -356,7 +391,8 @@ export function installAgentDocs(targetDir, {zh = false, lang, agent, paths, onl
|
|
|
356
391
|
const coreDir = findCoreDir(targetDir);
|
|
357
392
|
const version = getXdsVersion(coreDir);
|
|
358
393
|
const runPrefix = getRunPrefix(targetDir);
|
|
359
|
-
const
|
|
394
|
+
const stylingSystem = detectStylingSystem(targetDir);
|
|
395
|
+
const compressedIndex = generateCompressedIndex(version, {coreDir, zh, lang, runPrefix, stylingSystem});
|
|
360
396
|
const written = [];
|
|
361
397
|
|
|
362
398
|
// Explicit paths override everything
|
|
@@ -20,7 +20,7 @@ import {PathSafetyError} from '../utils/path-safety.mjs';
|
|
|
20
20
|
|
|
21
21
|
let tmpDir;
|
|
22
22
|
beforeEach(() => {
|
|
23
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-paths-'));
|
|
24
24
|
});
|
|
25
25
|
afterEach(() => {
|
|
26
26
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
@@ -6,6 +6,7 @@ import * as path from 'node:path';
|
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import {
|
|
8
8
|
generateCompressedIndex,
|
|
9
|
+
detectStylingSystem,
|
|
9
10
|
getXdsVersion,
|
|
10
11
|
installAgentDocs,
|
|
11
12
|
injectAgentsMd,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
let tmpDir;
|
|
21
22
|
|
|
22
23
|
beforeEach(() => {
|
|
23
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
24
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-agent-docs-test-'));
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
afterEach(() => {
|
|
@@ -39,31 +40,86 @@ describe('generateCompressedIndex', () => {
|
|
|
39
40
|
it('includes theme nudge rule', () => {
|
|
40
41
|
const result = generateCompressedIndex('1.0.0');
|
|
41
42
|
expect(result).toMatch(/astryx theme/);
|
|
42
|
-
expect(result).toMatch(/never override --
|
|
43
|
+
expect(result).toMatch(/never override --color-/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('defaults to the CSS-variable styling path (no compiler)', () => {
|
|
47
|
+
const result = generateCompressedIndex('1.0.0');
|
|
48
|
+
expect(result).toMatch(/style\/className with tokens/);
|
|
49
|
+
expect(result).toMatch(/var\(--color-\*/);
|
|
50
|
+
// Must NOT push xstyle when no StyleX compiler is present.
|
|
51
|
+
expect(result).not.toMatch(/xstyle prop/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('recommends xstyle when StyleX is configured', () => {
|
|
55
|
+
const result = generateCompressedIndex('1.0.0', {stylingSystem: 'stylex'});
|
|
56
|
+
expect(result).toMatch(/xstyle prop \/ StyleX tokens/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('recommends Tailwind utilities when Tailwind is configured', () => {
|
|
60
|
+
const result = generateCompressedIndex('1.0.0', {stylingSystem: 'tailwind'});
|
|
61
|
+
expect(result).toMatch(/Tailwind utilities backed by tokens/);
|
|
62
|
+
expect(result).toMatch(/tailwind-theme\.css/);
|
|
43
63
|
});
|
|
44
64
|
|
|
45
65
|
it('includes upgrade command and migration rule', () => {
|
|
46
66
|
const result = generateCompressedIndex('1.0.0');
|
|
47
|
-
expect(result).toContain('
|
|
48
|
-
expect(result).
|
|
49
|
-
expect(result).toMatch(/always run .+ astryx upgrade --apply/);
|
|
67
|
+
expect(result).toContain('upgrade --apply');
|
|
68
|
+
expect(result).toMatch(/after any @astryxdesign\/core bump/);
|
|
50
69
|
});
|
|
51
70
|
|
|
52
|
-
it('
|
|
71
|
+
it('states the runPrefix once in the CLI header', () => {
|
|
53
72
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'yarn'});
|
|
54
|
-
expect(result).toContain('yarn astryx
|
|
55
|
-
expect(result).toContain('yarn astryx upgrade --apply');
|
|
56
|
-
expect(result).toContain('after @astryxdesign/core bump, always run yarn astryx upgrade --apply');
|
|
73
|
+
expect(result).toContain('yarn astryx <cmd>');
|
|
57
74
|
expect(result).not.toContain('npx astryx');
|
|
58
75
|
});
|
|
59
76
|
|
|
60
77
|
it('uses pnpm exec prefix', () => {
|
|
61
78
|
const result = generateCompressedIndex('1.0.0', {runPrefix: 'pnpm exec'});
|
|
62
|
-
expect(result).toContain('pnpm exec astryx
|
|
79
|
+
expect(result).toContain('pnpm exec astryx <cmd>');
|
|
63
80
|
expect(result).not.toContain('npx astryx');
|
|
64
81
|
});
|
|
65
82
|
});
|
|
66
83
|
|
|
84
|
+
describe('detectStylingSystem', () => {
|
|
85
|
+
function writePkg(deps) {
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(tmpDir, 'package.json'),
|
|
88
|
+
JSON.stringify({name: 'x', devDependencies: deps}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it('defaults to css when no package.json', () => {
|
|
93
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns css for a plain project', () => {
|
|
97
|
+
writePkg({react: '19.0.0', vite: '6.0.0'});
|
|
98
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('detects stylex when the compiler plugin is present', () => {
|
|
102
|
+
writePkg({'@stylexjs/babel-plugin': '0.0.1'});
|
|
103
|
+
expect(detectStylingSystem(tmpDir)).toBe('stylex');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('detects tailwind when tailwindcss is present', () => {
|
|
107
|
+
writePkg({tailwindcss: '4.0.0'});
|
|
108
|
+
expect(detectStylingSystem(tmpDir)).toBe('tailwind');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does NOT treat the StyleX runtime alone as a compiler', () => {
|
|
112
|
+
// Only the runtime, no compiler plugin → must stay on the safe css path.
|
|
113
|
+
writePkg({'@stylexjs/stylex': '0.0.1'});
|
|
114
|
+
expect(detectStylingSystem(tmpDir)).toBe('css');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('prefers stylex over tailwind when both are configured', () => {
|
|
118
|
+
writePkg({'@stylexjs/babel-plugin': '0.0.1', tailwindcss: '4.0.0'});
|
|
119
|
+
expect(detectStylingSystem(tmpDir)).toBe('stylex');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
67
123
|
describe('getXdsVersion', () => {
|
|
68
124
|
it('reads version from core package.json', () => {
|
|
69
125
|
const coreDir = path.join(tmpDir, 'core');
|
|
@@ -70,7 +70,7 @@ beforeAll(() => {
|
|
|
70
70
|
|
|
71
71
|
let tmpDir;
|
|
72
72
|
beforeEach(() => {
|
|
73
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
73
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-import-path-'));
|
|
74
74
|
});
|
|
75
75
|
afterEach(() => {
|
|
76
76
|
fs.rmSync(tmpDir, {recursive: true, force: true});
|