@astryxdesign/cli 0.1.1 → 0.1.2-canary.03536f1
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.1.0/index.mjs +0 -9
- 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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Shared codemod execution primitives.
|
|
5
|
+
*
|
|
6
|
+
* Both the core registry runner (`runner.mjs` / `upgrade.mjs`) and the
|
|
7
|
+
* integration runner (`integration-runner.mjs`) execute codemods that follow
|
|
8
|
+
* the unified file-based contract:
|
|
9
|
+
*
|
|
10
|
+
* (file, api) => string | null | undefined
|
|
11
|
+
*
|
|
12
|
+
* where `file` is `{path, source}` and `api` is
|
|
13
|
+
* `{jscodeshift, stats, report}`. Config codemods target the consumer's
|
|
14
|
+
* astryx.config.* file; code codemods are applied to source files discovered
|
|
15
|
+
* under `--path`, filtered by each codemod's `fileExtensions`.
|
|
16
|
+
*
|
|
17
|
+
* A codemod ENTRY is normalized to a single shape across both callers:
|
|
18
|
+
*
|
|
19
|
+
* {id, type: 'code' | 'config', codemod: {title, transform, fileExtensions?,
|
|
20
|
+
* isOptional?}, package, version}
|
|
21
|
+
*
|
|
22
|
+
* Integration discovery emits this shape directly. The core registry stores
|
|
23
|
+
* entries as `{name, transform, meta}`; `runner.mjs` normalizes those to this
|
|
24
|
+
* shape at the boundary (see `runner.mjs`).
|
|
25
|
+
*
|
|
26
|
+
* Both kinds reuse the shared output validation from runner.mjs and surface a
|
|
27
|
+
* transform throw as an error (strictness contract).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as p from '@clack/prompts';
|
|
33
|
+
import {findConfigPath} from '../lib/project.mjs';
|
|
34
|
+
import {fixDirectiveCorruption, validateOutput} from './runner.mjs';
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_CODE_EXTENSIONS = [
|
|
37
|
+
'.tsx',
|
|
38
|
+
'.ts',
|
|
39
|
+
'.jsx',
|
|
40
|
+
'.js',
|
|
41
|
+
'.mjs',
|
|
42
|
+
'.cjs',
|
|
43
|
+
];
|
|
44
|
+
const PARSEABLE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Recursively find candidate source files in a directory.
|
|
48
|
+
* @param {string} dir
|
|
49
|
+
* @returns {string[]}
|
|
50
|
+
*/
|
|
51
|
+
export function findSourceFiles(dir) {
|
|
52
|
+
const results = [];
|
|
53
|
+
function walk(currentDir) {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = fs.readdirSync(currentDir, {withFileTypes: true});
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
64
|
+
walk(fullPath);
|
|
65
|
+
} else {
|
|
66
|
+
results.push(fullPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(dir);
|
|
71
|
+
return results.sort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* No-op log surface for silent (`--json`) mode.
|
|
76
|
+
* @param {boolean} silent
|
|
77
|
+
*/
|
|
78
|
+
export function makeLog(silent) {
|
|
79
|
+
return silent
|
|
80
|
+
? {step() {}, info() {}, success() {}, warn() {}, error() {}, message() {}}
|
|
81
|
+
: p.log;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply a config codemod to the consumer's astryx.config.* file.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} entry normalized codemod entry {id, codemod, package}
|
|
88
|
+
* @param {{apply: boolean, log: object, jscodeshift: Function}} ctx
|
|
89
|
+
* @returns {{filesChanged: number, writtenFiles: string[], errors: Array}}
|
|
90
|
+
*/
|
|
91
|
+
export function runConfigCodemod(entry, {apply, log, jscodeshift}) {
|
|
92
|
+
const {codemod, id, package: pkg} = entry;
|
|
93
|
+
const name = `${pkg}:${id}`;
|
|
94
|
+
const configPath = findConfigPath(process.cwd());
|
|
95
|
+
if (!configPath) {
|
|
96
|
+
log.info(` ${codemod.title} — no astryx.config.* found; skipping.`);
|
|
97
|
+
return {filesChanged: 0, writtenFiles: [], errors: []};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const relativePath = path.relative(process.cwd(), configPath);
|
|
101
|
+
try {
|
|
102
|
+
const source = fs.readFileSync(configPath, 'utf-8');
|
|
103
|
+
const ext = path.extname(configPath);
|
|
104
|
+
const parser = ext === '.tsx' || ext === '.ts' ? 'tsx' : 'babel';
|
|
105
|
+
const j = jscodeshift.withParser(parser);
|
|
106
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
107
|
+
let result = codemod.transform({source, path: configPath}, api);
|
|
108
|
+
|
|
109
|
+
if (result == null || result === source) {
|
|
110
|
+
return {filesChanged: 0, writtenFiles: [], errors: []};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
result = fixDirectiveCorruption(result);
|
|
114
|
+
const validation = validateOutput(result, source, j, {
|
|
115
|
+
parse: PARSEABLE_EXTENSIONS.includes(ext),
|
|
116
|
+
});
|
|
117
|
+
if (!validation.valid) {
|
|
118
|
+
log.error(` ✗ ${relativePath} — ${validation.reason}`);
|
|
119
|
+
return {
|
|
120
|
+
filesChanged: 0,
|
|
121
|
+
writtenFiles: [],
|
|
122
|
+
errors: [{file: relativePath, codemod: name, error: validation.reason}],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (apply) {
|
|
127
|
+
fs.writeFileSync(configPath, result, 'utf-8');
|
|
128
|
+
log.success(` ✓ ${relativePath}`);
|
|
129
|
+
} else {
|
|
130
|
+
log.warn(` ~ ${relativePath} (would change)`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
filesChanged: 1,
|
|
134
|
+
writtenFiles: apply ? [configPath] : [],
|
|
135
|
+
errors: [],
|
|
136
|
+
};
|
|
137
|
+
} catch (err) {
|
|
138
|
+
log.error(` ✗ ${relativePath} — ${err.message}`);
|
|
139
|
+
return {
|
|
140
|
+
filesChanged: 0,
|
|
141
|
+
writtenFiles: [],
|
|
142
|
+
errors: [{file: relativePath, codemod: name, error: err.message}],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Apply a code codemod to discovered source files.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} entry normalized codemod entry {id, codemod, package}
|
|
151
|
+
* @param {string[]} files
|
|
152
|
+
* @param {{apply: boolean, log: object, jscodeshift: Function}} ctx
|
|
153
|
+
* @returns {{filesChanged: number, writtenFiles: string[], errors: Array}}
|
|
154
|
+
*/
|
|
155
|
+
export function runCodeCodemod(entry, files, {apply, log, jscodeshift}) {
|
|
156
|
+
const {codemod, id, package: pkg} = entry;
|
|
157
|
+
const name = `${pkg}:${id}`;
|
|
158
|
+
const extensions = new Set(codemod.fileExtensions ?? DEFAULT_CODE_EXTENSIONS);
|
|
159
|
+
|
|
160
|
+
let filesChanged = 0;
|
|
161
|
+
const writtenFiles = [];
|
|
162
|
+
const errors = [];
|
|
163
|
+
|
|
164
|
+
for (const filePath of files) {
|
|
165
|
+
const ext = path.extname(filePath);
|
|
166
|
+
if (!extensions.has(ext)) continue;
|
|
167
|
+
|
|
168
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
169
|
+
try {
|
|
170
|
+
const source = fs.readFileSync(filePath, 'utf-8');
|
|
171
|
+
const parser = ext === '.tsx' || ext === '.ts' ? 'tsx' : 'babel';
|
|
172
|
+
const j = jscodeshift.withParser(parser);
|
|
173
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
174
|
+
let result = codemod.transform({source, path: filePath}, api);
|
|
175
|
+
|
|
176
|
+
if (result == null || result === source) continue;
|
|
177
|
+
|
|
178
|
+
result = fixDirectiveCorruption(result);
|
|
179
|
+
const validation = validateOutput(result, source, j, {
|
|
180
|
+
parse: PARSEABLE_EXTENSIONS.includes(ext),
|
|
181
|
+
});
|
|
182
|
+
if (!validation.valid) {
|
|
183
|
+
log.error(` ✗ ${relativePath} — ${validation.reason}`);
|
|
184
|
+
errors.push({
|
|
185
|
+
file: relativePath,
|
|
186
|
+
codemod: name,
|
|
187
|
+
error: validation.reason,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
filesChanged++;
|
|
193
|
+
if (apply) {
|
|
194
|
+
fs.writeFileSync(filePath, result, 'utf-8');
|
|
195
|
+
writtenFiles.push(filePath);
|
|
196
|
+
log.success(` ✓ ${relativePath}`);
|
|
197
|
+
} else {
|
|
198
|
+
log.warn(` ~ ${relativePath} (would change)`);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
log.error(` ✗ ${relativePath} — ${err.message}`);
|
|
202
|
+
errors.push({file: relativePath, codemod: name, error: err.message});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {filesChanged, writtenFiles, errors};
|
|
207
|
+
}
|
package/src/codemods/runner.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import * as fs from 'node:fs';
|
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
import * as p from '@clack/prompts';
|
|
14
14
|
import {humanLog} from '../lib/json.mjs';
|
|
15
|
+
import {runConfigCodemod} from './run-codemod.mjs';
|
|
15
16
|
|
|
16
17
|
// Known corruption patterns that indicate a broken transform.
|
|
17
18
|
// Each entry: [regex, human-readable description]
|
|
@@ -128,83 +129,42 @@ export function validateOutput(result, source, j, {parse = true} = {}) {
|
|
|
128
129
|
return {valid: true};
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Normalize a core registry transform entry to the unified codemod entry shape
|
|
134
|
+
* consumed by the shared runner (`run-codemod.mjs`).
|
|
135
|
+
*
|
|
136
|
+
* Core registry entries are stored as `{name, transform, meta, optional}`.
|
|
137
|
+
* The shared runner — the same one integration codemods use — operates on
|
|
138
|
+
* `{id, type, codemod: {title, transform, fileExtensions?, isOptional?},
|
|
139
|
+
* package, version}`.
|
|
140
|
+
*
|
|
141
|
+
* CONVENTION — how a core registry entry signals it is a CONFIG codemod:
|
|
142
|
+
* set `meta.codemodType === 'config'` on the entry (the default is a 'code'
|
|
143
|
+
* codemod). A config codemod runs against the consumer's astryx.config.* file
|
|
144
|
+
* via the unified `(file, api)`/jscodeshift contract; a code codemod runs
|
|
145
|
+
* against discovered source files. Any future core config codemod (e.g. a
|
|
146
|
+
* v0.1.3 one) must set `meta.codemodType = 'config'` and author its transform
|
|
147
|
+
* with the same `(file, api) => string | null | undefined` contract used by
|
|
148
|
+
* `createConfigCodemod`.
|
|
149
|
+
*
|
|
150
|
+
* @param {{name: string, transform: Function, meta: object, optional?: boolean}} transformEntry
|
|
151
|
+
* @param {string} version
|
|
152
|
+
* @returns {{id: string, type: 'code'|'config', codemod: object, package: string, version: string}}
|
|
153
|
+
*/
|
|
154
|
+
function toUnifiedEntry(transformEntry, version) {
|
|
155
|
+
const {name, transform, meta, optional} = transformEntry;
|
|
156
|
+
const type = meta?.codemodType === 'config' ? 'config' : 'code';
|
|
202
157
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
158
|
+
id: name,
|
|
159
|
+
type,
|
|
160
|
+
codemod: {
|
|
161
|
+
title: meta.title,
|
|
162
|
+
transform,
|
|
163
|
+
fileExtensions: meta.fileExtensions,
|
|
164
|
+
isOptional: !!optional,
|
|
165
|
+
},
|
|
166
|
+
package: 'core',
|
|
167
|
+
version,
|
|
208
168
|
};
|
|
209
169
|
}
|
|
210
170
|
|
|
@@ -216,11 +176,12 @@ async function runConfigCodemod(transformEntry, {apply, log}) {
|
|
|
216
176
|
* @param {boolean} options.apply - Write changes to disk
|
|
217
177
|
* @param {string} options.path - Source directory to scan
|
|
218
178
|
* @param {string|undefined} options.codemod - Run only this specific transform
|
|
179
|
+
* @param {Set<string>} [options.skipCodemods] - Transform names to exclude
|
|
219
180
|
* @param {boolean} [options.silent] - Suppress all human-facing output (for --json)
|
|
220
181
|
*/
|
|
221
182
|
export async function runCodemods(
|
|
222
183
|
versionManifests,
|
|
223
|
-
{apply, path: srcPath, codemod, silent = false},
|
|
184
|
+
{apply, path: srcPath, codemod, skipCodemods, silent = false},
|
|
224
185
|
) {
|
|
225
186
|
// No-op stub object so silent mode skips clack stdout entirely without
|
|
226
187
|
// littering the body with `if (!silent)` guards.
|
|
@@ -233,20 +194,31 @@ export async function runCodemods(
|
|
|
233
194
|
|
|
234
195
|
const resolvedPath = path.resolve(srcPath);
|
|
235
196
|
|
|
236
|
-
|
|
197
|
+
// Config codemods target the consumer's astryx.config.* and never read
|
|
198
|
+
// source files, so a missing --path should not block them. Only hard-fail
|
|
199
|
+
// on a missing source path when there is at least one CODE codemod to run.
|
|
200
|
+
const hasCodeCodemod = versionManifests.some(({transforms}) =>
|
|
201
|
+
transforms.some(t => t.meta?.codemodType !== 'config'),
|
|
202
|
+
);
|
|
203
|
+
const sourcePathExists = fs.existsSync(resolvedPath);
|
|
204
|
+
|
|
205
|
+
if (!sourcePathExists && hasCodeCodemod) {
|
|
237
206
|
log.error(`Source path not found: ${resolvedPath}`);
|
|
238
207
|
return {ok: false, reason: 'source_path_missing', resolvedPath};
|
|
239
208
|
}
|
|
240
209
|
|
|
241
|
-
|
|
242
|
-
|
|
210
|
+
let files = [];
|
|
211
|
+
if (sourcePathExists) {
|
|
212
|
+
log.step(`Scanning ${resolvedPath} for source files...`);
|
|
213
|
+
files = findSourceFiles(resolvedPath);
|
|
243
214
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
215
|
+
if (files.length === 0) {
|
|
216
|
+
log.warn('No source files found.');
|
|
217
|
+
} else {
|
|
218
|
+
log.info(
|
|
219
|
+
`Found ${files.length} source file${files.length === 1 ? '' : 's'}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
250
222
|
}
|
|
251
223
|
|
|
252
224
|
// Dynamically import jscodeshift
|
|
@@ -265,6 +237,8 @@ export async function runCodemods(
|
|
|
265
237
|
for (const transformEntry of transforms) {
|
|
266
238
|
// Filter by codemod name if specified
|
|
267
239
|
if (codemod && transformEntry.name !== codemod) continue;
|
|
240
|
+
// Exclude explicitly skipped codemods (by transform name).
|
|
241
|
+
if (skipCodemods?.has(transformEntry.name)) continue;
|
|
268
242
|
|
|
269
243
|
const {name, transform, meta, optional} = transformEntry;
|
|
270
244
|
const transformExtensions = new Set(
|
|
@@ -279,20 +253,23 @@ export async function runCodemods(
|
|
|
279
253
|
|
|
280
254
|
log.info(` ${meta.title}`);
|
|
281
255
|
|
|
282
|
-
|
|
283
|
-
|
|
256
|
+
// Config codemods are routed through the SAME shared runner that
|
|
257
|
+
// integration config codemods use: the transform follows the unified
|
|
258
|
+
// `(file, api)` contract and targets the consumer's astryx.config.*.
|
|
259
|
+
// A core entry signals "config" via `meta.codemodType === 'config'`
|
|
260
|
+
// (see toUnifiedEntry).
|
|
261
|
+
if (meta?.codemodType === 'config') {
|
|
262
|
+
const result = runConfigCodemod(toUnifiedEntry(transformEntry, version), {
|
|
263
|
+
apply,
|
|
264
|
+
log,
|
|
265
|
+
jscodeshift,
|
|
266
|
+
});
|
|
284
267
|
if (result.errors.length > 0) {
|
|
285
|
-
|
|
286
|
-
errors.push({
|
|
287
|
-
file: error.file ?? 'config',
|
|
288
|
-
codemod: name,
|
|
289
|
-
error: error.error,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
268
|
+
errors.push(...result.errors);
|
|
292
269
|
} else if (result.filesChanged > 0) {
|
|
293
270
|
totalFilesChanged += result.filesChanged;
|
|
294
271
|
totalTransformsApplied += result.filesChanged;
|
|
295
|
-
writtenFiles.push(...
|
|
272
|
+
writtenFiles.push(...result.writtenFiles);
|
|
296
273
|
}
|
|
297
274
|
continue;
|
|
298
275
|
}
|
|
@@ -6,20 +6,11 @@
|
|
|
6
6
|
* Lists all codemods for the v0.1.0 release in the order they should run.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import migrateXdsConfigSurfaces, {
|
|
10
|
-
meta as migrateXdsConfigSurfacesMeta,
|
|
11
|
-
} from './migrate-xds-config-surfaces.mjs';
|
|
12
|
-
|
|
13
9
|
import migrateXdsModuleSpecifiers, {
|
|
14
10
|
meta as migrateXdsModuleSpecifiersMeta,
|
|
15
11
|
} from './migrate-xds-module-specifiers.mjs';
|
|
16
12
|
|
|
17
13
|
export default [
|
|
18
|
-
{
|
|
19
|
-
name: 'migrate-xds-config-surfaces',
|
|
20
|
-
transform: migrateXdsConfigSurfaces,
|
|
21
|
-
meta: migrateXdsConfigSurfacesMeta,
|
|
22
|
-
},
|
|
23
14
|
{
|
|
24
15
|
name: 'migrate-xds-module-specifiers',
|
|
25
16
|
transform: migrateXdsModuleSpecifiers,
|
|
@@ -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
|
+
];
|