@astryxdesign/cli 0.1.0-canary.ec1f71f → 0.1.0-canary.f718310
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/docs/theme.doc.dense.mjs +2 -2
- package/docs/theme.doc.mjs +0 -14
- package/docs/theme.doc.zh.mjs +2 -2
- package/package.json +8 -8
- package/src/api/search.mjs +13 -207
- package/src/api/template.mjs +1 -2
- package/src/codemods/__tests__/registry.test.mjs +0 -1
- package/src/codemods/registry.mjs +0 -1
- package/src/codemods/runner.mjs +51 -105
- package/src/commands/agent-docs.mjs +56 -92
- package/src/commands/agent-docs.test.mjs +9 -65
- package/src/commands/init.mjs +1 -9
- package/src/commands/upgrade.mjs +38 -117
- package/src/index.mjs +0 -1
- package/src/lib/config.mjs +0 -12
- package/src/lib/error-codes.mjs +0 -3
- package/src/types/error-codes.d.ts +0 -1
- package/src/utils/update-check.mjs +26 -4
- package/src/utils/update-check.test.mjs +63 -1
- package/templates/blocks/components/AppShell/AppShellContentOnly.tsx +9 -1
- package/templates/blocks/components/AppShell/AppShellShowcase.tsx +10 -1
- package/templates/blocks/components/AppShell/AppShellSideNavOnly.tsx +9 -1
- package/templates/blocks/components/AppShell/AppShellTopNavOnly.tsx +9 -1
- package/templates/blocks/components/AppShell/AppShellTopNavWithSideNav.tsx +9 -1
- package/templates/blocks/components/AppShell/AppShellWithBanner.tsx +9 -1
- package/templates/blocks/components/AspectRatio/AspectRatioShowcase.tsx +19 -12
- package/templates/blocks/components/Banner/BannerShowcase.tsx +8 -1
- package/templates/blocks/components/Blockquote/BlockquoteShowcase.tsx +8 -1
- package/templates/blocks/components/Carousel/CarouselShowcase.tsx +12 -2
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerShowcase.tsx +9 -6
- package/templates/blocks/components/ChatLayout/ChatLayoutPanelChat.tsx +12 -10
- package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +9 -1
- package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +9 -1
- package/templates/blocks/components/ChatMessageList/ChatMessageListShowcase.tsx +9 -1
- package/templates/blocks/components/ChatMessageMetadata/ChatMessageMetadataShowcase.tsx +8 -1
- package/templates/blocks/components/ChatSendButton/ChatSendButtonInComposer.tsx +8 -1
- package/templates/blocks/components/Citation/CitationInlineText.tsx +4 -4
- package/templates/blocks/components/Code/CodeInlineInParagraph.tsx +8 -1
- 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 +12 -9
- package/templates/blocks/components/ContextMenu/ContextMenuShowcase.tsx +15 -13
- package/templates/blocks/components/Divider/DividerShowcase.tsx +8 -1
- package/templates/blocks/components/Divider/DividerVertical.tsx +9 -7
- package/templates/blocks/components/Field/FieldShowcase.tsx +8 -1
- package/templates/blocks/components/FormLayout/FormLayoutHorizontal.tsx +6 -1
- package/templates/blocks/components/Grid/GridResponsiveAutoFit.tsx +9 -1
- package/templates/blocks/components/HoverCard/HoverCardInlineTextHoverCard.tsx +6 -4
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +6 -1
- package/templates/blocks/components/HoverCard/HoverCardProfileHoverCard.tsx +8 -2
- package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +8 -1
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +12 -2
- package/templates/blocks/components/OverflowList/OverflowListOverflowBadges.tsx +11 -8
- package/templates/blocks/components/OverflowList/OverflowListOverflowDropdownActions.tsx +12 -9
- package/templates/blocks/components/Overlay/OverlayBottomStrip.tsx +17 -4
- package/templates/blocks/components/Overlay/OverlayHoverReveal.tsx +16 -15
- package/templates/blocks/components/Overlay/OverlayShowcase.tsx +21 -5
- package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +14 -2
- package/templates/blocks/components/Pagination/PaginationPageSize.tsx +14 -12
- package/templates/blocks/components/Pagination/PaginationVariants.tsx +8 -1
- package/templates/blocks/components/Pagination/PaginationWithTable.tsx +14 -2
- package/templates/blocks/components/Tokenizer/TokenizerClear.tsx +6 -1
- package/templates/blocks/components/Tokenizer/TokenizerCreatable.tsx +7 -2
- package/templates/blocks/components/Tokenizer/TokenizerEndContent.tsx +6 -1
- package/templates/blocks/components/Tokenizer/TokenizerIcon.tsx +6 -1
- package/templates/blocks/components/Tokenizer/TokenizerMaxEntries.tsx +6 -1
- package/templates/blocks/components/Tokenizer/TokenizerOverflow.tsx +7 -2
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +6 -1
- package/templates/blocks/components/Tokenizer/TokenizerStates.tsx +9 -4
- package/templates/blocks/components/Toolbar/ToolbarCardHeader.tsx +10 -1
- package/templates/blocks/components/Toolbar/ToolbarSizes.tsx +8 -1
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +8 -1
- package/templates/blocks/components/Toolbar/ToolbarThreeSlot.tsx +10 -1
- package/templates/blocks/components/Toolbar/ToolbarWithTabs.tsx +11 -8
- package/templates/pages/ai-chat/page.tsx +64 -71
- package/templates/pages/ai-chat-landing/page.tsx +12 -8
- package/templates/pages/centered-hero/page.tsx +15 -13
- package/templates/pages/classic-gallery/page.tsx +34 -27
- package/templates/pages/detail-page/page.tsx +18 -18
- package/templates/pages/documentation/page.tsx +14 -11
- package/templates/pages/documentation-design/page.tsx +13 -10
- package/templates/pages/documentation-technical/page.tsx +16 -15
- package/templates/pages/editor/page.tsx +54 -42
- package/templates/pages/file-explorer/page.tsx +16 -13
- package/templates/pages/form-two-column/page.tsx +17 -13
- package/templates/pages/gallery-hero/page.tsx +15 -13
- package/templates/pages/ide/page.tsx +39 -32
- package/templates/pages/library/page.tsx +23 -16
- package/templates/pages/login/page.tsx +18 -14
- package/templates/pages/login-card/page.tsx +18 -14
- package/templates/pages/login-split/page.tsx +48 -50
- package/templates/pages/login-sso/page.tsx +13 -9
- package/templates/pages/mixed-gallery/page.tsx +45 -51
- package/templates/pages/payment-form/page.tsx +70 -56
- package/templates/pages/product-detail/page.tsx +33 -27
- package/templates/pages/product-gallery/page.tsx +13 -7
- package/templates/pages/settings-dialog/page.tsx +43 -35
- package/templates/pages/settings-sidebar/page.tsx +47 -39
- package/templates/pages/side-gallery/page.tsx +9 -6
- package/templates/pages/table-grouped/page.tsx +15 -11
- package/templates/pages/theme-showcase/page.tsx +37 -33
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +0 -51
- package/src/codemods/transforms/v0.1.0/index.mjs +0 -28
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +0 -84
- package/src/commands/build.mjs +0 -196
- package/src/lib/config.test.mjs +0 -42
package/docs/theme.doc.dense.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsDense = {
|
|
6
6
|
description: 'Theme provider, custom themes, light/dark, component overrides',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: 'Quick Start', content: [null, null,
|
|
9
|
-
{ title: 'Themes', content: [null,
|
|
8
|
+
{ title: 'Quick Start', content: [null, null, { type: 'prose', text: 'default import = runtime injection. /built import = pre-compiled CSS (pair with theme.css).' }] },
|
|
9
|
+
{ title: 'Themes', content: [null, { type: 'prose', text: 'published: neutral (start here), butter, chocolate, gothic (dark-only), matcha, stone, y2k. @astryxdesign/theme-{name} = source (runtime). @astryxdesign/theme-{name}/built = optimized (+ theme.css).' }] },
|
|
10
10
|
{ title: 'Props', content: [null] },
|
|
11
11
|
{ title: 'Custom Theme', content: [{ type: 'prose', text: 'CLI wizard or manual defineTheme. only override tokens that differ.' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: 'scale configs (color, typography, radius, motion) + explicit token overrides + component overrides. color derives full palette from accent hex via HCT.' }, null, null] },
|
package/docs/theme.doc.mjs
CHANGED
|
@@ -14,12 +14,6 @@ export const docs = {
|
|
|
14
14
|
title: 'Quick Start',
|
|
15
15
|
category: 'guide',
|
|
16
16
|
content: [
|
|
17
|
-
{
|
|
18
|
-
type: 'code',
|
|
19
|
-
lang: 'bash',
|
|
20
|
-
label: 'Install a theme package',
|
|
21
|
-
code: 'npm install @astryxdesign/theme-neutral',
|
|
22
|
-
},
|
|
23
17
|
{
|
|
24
18
|
type: 'code',
|
|
25
19
|
lang: 'tsx',
|
|
@@ -51,10 +45,6 @@ function App() {
|
|
|
51
45
|
);
|
|
52
46
|
}`,
|
|
53
47
|
},
|
|
54
|
-
{
|
|
55
|
-
type: 'prose',
|
|
56
|
-
text: 'Each theme ships as its own npm package. Install the one you want, then wrap your app in `<Theme>` — the same pattern works for every theme; just swap the package and import name.',
|
|
57
|
-
},
|
|
58
48
|
{
|
|
59
49
|
type: 'prose',
|
|
60
50
|
text: 'The default import uses runtime style injection, which works everywhere with no build step. The `/built` import skips injection and relies on the pre-compiled CSS file for better performance and SSR support.',
|
|
@@ -65,10 +55,6 @@ function App() {
|
|
|
65
55
|
title: 'Available Themes',
|
|
66
56
|
category: 'guide',
|
|
67
57
|
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'prose',
|
|
70
|
-
text: 'Install the theme package you want with `npm install @astryxdesign/theme-{name}`, then import its theme object as shown below.',
|
|
71
|
-
},
|
|
72
58
|
{
|
|
73
59
|
type: 'table',
|
|
74
60
|
headers: ['Theme', 'Import', 'Description'],
|
package/docs/theme.doc.zh.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
export const docsZh = {
|
|
6
6
|
description: 'Theme 提供者、自定义主题、亮/暗模式和组件样式覆盖。',
|
|
7
7
|
sections: [
|
|
8
|
-
{ title: '快速开始', content: [null, null,
|
|
9
|
-
{ title: '可用主题', content: [null,
|
|
8
|
+
{ title: '快速开始', content: [null, null, { type: 'prose', text: '默认导入使用运行时样式注入。/built 导入使用预编译 CSS(需配合 theme.css)。' }] },
|
|
9
|
+
{ title: '可用主题', content: [null, { type: 'prose', text: '已发布主题:neutral(推荐起点)、butter、chocolate、gothic(仅暗色)、matcha、stone、y2k。@astryxdesign/theme-{name} = 源码版(运行时注入)。@astryxdesign/theme-{name}/built = 优化版(配合 theme.css)。' }] },
|
|
10
10
|
{ title: 'Theme 属性', content: [null] },
|
|
11
11
|
{ title: '创建自定义主题', content: [{ type: 'prose', text: '使用 CLI 向导(推荐)或手动 defineTheme。只覆盖与默认值不同的令牌。' }, null] },
|
|
12
12
|
{ title: 'defineTheme', content: [{ type: 'prose', text: '支持比例配置(typography、radius、motion)+ 显式令牌覆盖 + 组件覆盖。' }, null, null] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
3
|
+
"version": "0.1.0-canary.f718310",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"xds",
|
|
19
19
|
"cli",
|
|
20
20
|
"design-system",
|
|
21
21
|
"init",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"jscodeshift": "^17.3.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
58
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
59
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
57
|
+
"@astryxdesign/core": "0.1.0-canary.f718310",
|
|
58
|
+
"@astryxdesign/lab": "0.1.0-canary.f718310",
|
|
59
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.f718310"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@astryxdesign/core": {
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@astryxdesign/core": "0.1.0-canary.
|
|
74
|
-
"@astryxdesign/lab": "0.1.0-canary.
|
|
75
|
-
"@astryxdesign/theme-neutral": "0.1.0-canary.
|
|
73
|
+
"@astryxdesign/core": "0.1.0-canary.f718310",
|
|
74
|
+
"@astryxdesign/lab": "0.1.0-canary.f718310",
|
|
75
|
+
"@astryxdesign/theme-neutral": "0.1.0-canary.f718310"
|
|
76
76
|
},
|
|
77
77
|
"scripts": {
|
|
78
78
|
"astryx": "node bin/astryx.mjs",
|
package/src/api/search.mjs
CHANGED
|
@@ -41,184 +41,14 @@ import {
|
|
|
41
41
|
} from '../lib/component-discovery.mjs';
|
|
42
42
|
import {discoverHooks, findHookDoc} from '../lib/hook-discovery.mjs';
|
|
43
43
|
import {levenshteinDistance} from '../lib/string-utils.mjs';
|
|
44
|
-
import {discoverTemplates
|
|
44
|
+
import {discoverTemplates} from './template.mjs';
|
|
45
45
|
import {AstryxError} from './error.mjs';
|
|
46
46
|
|
|
47
47
|
const DOCS_DIR = path.join(CLI_ROOT, 'docs');
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* Synonym / intent map: product-language terms an agent is likely to type,
|
|
51
|
-
* expanded to the catalog's vocabulary so oblique queries still rank. Keys and
|
|
52
|
-
* values are matched bidirectionally (typing any value also pulls in the key
|
|
53
|
-
* and its siblings). Lowercase, single words or short phrases.
|
|
54
|
-
*/
|
|
55
|
-
const SYNONYMS = {
|
|
56
|
-
dashboard: ['overview', 'analytics', 'kpi', 'kpis', 'metrics', 'stats', 'reporting', 'insights', 'control'],
|
|
57
|
-
login: ['signin', 'auth', 'authentication', 'sso', 'credentials', 'account'],
|
|
58
|
-
signup: ['register', 'registration', 'onboarding'],
|
|
59
|
-
payment: ['checkout', 'billing', 'card', 'pay', 'purchase', 'order'],
|
|
60
|
-
pricing: ['plans', 'plan', 'tiers', 'tier', 'subscription', 'subscriptions'],
|
|
61
|
-
chat: ['messaging', 'message', 'messages', 'conversation', 'inbox', 'dm'],
|
|
62
|
-
settings: ['preferences', 'config', 'configuration', 'account'],
|
|
63
|
-
calendar: ['schedule', 'scheduling', 'events', 'event', 'month', 'agenda'],
|
|
64
|
-
table: ['list', 'rows', 'records', 'grid', 'spreadsheet', 'datatable'],
|
|
65
|
-
gallery: ['photos', 'photo', 'images', 'image', 'pictures'],
|
|
66
|
-
hero: ['banner', 'splash', 'headline', 'landing'],
|
|
67
|
-
form: ['fields', 'input', 'inputs', 'survey'],
|
|
68
|
-
profile: ['bio', 'avatar', 'user'],
|
|
69
|
-
documentation: ['docs', 'reference', 'guide', 'api'],
|
|
70
|
-
navigation: ['nav', 'menu', 'sidebar'],
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Flatten into a token -> Set(expansions) lookup (bidirectional).
|
|
74
|
-
const SYNONYM_INDEX = (() => {
|
|
75
|
-
const idx = new Map();
|
|
76
|
-
const add = (a, b) => {
|
|
77
|
-
if (!idx.has(a)) idx.set(a, new Set());
|
|
78
|
-
idx.get(a).add(b);
|
|
79
|
-
};
|
|
80
|
-
for (const [key, vals] of Object.entries(SYNONYMS)) {
|
|
81
|
-
for (const v of vals) {
|
|
82
|
-
add(key, v);
|
|
83
|
-
add(v, key);
|
|
84
|
-
for (const v2 of vals) if (v2 !== v) add(v, v2);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return idx;
|
|
88
|
-
})();
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Light stemmer: strips common English suffixes so "charts"/"charting" and
|
|
92
|
-
* "chart" share a root. Deliberately crude (no Porter) — good enough to bridge
|
|
93
|
-
* plural/gerund gaps without a dependency.
|
|
94
|
-
* @param {string} w
|
|
95
|
-
* @returns {string}
|
|
96
|
-
*/
|
|
97
|
-
export function stem(w) {
|
|
98
|
-
let s = w;
|
|
99
|
-
for (const suf of ['ing', 'ed', 'ies', 'es', 's']) {
|
|
100
|
-
if (s.length > suf.length + 2 && s.endsWith(suf)) {
|
|
101
|
-
s = suf === 'ies' ? s.slice(0, -3) + 'y' : s.slice(0, -suf.length);
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return s;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
49
|
/** Valid domain filters for `--type`. */
|
|
110
50
|
export const SEARCH_DOMAINS = ['component', 'hook', 'doc', 'template'];
|
|
111
51
|
|
|
112
|
-
/**
|
|
113
|
-
* Filler words stripped from multi-word queries so natural-language phrasing
|
|
114
|
-
* ("a page where you can see business stats") ranks on its content words.
|
|
115
|
-
*/
|
|
116
|
-
const STOPWORDS = new Set([
|
|
117
|
-
'a', 'an', 'the', 'of', 'for', 'to', 'with', 'and', 'or', 'in', 'on', 'at',
|
|
118
|
-
'by', 'that', 'this', 'my', 'your', 'our', 'their', 'is', 'are', 'be', 'it',
|
|
119
|
-
'its', 'as', 'from', 'page', 'screen', 'app', 'application', 'view', 'where',
|
|
120
|
-
'you', 'can', 'some', 'like', 'just', 'basically', 'kinda', 'want', 'wants',
|
|
121
|
-
'need', 'needs', 'something', 'thing', 'things', 'build', 'make', 'create',
|
|
122
|
-
'i', 'me', 'we', 'us', 'so', 'up', 'out', 'over', 'side', 'one', 'big',
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Split a query into meaningful content tokens (lowercased, stopwords + very
|
|
127
|
-
* short words removed). Empty for single-word queries (callers fall back to
|
|
128
|
-
* whole-phrase scoring).
|
|
129
|
-
* @param {string} term - Already-lowercased query.
|
|
130
|
-
* @returns {string[]}
|
|
131
|
-
*/
|
|
132
|
-
export function tokenizeQuery(term) {
|
|
133
|
-
return term
|
|
134
|
-
.split(/\s+/)
|
|
135
|
-
// Strip only leading/trailing punctuation; keep joined identifiers intact
|
|
136
|
-
// (e.g. "foo_bar" stays one token) so gibberish stays gibberish.
|
|
137
|
-
.map(t => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, ''))
|
|
138
|
-
.filter(t => t.length >= 2 && !STOPWORDS.has(t));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Score a candidate against a query, handling multi-word natural language.
|
|
143
|
-
* Tries the whole phrase (so exact/near matches still win) AND a per-token
|
|
144
|
-
* pass (so "data table with filters" matches `table-page` via table+filter),
|
|
145
|
-
* and returns whichever is stronger.
|
|
146
|
-
*
|
|
147
|
-
* @param {string} term - Lowercased full query.
|
|
148
|
-
* @param {string[]} tokens - Content tokens from tokenizeQuery(term).
|
|
149
|
-
* @param {object} candidate
|
|
150
|
-
* @returns {{score: number, reason: string} | null}
|
|
151
|
-
*/
|
|
152
|
-
/**
|
|
153
|
-
* Minimum per-token score (in the multi-word pass) to count as a real match.
|
|
154
|
-
* 50 = a genuine name/keyword/description hit; below that is loose Levenshtein
|
|
155
|
-
* fuzz that would otherwise turn gibberish queries into noise.
|
|
156
|
-
*/
|
|
157
|
-
const MIN_TOKEN_SCORE = 50;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Best score for a token against a candidate, fanning out through synonyms
|
|
161
|
-
* (synonym hits are discounted so a direct hit always wins).
|
|
162
|
-
* @returns {{score: number, reason: string} | null}
|
|
163
|
-
*/
|
|
164
|
-
function bestForToken(tok, candidate) {
|
|
165
|
-
let best = scoreCandidate(tok, candidate);
|
|
166
|
-
const syns = SYNONYM_INDEX.get(tok);
|
|
167
|
-
if (syns) {
|
|
168
|
-
for (const s of syns) {
|
|
169
|
-
const h = scoreCandidate(s, candidate);
|
|
170
|
-
if (h) {
|
|
171
|
-
const score = Math.round(h.score * 0.85);
|
|
172
|
-
if (!best || score > best.score) best = {score, reason: `${h.reason} (~${tok})`};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return best;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function scoreQuery(term, tokens, candidate) {
|
|
180
|
-
const full = scoreCandidate(term, candidate);
|
|
181
|
-
|
|
182
|
-
// 0–1 content tokens: keep whole-phrase fuzzy matching (typo tolerance for
|
|
183
|
-
// single words), but if stopwords left exactly one DIFFERENT token (e.g.
|
|
184
|
-
// "pricing page" → "pricing"), score that token too and take the stronger.
|
|
185
|
-
if (tokens.length <= 1) {
|
|
186
|
-
const single = tokens.length === 1 ? bestForToken(tokens[0], candidate) : null;
|
|
187
|
-
if (full && (!single || full.score >= single.score)) return full;
|
|
188
|
-
return single;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Multi-word natural language: score each content token, counting only
|
|
192
|
-
// strong hits, then reward coverage so candidates matching more terms win.
|
|
193
|
-
let sum = 0;
|
|
194
|
-
let matched = 0;
|
|
195
|
-
const hitTerms = [];
|
|
196
|
-
for (const tok of tokens) {
|
|
197
|
-
const h = bestForToken(tok, candidate);
|
|
198
|
-
if (h && h.score >= MIN_TOKEN_SCORE) {
|
|
199
|
-
sum += h.score;
|
|
200
|
-
matched++;
|
|
201
|
-
hitTerms.push(tok);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (matched === 0) return full;
|
|
205
|
-
|
|
206
|
-
// Reward the AVERAGE strength of the concepts that matched (not divided by
|
|
207
|
-
// total query length — that penalizes verbose / low-fidelity prompts), plus
|
|
208
|
-
// a bonus per additional matched concept and a coverage term. A candidate
|
|
209
|
-
// that matches several of the query's concepts beats one matching a single
|
|
210
|
-
// incidental word.
|
|
211
|
-
const avgMatched = sum / matched;
|
|
212
|
-
const coverage = matched / tokens.length;
|
|
213
|
-
const tokenScore = Math.round(avgMatched + Math.min(matched - 1, 3) * 12 + coverage * 15);
|
|
214
|
-
|
|
215
|
-
if (full && full.score >= tokenScore) return full;
|
|
216
|
-
return {
|
|
217
|
-
score: tokenScore,
|
|
218
|
-
reason: `matches ${matched}/${tokens.length} terms: ${hitTerms.join(', ')}`,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
52
|
/**
|
|
223
53
|
* Score a single candidate against the search term across name, keywords,
|
|
224
54
|
* and prose signals. Returns the best (highest) score plus a human reason,
|
|
@@ -277,13 +107,10 @@ export function scoreCandidate(term, {name, keywords = [], description = '', pro
|
|
|
277
107
|
else if (dist === 2) consider(30, `keyword "${kw}" (distance ${dist})`);
|
|
278
108
|
}
|
|
279
109
|
|
|
280
|
-
// ── Prose / description signals (
|
|
281
|
-
// Match the term's stem as a whole word, tolerating plural/gerund suffixes
|
|
282
|
-
// so "chart" matches "charts" and "filter" matches "filtering".
|
|
110
|
+
// ── Prose / description signals (whole-word boundary) ───────────
|
|
283
111
|
if (term.length >= 3) {
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const re = new RegExp(`\\b${escaped}(s|es|ing|ed|ies)?\\b`);
|
|
112
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
const re = new RegExp(`\\b${escaped}\\b`);
|
|
287
114
|
if (description && re.test(description.toLowerCase())) {
|
|
288
115
|
consider(50, `description mentions "${term}"`);
|
|
289
116
|
} else {
|
|
@@ -413,30 +240,14 @@ async function gatherTemplates(cwd) {
|
|
|
413
240
|
} catch {
|
|
414
241
|
return [];
|
|
415
242
|
}
|
|
416
|
-
return templates.map(t => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
keywords = keywords.concat(extractComponents(t.filePath));
|
|
425
|
-
} catch {
|
|
426
|
-
// Best-effort: skip keyword enrichment if the source can't be read.
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (t.category) keywords = keywords.concat(t.category.split(/[^A-Za-z0-9]+/).filter(Boolean));
|
|
430
|
-
}
|
|
431
|
-
return {
|
|
432
|
-
domain: 'template',
|
|
433
|
-
name: t.dirName,
|
|
434
|
-
keywords,
|
|
435
|
-
description: t.description || '',
|
|
436
|
-
_displayName: t.name,
|
|
437
|
-
_kind: t.type, // 'page' | 'block'
|
|
438
|
-
};
|
|
439
|
-
});
|
|
243
|
+
return templates.map(t => ({
|
|
244
|
+
domain: 'template',
|
|
245
|
+
name: t.dirName,
|
|
246
|
+
keywords: Array.isArray(t.componentsUsed) ? t.componentsUsed : [],
|
|
247
|
+
description: t.description || '',
|
|
248
|
+
_displayName: t.name,
|
|
249
|
+
_kind: t.type, // 'page' | 'block'
|
|
250
|
+
}));
|
|
440
251
|
}
|
|
441
252
|
|
|
442
253
|
/**
|
|
@@ -514,7 +325,6 @@ export async function search(query, options = {}) {
|
|
|
514
325
|
}
|
|
515
326
|
|
|
516
327
|
const term = String(query).trim().toLowerCase();
|
|
517
|
-
const tokens = tokenizeQuery(term);
|
|
518
328
|
|
|
519
329
|
const coreDir = findCoreDir(cwd);
|
|
520
330
|
if (!coreDir) {
|
|
@@ -532,13 +342,9 @@ export async function search(query, options = {}) {
|
|
|
532
342
|
|
|
533
343
|
const all = [...components, ...hooks, ...docTopics, ...templates];
|
|
534
344
|
|
|
535
|
-
// Score every candidate on its own merits. The consumer groups results by
|
|
536
|
-
// role (page / block / component) and takes the top of each, so there's no
|
|
537
|
-
// cross-role competition to engineer — a target page only needs to be the
|
|
538
|
-
// strongest PAGE, not outrank every component.
|
|
539
345
|
const scored = [];
|
|
540
346
|
for (const candidate of all) {
|
|
541
|
-
const hit =
|
|
347
|
+
const hit = scoreCandidate(term, candidate);
|
|
542
348
|
if (hit) scored.push(toResult(candidate, hit.score, hit.reason));
|
|
543
349
|
}
|
|
544
350
|
|
package/src/api/template.mjs
CHANGED
|
@@ -95,7 +95,6 @@ async function discoverPages() {
|
|
|
95
95
|
dirName: dir.name,
|
|
96
96
|
name: doc?.name || dir.name,
|
|
97
97
|
description: doc?.description || '',
|
|
98
|
-
category: doc?.category || '',
|
|
99
98
|
isReady: doc?.isReady ?? true,
|
|
100
99
|
scaffold: doc?.scaffold ?? false,
|
|
101
100
|
filePath: path.join(dirPath, 'page.tsx'),
|
|
@@ -246,7 +245,7 @@ const UBIQUITOUS = new Set([
|
|
|
246
245
|
'StackItem', 'Icon',
|
|
247
246
|
]);
|
|
248
247
|
|
|
249
|
-
|
|
248
|
+
function extractComponents(pagePath) {
|
|
250
249
|
const src = fs.readFileSync(pagePath, 'utf-8');
|
|
251
250
|
// Match JSX opening tags, e.g. `<Section` or the legacy `<XDSSection`.
|
|
252
251
|
// Templates author bare component names post un-prefix migration
|
|
@@ -17,7 +17,6 @@ const registry = new Map([
|
|
|
17
17
|
['0.0.13', () => import('./transforms/v0.0.13/index.mjs')],
|
|
18
18
|
['0.0.14', () => import('./transforms/v0.0.14/index.mjs')],
|
|
19
19
|
['0.0.15', () => import('./transforms/v0.0.15/index.mjs')],
|
|
20
|
-
['0.1.0', () => import('./transforms/v0.1.0/index.mjs')],
|
|
21
20
|
]);
|
|
22
21
|
|
|
23
22
|
// Re-export from the shared utility so registry callers and other consumers
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
|
+
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
14
15
|
import {humanLog} from '../lib/json.mjs';
|
|
15
16
|
|
|
16
17
|
// Known corruption patterns that indicate a broken transform.
|
|
@@ -86,6 +87,39 @@ function findSourceFiles(dir) {
|
|
|
86
87
|
return results.sort();
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Detect the project's formatter and run it on changed files.
|
|
92
|
+
* Tries prettier, then biome. Silently skips if none found.
|
|
93
|
+
*
|
|
94
|
+
* @param {string[]} files - Absolute paths to files that were modified
|
|
95
|
+
* @param {boolean} [silent] - Suppress human-facing output
|
|
96
|
+
*/
|
|
97
|
+
async function formatChangedFiles(files, silent = false) {
|
|
98
|
+
if (files.length === 0) return;
|
|
99
|
+
|
|
100
|
+
const {execSync} = await import('node:child_process');
|
|
101
|
+
const fileArgs = files.map(f => `"${f}"`).join(' ');
|
|
102
|
+
const prefix = getRunPrefix();
|
|
103
|
+
|
|
104
|
+
const formatters = [
|
|
105
|
+
{name: 'prettier', cmd: `${prefix} prettier --write ${fileArgs}`},
|
|
106
|
+
{name: 'biome', cmd: `${prefix} biome format --write ${fileArgs}`},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const {name, cmd} of formatters) {
|
|
110
|
+
try {
|
|
111
|
+
execSync(cmd, {stdio: 'pipe', timeout: 30000});
|
|
112
|
+
if (!silent)
|
|
113
|
+
p.log.info(
|
|
114
|
+
`Formatted ${files.length} file${files.length === 1 ? '' : 's'} with ${name}`,
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
} catch {
|
|
118
|
+
// Formatter not available or failed — try next
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
/**
|
|
90
124
|
* Validate transform output before writing to disk.
|
|
91
125
|
*
|
|
@@ -128,86 +162,6 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
|
|
|
128
162
|
return {valid: true};
|
|
129
163
|
}
|
|
130
164
|
|
|
131
|
-
function isConfigCodemod(transformEntry) {
|
|
132
|
-
return transformEntry.meta?.codemodType === 'config';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const CONFIG_CODEMOD_PATHS = new Set([
|
|
136
|
-
'package.json',
|
|
137
|
-
'astryx.config.mjs',
|
|
138
|
-
'xds.config.mjs',
|
|
139
|
-
]);
|
|
140
|
-
|
|
141
|
-
function resolveConfigPath(relativePath) {
|
|
142
|
-
if (!CONFIG_CODEMOD_PATHS.has(relativePath)) {
|
|
143
|
-
throw new Error(`unsupported config codemod path: ${relativePath}`);
|
|
144
|
-
}
|
|
145
|
-
return path.resolve(process.cwd(), relativePath);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function readOptionalConfigFile(relativePath) {
|
|
149
|
-
const fullPath = resolveConfigPath(relativePath);
|
|
150
|
-
if (!fs.existsSync(fullPath)) return null;
|
|
151
|
-
return {path: relativePath, source: fs.readFileSync(fullPath, 'utf-8')};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getConfigCodemodContext() {
|
|
155
|
-
return {
|
|
156
|
-
packageJson: readOptionalConfigFile('package.json'),
|
|
157
|
-
astryxConfig: readOptionalConfigFile('astryx.config.mjs'),
|
|
158
|
-
xdsConfig: readOptionalConfigFile('xds.config.mjs'),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function runConfigCodemod(transformEntry, {apply, log}) {
|
|
163
|
-
const {transform} = transformEntry;
|
|
164
|
-
const api = {config: getConfigCodemodContext()};
|
|
165
|
-
const result = await transform({path: process.cwd(), source: ''}, api);
|
|
166
|
-
const errors = result?.errors ?? [];
|
|
167
|
-
if (errors.length > 0) {
|
|
168
|
-
for (const error of errors) {
|
|
169
|
-
log.error(` ✗ ${error.file ?? 'config'} — ${error.error}`);
|
|
170
|
-
}
|
|
171
|
-
return {filesChanged: 0, errors};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const changes = result?.changes ?? [];
|
|
175
|
-
if (changes.length === 0) return {filesChanged: 0, errors: []};
|
|
176
|
-
|
|
177
|
-
for (const change of changes) {
|
|
178
|
-
const fullPath = resolveConfigPath(change.path);
|
|
179
|
-
if (change.delete) {
|
|
180
|
-
if (apply) fs.rmSync(fullPath, {force: true});
|
|
181
|
-
log[apply ? 'success' : 'warn'](
|
|
182
|
-
` ${apply ? '✓' : '~'} ${change.path} (delete${apply ? '' : ', dry run'})`,
|
|
183
|
-
);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (apply) {
|
|
188
|
-
fs.writeFileSync(fullPath, change.source, 'utf-8');
|
|
189
|
-
}
|
|
190
|
-
log[apply ? 'success' : 'warn'](
|
|
191
|
-
` ${apply ? '✓' : '~'} ${change.path}${apply ? '' : ' (would change)'}`,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (changes.length > 0) {
|
|
196
|
-
const verb = apply ? 'Updated' : 'Would update';
|
|
197
|
-
log.info(
|
|
198
|
-
` ${verb} ${changes.length} config file${changes.length === 1 ? '' : 's'}`,
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
filesChanged: changes.length,
|
|
204
|
-
writtenFiles: changes
|
|
205
|
-
.filter(change => !change.delete && path.extname(change.path) !== '.json')
|
|
206
|
-
.map(change => resolveConfigPath(change.path)),
|
|
207
|
-
errors: [],
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
165
|
/**
|
|
212
166
|
* Run codemods against source files.
|
|
213
167
|
*
|
|
@@ -243,12 +197,18 @@ export async function runCodemods(
|
|
|
243
197
|
|
|
244
198
|
if (files.length === 0) {
|
|
245
199
|
log.warn('No source files found.');
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
totalFilesChanged: 0,
|
|
203
|
+
totalTransformsApplied: 0,
|
|
204
|
+
errors: [],
|
|
205
|
+
writtenFiles: [],
|
|
206
|
+
skippedOptional: [],
|
|
207
|
+
};
|
|
250
208
|
}
|
|
251
209
|
|
|
210
|
+
log.info(`Found ${files.length} source file${files.length === 1 ? '' : 's'}`);
|
|
211
|
+
|
|
252
212
|
// Dynamically import jscodeshift
|
|
253
213
|
const jscodeshift = (await import('jscodeshift')).default;
|
|
254
214
|
|
|
@@ -279,24 +239,6 @@ export async function runCodemods(
|
|
|
279
239
|
|
|
280
240
|
log.info(` ${meta.title}`);
|
|
281
241
|
|
|
282
|
-
if (isConfigCodemod(transformEntry)) {
|
|
283
|
-
const result = await runConfigCodemod(transformEntry, {apply, log});
|
|
284
|
-
if (result.errors.length > 0) {
|
|
285
|
-
for (const error of result.errors) {
|
|
286
|
-
errors.push({
|
|
287
|
-
file: error.file ?? 'config',
|
|
288
|
-
codemod: name,
|
|
289
|
-
error: error.error,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
} else if (result.filesChanged > 0) {
|
|
293
|
-
totalFilesChanged += result.filesChanged;
|
|
294
|
-
totalTransformsApplied += result.filesChanged;
|
|
295
|
-
writtenFiles.push(...(result.writtenFiles ?? []));
|
|
296
|
-
}
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
242
|
let filesChanged = 0;
|
|
301
243
|
|
|
302
244
|
for (const filePath of files) {
|
|
@@ -381,6 +323,12 @@ export async function runCodemods(
|
|
|
381
323
|
}
|
|
382
324
|
}
|
|
383
325
|
|
|
326
|
+
// Post-codemod formatting: run the project's formatter on changed files
|
|
327
|
+
// so codemods don't introduce style drift (jscodeshift may change quotes, etc.)
|
|
328
|
+
if (apply && writtenFiles.length > 0) {
|
|
329
|
+
await formatChangedFiles(writtenFiles, silent);
|
|
330
|
+
}
|
|
331
|
+
|
|
384
332
|
if (totalValidationBlocked > 0) {
|
|
385
333
|
log.warn(
|
|
386
334
|
`${totalValidationBlocked} file${totalValidationBlocked === 1 ? ' was' : 's were'} blocked by validation — no changes written to ${totalValidationBlocked === 1 ? 'that file' : 'those files'}.`,
|
|
@@ -418,9 +366,7 @@ export async function runCodemods(
|
|
|
418
366
|
if (meta.description) {
|
|
419
367
|
log.info(` ${meta.description}`);
|
|
420
368
|
}
|
|
421
|
-
log.info(
|
|
422
|
-
` Run: astryx upgrade --codemod ${name} --path <dir> --apply`,
|
|
423
|
-
);
|
|
369
|
+
log.info(` Run: astryx upgrade --codemod ${name} --path <dir> --apply`);
|
|
424
370
|
}
|
|
425
371
|
}
|
|
426
372
|
|