@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.db4e378
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/README.md +86 -72
- package/api/build/build.doc.mjs +3 -3
- package/api/build/build.test.mjs +14 -3
- package/api/build/build.type.d.mts +35 -1
- package/api/build/build.type.mjs +24 -3
- package/api/build/help/help.d.mts +8 -5
- package/api/build/help/help.mjs +56 -6
- package/api/component/component.doc.mjs +12 -3
- package/api/component/component.mjs +31 -2
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +16 -5
- package/api/component/component.type.mjs +13 -5
- package/api/component/detail/blocks/blocks.d.mts +2 -1
- package/api/component/detail/blocks/blocks.mjs +4 -3
- package/api/component/list/list.d.mts +0 -5
- package/api/component/list/list.mjs +37 -9
- package/api/discover/discover.doc.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +7 -4
- package/api/init/init.doc.mjs +4 -0
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.mjs +11 -0
- package/api/integration/add-contribution.test.mjs +80 -0
- package/api/integration/integrationAdd.doc.mjs +7 -4
- package/api/integration/integrationAddComponent.doc.mjs +1 -1
- package/api/integration/validate-integration.mjs +2 -0
- package/api/integration/validate-integration.test.mjs +91 -0
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/parseResponse.doc.mjs +2 -2
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +2 -9
- package/api/search/search.doc.mjs +5 -1
- package/api/search/search.mjs +10 -3
- package/api/search/search.test.mjs +34 -0
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +1 -1
- package/api/template/copy/copy.mjs +17 -23
- package/api/template/copy/copy.test.mjs +17 -0
- package/api/template/template.doc.mjs +2 -1
- package/api/theme/add/add.binary.test.mjs +91 -0
- package/api/theme/add/add.mjs +20 -3
- package/api/theme/add/add.staging.test.mjs +66 -0
- package/api/theme/build/build.mjs +114 -37
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/palette/generate/generate.mjs +7 -2
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.mjs +8 -1
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +15 -0
- package/api/theme/themeBuild.doc.mjs +7 -4
- package/api/theme/themeTemplate.doc.mjs +4 -1
- package/api/upgrade/_adapter.mjs +27 -4
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +2 -1
- package/api/upgrade/upgrade.type.d.mts +4 -0
- package/api/upgrade/upgrade.type.mjs +1 -0
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transform-prop.mjs +109 -0
- package/assets/codemods/transform-prop.test.mjs +95 -0
- package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +86 -165
- package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +72 -210
- package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +83 -115
- package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +57 -186
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +61 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +126 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +86 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +99 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +122 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +95 -0
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/authoring/config/config.doc.mjs +1 -1
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/type.ts +15 -3
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +5 -5
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +4 -1
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +32 -47
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +40 -0
- package/clients/cli/commands/component/index.mjs +2 -7
- package/clients/cli/commands/component-package.test.mjs +46 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +4 -2
- package/clients/cli/commands/discover.mjs +3 -3
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor.mjs +6 -16
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +16 -5
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +19 -8
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +21 -5
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +14 -3
- package/clients/cli/commands/layout-expand.doc.mjs +21 -4
- package/clients/cli/commands/layout.doc.mjs +2 -2
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +6 -3
- package/clients/cli/commands/search.mjs +17 -7
- package/clients/cli/commands/search.test.mjs +75 -0
- package/clients/cli/commands/swizzle.doc.mjs +2 -1
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +28 -6
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/theme-add.doc.mjs +2 -1
- package/clients/cli/commands/theme-build.doc.mjs +7 -6
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +1 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +17 -6
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/index.mjs +13 -30
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +32 -6
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +97 -0
- package/clients/cli/lib/hook-format.mjs +5 -5
- package/clients/cli/lib/json-shim.mjs +38 -2
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +22 -0
- package/clients/cli/lib/manifest.test.mjs +17 -0
- package/foundation/agent-docs/agent-docs.d.mts +4 -0
- package/foundation/agent-docs/agent-docs.mjs +69 -3
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project.mjs +123 -9
- package/foundation/config/project.test.mjs +126 -0
- package/foundation/discovery/template-adapter.d.mts +14 -5
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +292 -25
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/integrations/autolink.mjs +6 -1
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/error-codes.doc.mjs +1 -2
- package/foundation/response/error-codes.mjs +1 -1
- package/foundation/response/error-codes.test.mjs +83 -0
- package/foundation/response/json-contract.test.mjs +11 -0
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.mjs +10 -10
- package/foundation/response/response-types.doc.test.mjs +158 -0
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +3 -2
- package/foundation/xle/expand.test.mjs +54 -0
- package/package.json +9 -9
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
|
@@ -73,7 +73,7 @@ export const doc = {
|
|
|
73
73
|
{
|
|
74
74
|
type: 'theme.build',
|
|
75
75
|
description:
|
|
76
|
-
'Build receipt: theme name
|
|
76
|
+
'Build receipt {name, tokenCount, componentCount, sizeKB, outputs, warnings, notices}: the theme name; how many tokens (portable plus theme-local) and component targets it overrides; the CSS size in KB; the written outputs {css, js, dts, and variantsDts when custom prop values were augmented}; warnings, the defects the author should fix, including exact canonical replacements for deprecated component target keys and declarations the generator dropped; and notices, advisories about a correct theme, such as a font it names but does not load. Resolves to null instead when the theme produced no CSS (nothing to build).',
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
type: 'theme.build.check',
|
|
@@ -87,10 +87,13 @@ export const doc = {
|
|
|
87
87
|
code: 'ERR_THEME_LOAD',
|
|
88
88
|
when: 'the file cannot be loaded or parsed into a defineTheme() result',
|
|
89
89
|
},
|
|
90
|
-
{
|
|
90
|
+
{
|
|
91
|
+
code: 'ERR_THEME_INVALID',
|
|
92
|
+
when: 'the resolved theme is invalid, for example it has no name, or a custom Heading type has no standalone rule with a declaration the compiler can emit',
|
|
93
|
+
},
|
|
91
94
|
{
|
|
92
95
|
code: 'ERR_PATH_TRAVERSAL',
|
|
93
|
-
when: 'the theme name contains a path separator or traversal marker',
|
|
96
|
+
when: 'the theme name contains a path separator or traversal marker, or a relative `out` path resolves outside cwd (including through a symlink)',
|
|
94
97
|
},
|
|
95
98
|
{
|
|
96
99
|
code: 'ERR_CORE_NOT_FOUND',
|
|
@@ -102,7 +105,7 @@ export const doc = {
|
|
|
102
105
|
},
|
|
103
106
|
{
|
|
104
107
|
code: 'ERR_WRITE_FAILED',
|
|
105
|
-
when: 'writing the outputs fails (staged temp files are rolled back)',
|
|
108
|
+
when: 'creating the output directory or writing the outputs fails (staged temp files are rolled back)',
|
|
106
109
|
},
|
|
107
110
|
],
|
|
108
111
|
examples: [
|
|
@@ -51,7 +51,10 @@ export const doc = {
|
|
|
51
51
|
'Receipt: the path (relative to cwd), whether it was written, and the reason it was not. `exists` when a file was already there, which is a success, not a failure.',
|
|
52
52
|
},
|
|
53
53
|
],
|
|
54
|
-
throws: [
|
|
54
|
+
throws: [
|
|
55
|
+
{code: 'ERR_PATH_TRAVERSAL', when: 'the target path escapes cwd'},
|
|
56
|
+
{code: 'ERR_WRITE_FAILED', when: 'the template file cannot be written'},
|
|
57
|
+
],
|
|
55
58
|
examples: [
|
|
56
59
|
{label: 'Write it at the project root', code: 'themeTemplate();'},
|
|
57
60
|
{
|
package/api/upgrade/_adapter.mjs
CHANGED
|
@@ -104,6 +104,27 @@ export function uniqueFiles(files) {
|
|
|
104
104
|
];
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Run app code with its stdout writes sent to stderr. Stdout carries only the
|
|
109
|
+
* command's own output, so a hook that prints cannot corrupt `--json`.
|
|
110
|
+
* @template T
|
|
111
|
+
* @param {() => T | Promise<T>} fn
|
|
112
|
+
* @returns {Promise<T>}
|
|
113
|
+
*/
|
|
114
|
+
async function withStdoutOnStderr(fn) {
|
|
115
|
+
const realWrite = process.stdout.write;
|
|
116
|
+
process.stdout.write = /** @type {any} */ (
|
|
117
|
+
function (/** @type {any} */ chunk, /** @type {any[]} */ ...rest) {
|
|
118
|
+
return process.stderr.write(chunk, ...rest);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
try {
|
|
122
|
+
return await fn();
|
|
123
|
+
} finally {
|
|
124
|
+
process.stdout.write = realWrite;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
/**
|
|
108
129
|
* Run the app config's post-codemod hooks (config.hooks.postCodemod).
|
|
109
130
|
* Dry-run PREVIEWS (buildCommand still called, so a throw fails); apply executes.
|
|
@@ -123,7 +144,9 @@ export async function runPostCodemodHooks(hooks, context) {
|
|
|
123
144
|
);
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
const cmd = await
|
|
147
|
+
const cmd = await withStdoutOnStderr(() =>
|
|
148
|
+
hook.buildCommand({packageDir, files}),
|
|
149
|
+
);
|
|
127
150
|
if (!cmd) {
|
|
128
151
|
logger.log(`Post-codemod hook ${label} produced no command; skipping.`);
|
|
129
152
|
continue;
|
|
@@ -147,7 +170,7 @@ export async function runPostCodemodHooks(hooks, context) {
|
|
|
147
170
|
env: {...process.env, ...(cmd.options?.env ?? {})},
|
|
148
171
|
}),
|
|
149
172
|
);
|
|
150
|
-
logger.log(
|
|
173
|
+
logger.log(`[ok] Post-codemod hook ${label} completed.`);
|
|
151
174
|
}
|
|
152
175
|
}
|
|
153
176
|
|
|
@@ -186,7 +209,7 @@ export async function prepareAgentDocsRefresh({
|
|
|
186
209
|
if (initial.status === 'missing') {
|
|
187
210
|
summary.action = 'nudge-init';
|
|
188
211
|
logger.warn(
|
|
189
|
-
`No Astryx agent-docs block found
|
|
212
|
+
`No Astryx agent-docs block found - AI agents have no component index. Run \`${formatCliCommand('astryx init --features agents')}\` to install it.`,
|
|
190
213
|
);
|
|
191
214
|
return {cwd, renderedBlock: null, summary};
|
|
192
215
|
}
|
|
@@ -240,7 +263,7 @@ export function applyAgentDocsRefresh(plan) {
|
|
|
240
263
|
summary.files = written;
|
|
241
264
|
summary.action = summary.refreshed ? 'refreshed' : 'error';
|
|
242
265
|
if (summary.refreshed) {
|
|
243
|
-
logger.log(
|
|
266
|
+
logger.log(`[ok] Agent docs refreshed -> ${written.join(', ')}`);
|
|
244
267
|
} else {
|
|
245
268
|
logger.warn(
|
|
246
269
|
`Agent docs look stale but couldn't be refreshed. Run \`${formatCliCommand('astryx init --features agents')}\` to reinstall the block.`,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import {collectAllCodemods} from '../_adapter.mjs';
|
|
12
12
|
import {logger} from '../../logger.mjs';
|
|
13
|
+
import {toAscii} from '../../../assets/codemods/term-log.mjs';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* List every available codemod (oldest→newest).
|
|
@@ -19,7 +20,7 @@ export async function list() {
|
|
|
19
20
|
const codemods = await collectAllCodemods();
|
|
20
21
|
logger.log('Available codemods:');
|
|
21
22
|
for (const {name, title, pr, optional} of codemods) {
|
|
22
|
-
logger.log(` ${name}
|
|
23
|
+
logger.log(` ${name} - ${toAscii(title)}${optional ? ' (optional)' : ''} (${pr})`);
|
|
23
24
|
}
|
|
24
25
|
logger.log('Done\n');
|
|
25
26
|
return {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Colocated tests for the upgrade.list leaf: every field an entry
|
|
5
|
+
* carries is declared on the canonical `UpgradeListEntry` type, and every
|
|
6
|
+
* declared field is present on every entry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect} from 'vitest';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import {fileURLToPath} from 'node:url';
|
|
12
|
+
import ts from 'typescript';
|
|
13
|
+
import {list} from './list.mjs';
|
|
14
|
+
|
|
15
|
+
const TYPE_FILE = fileURLToPath(
|
|
16
|
+
new URL('../upgrade.type.mjs', import.meta.url),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Property names a JSDoc `@typedef {object}` declares, parsed from the file.
|
|
21
|
+
* @param {string} typedefName
|
|
22
|
+
* @returns {string[]}
|
|
23
|
+
*/
|
|
24
|
+
function declaredProperties(typedefName) {
|
|
25
|
+
const source = ts.createSourceFile(
|
|
26
|
+
TYPE_FILE,
|
|
27
|
+
fs.readFileSync(TYPE_FILE, 'utf8'),
|
|
28
|
+
ts.ScriptTarget.Latest,
|
|
29
|
+
true,
|
|
30
|
+
ts.ScriptKind.JS,
|
|
31
|
+
);
|
|
32
|
+
/** @type {string[]} */
|
|
33
|
+
const names = [];
|
|
34
|
+
for (const statement of source.statements) {
|
|
35
|
+
for (const doc of /** @type {any} */ (statement).jsDoc ?? []) {
|
|
36
|
+
for (const tag of doc.tags ?? []) {
|
|
37
|
+
if (
|
|
38
|
+
!ts.isJSDocTypedefTag(tag) ||
|
|
39
|
+
tag.name?.getText(source) !== typedefName
|
|
40
|
+
) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const literal = tag.typeExpression;
|
|
44
|
+
if (literal && ts.isJSDocTypeLiteral(literal)) {
|
|
45
|
+
for (const property of literal.jsDocPropertyTags ?? []) {
|
|
46
|
+
names.push(property.name.getText(source));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return names.sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('upgrade.list entries', () => {
|
|
56
|
+
it('carry exactly the fields UpgradeListEntry declares', async () => {
|
|
57
|
+
const declared = declaredProperties('UpgradeListEntry');
|
|
58
|
+
expect(declared.length).toBeGreaterThan(0);
|
|
59
|
+
|
|
60
|
+
const {type, data} = await list();
|
|
61
|
+
expect(type).toBe('upgrade.list');
|
|
62
|
+
expect(data.length).toBeGreaterThan(0);
|
|
63
|
+
for (const entry of data) {
|
|
64
|
+
expect(Object.keys(entry).sort()).toEqual(declared);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('marks optional codemods with a boolean', async () => {
|
|
69
|
+
const {data} = await list();
|
|
70
|
+
for (const entry of data) expect(typeof entry.optional).toBe('boolean');
|
|
71
|
+
expect(data.some(entry => entry.optional)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -24,7 +24,7 @@ import {logger} from '../../logger.mjs';
|
|
|
24
24
|
* @returns {import('../upgrade.type.mjs').UpgradeStatusResponse}
|
|
25
25
|
*/
|
|
26
26
|
export function statusUpToDate({from, to, agentDocs, registryCompositions}) {
|
|
27
|
-
logger.log('
|
|
27
|
+
logger.log('[ok] Already up to date - no codemods to run.');
|
|
28
28
|
logger.log('Use --force to run codemods anyway.');
|
|
29
29
|
logger.log(registryCompositions?.ok === false ? 'Finished with unresolved registry items\n' : 'Done\n');
|
|
30
30
|
return {
|
|
@@ -45,7 +45,7 @@ export function statusUpToDate({from, to, agentDocs, registryCompositions}) {
|
|
|
45
45
|
* @returns {import('../upgrade.type.mjs').UpgradeStatusResponse}
|
|
46
46
|
*/
|
|
47
47
|
export function statusNoCodemods({from, to, agentDocs, registryCompositions}) {
|
|
48
|
-
logger.log('
|
|
48
|
+
logger.log('[ok] No codemods available for this version range.');
|
|
49
49
|
logger.log(registryCompositions?.ok === false ? 'Finished with unresolved registry items\n' : 'Done\n');
|
|
50
50
|
return {
|
|
51
51
|
type: 'upgrade.status',
|
|
@@ -80,7 +80,8 @@ export const doc = {
|
|
|
80
80
|
{
|
|
81
81
|
name: 'options.installDeps',
|
|
82
82
|
type: 'boolean',
|
|
83
|
-
description:
|
|
83
|
+
description:
|
|
84
|
+
'Install jscodeshift when it is missing; otherwise a missing jscodeshift throws ERR_DEP_MISSING.',
|
|
84
85
|
},
|
|
85
86
|
{
|
|
86
87
|
name: 'options.registry',
|
|
@@ -12,6 +12,10 @@ export type UpgradeListEntry = {
|
|
|
12
12
|
name: string;
|
|
13
13
|
title: string;
|
|
14
14
|
version: string;
|
|
15
|
+
/**
|
|
16
|
+
* True when the codemod runs only if named with `--codemod`.
|
|
17
|
+
*/
|
|
18
|
+
optional: boolean;
|
|
15
19
|
};
|
|
16
20
|
/**
|
|
17
21
|
* State of the managed agent-docs block (`<!-- ASTRYX:START --> … END -->`)
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* `p.log.step(...)`, `p.log.success(...)`, etc. All output routes through
|
|
12
12
|
* `humanLog`, the CLI's stdout-discipline primitive, which is a no-op in
|
|
13
13
|
* `--json` mode — so these human logs can never corrupt a JSON envelope.
|
|
14
|
+
* Every line is plain ASCII: level prefixes are ASCII and typographic glyphs
|
|
15
|
+
* in a message are transliterated before it is printed.
|
|
14
16
|
*
|
|
15
17
|
* The side-effecting API commands (upgrade/init/themeBuild) do NOT use this;
|
|
16
18
|
* they write through the shared `logger` (api/logger.mjs).
|
|
@@ -18,8 +20,30 @@
|
|
|
18
20
|
|
|
19
21
|
import {humanLog} from '../../foundation/response/json.mjs';
|
|
20
22
|
|
|
21
|
-
/** @
|
|
22
|
-
const
|
|
23
|
+
/** @type {Array<[RegExp, string]>} */
|
|
24
|
+
const ASCII_GLYPHS = [
|
|
25
|
+
[/[\u2014\u2013]/g, '-'],
|
|
26
|
+
[/\u2192/g, '->'],
|
|
27
|
+
[/\u2713/g, '[ok]'],
|
|
28
|
+
[/\u2717/g, '!!'],
|
|
29
|
+
[/\u26a0\ufe0f?/g, '!'],
|
|
30
|
+
[/\u2022/g, '-'],
|
|
31
|
+
[/\u2026/g, '...'],
|
|
32
|
+
[/[\u2018\u2019]/g, "'"],
|
|
33
|
+
[/[\u201c\u201d]/g, '"'],
|
|
34
|
+
[/\u00a0/g, ' '],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A message as plain ASCII: typographic glyphs become their ASCII spelling.
|
|
39
|
+
* @param {unknown} msg
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function toAscii(msg) {
|
|
43
|
+
let text = msg === undefined || msg === null ? '' : String(msg);
|
|
44
|
+
for (const [glyph, ascii] of ASCII_GLYPHS) text = text.replace(glyph, ascii);
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
23
47
|
|
|
24
48
|
/**
|
|
25
49
|
* Human-facing log surface (the small `log` API codemods use). All lines go to
|
|
@@ -28,15 +52,15 @@ const toStr = (msg) => (msg === undefined || msg === null ? '' : String(msg));
|
|
|
28
52
|
*/
|
|
29
53
|
export const log = {
|
|
30
54
|
/** @param {unknown} msg */
|
|
31
|
-
message: (msg) => humanLog(
|
|
55
|
+
message: (msg) => humanLog(toAscii(msg)),
|
|
32
56
|
/** @param {unknown} msg */
|
|
33
|
-
info: (msg) => humanLog(
|
|
57
|
+
info: (msg) => humanLog(toAscii(msg)),
|
|
34
58
|
/** @param {unknown} msg */
|
|
35
|
-
step: (msg) => humanLog(
|
|
59
|
+
step: (msg) => humanLog(toAscii(msg)),
|
|
36
60
|
/** @param {unknown} msg */
|
|
37
|
-
success: (msg) => humanLog(
|
|
61
|
+
success: (msg) => humanLog(`[ok] ${toAscii(msg)}`),
|
|
38
62
|
/** @param {unknown} msg */
|
|
39
|
-
warn: (msg) => humanLog(
|
|
63
|
+
warn: (msg) => humanLog(`! ${toAscii(msg)}`),
|
|
40
64
|
/** @param {unknown} msg */
|
|
41
|
-
error: (msg) => humanLog(
|
|
65
|
+
error: (msg) => humanLog(`!! ${toAscii(msg)}`),
|
|
42
66
|
};
|
|
@@ -51,7 +51,7 @@ describe('sanity: helpers are live in human mode', () => {
|
|
|
51
51
|
setJsonMode(false);
|
|
52
52
|
p.log.success('done');
|
|
53
53
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
54
|
-
expect(logSpy).toHaveBeenCalledWith('
|
|
54
|
+
expect(logSpy).toHaveBeenCalledWith('[ok] done');
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('every log helper emits exactly one stdout line in human mode', () => {
|
|
@@ -60,3 +60,21 @@ describe('sanity: helpers are live in human mode', () => {
|
|
|
60
60
|
expect(logSpy).toHaveBeenCalledTimes(6);
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
describe('ASCII output', () => {
|
|
65
|
+
it('prints ASCII level prefixes and transliterates glyphs in messages', () => {
|
|
66
|
+
setJsonMode(false);
|
|
67
|
+
p.log.success('src/a.tsx');
|
|
68
|
+
p.log.warn('stale \u2014 refresh');
|
|
69
|
+
p.log.error(' \u2717 src/b.tsx \u2014 rename \u2192 failed');
|
|
70
|
+
p.log.info(' \u2022 name \u2014 title\u2026');
|
|
71
|
+
const lines = logSpy.mock.calls.map(call => call.join(' '));
|
|
72
|
+
expect(lines).toEqual([
|
|
73
|
+
'[ok] src/a.tsx',
|
|
74
|
+
'! stale - refresh',
|
|
75
|
+
'!! !! src/b.tsx - rename -> failed',
|
|
76
|
+
' - name - title...',
|
|
77
|
+
]);
|
|
78
|
+
for (const line of lines) expect(line).not.toMatch(/[\u0080-\uFFFF]/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find imported components, resolve their local JSX names (including aliases),
|
|
5
|
+
* and yield matching JSX prop paths to a visitor.
|
|
6
|
+
*
|
|
7
|
+
* This helper intentionally stops at the JSX boundary. It does not follow
|
|
8
|
+
* variables, helper calls, object spreads, wrapper components, or types.
|
|
9
|
+
*
|
|
10
|
+
* @param {any} root jscodeshift collection for one source file
|
|
11
|
+
* @param {any} j configured jscodeshift instance
|
|
12
|
+
* @param {{
|
|
13
|
+
* matchesImport: (source: string) => boolean,
|
|
14
|
+
* components: ReadonlyMap<string, ReadonlySet<string>>,
|
|
15
|
+
* normalizeComponentName?: (name: string) => string,
|
|
16
|
+
* }} options
|
|
17
|
+
* @param {(propPath: any, context: {component: string, localName: string}) => void} visit
|
|
18
|
+
*/
|
|
19
|
+
export function transformProp(root, j, options, visit) {
|
|
20
|
+
const normalize = options.normalizeComponentName ?? (name => name);
|
|
21
|
+
/** @type {Map<string, {component: string, props: ReadonlySet<string>}>} */
|
|
22
|
+
const directBindings = new Map();
|
|
23
|
+
/** @type {Set<string>} */
|
|
24
|
+
const namespaceBindings = new Set();
|
|
25
|
+
|
|
26
|
+
root.find(j.ImportDeclaration).forEach((/** @type {any} */ importPath) => {
|
|
27
|
+
const source = importPath.node.source.value;
|
|
28
|
+
if (
|
|
29
|
+
importPath.node.importKind === 'type' ||
|
|
30
|
+
typeof source !== 'string' ||
|
|
31
|
+
!options.matchesImport(source)
|
|
32
|
+
) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const subpathName = normalize(source.split('/').at(-1) ?? '');
|
|
37
|
+
const subpathProps = options.components.get(subpathName);
|
|
38
|
+
|
|
39
|
+
for (const specifier of importPath.node.specifiers ?? []) {
|
|
40
|
+
if (specifier.importKind === 'type') continue;
|
|
41
|
+
|
|
42
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
43
|
+
const importedName =
|
|
44
|
+
specifier.imported?.name ?? specifier.imported?.value;
|
|
45
|
+
if (typeof importedName !== 'string') continue;
|
|
46
|
+
const component = normalize(importedName);
|
|
47
|
+
const props = options.components.get(component);
|
|
48
|
+
if (!props) continue;
|
|
49
|
+
directBindings.set(specifier.local?.name ?? importedName, {
|
|
50
|
+
component,
|
|
51
|
+
props,
|
|
52
|
+
});
|
|
53
|
+
} else if (specifier.type === 'ImportDefaultSpecifier' && subpathProps) {
|
|
54
|
+
directBindings.set(specifier.local.name, {
|
|
55
|
+
component: subpathName,
|
|
56
|
+
props: subpathProps,
|
|
57
|
+
});
|
|
58
|
+
} else if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
59
|
+
namespaceBindings.add(specifier.local.name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (directBindings.size === 0 && namespaceBindings.size === 0) return;
|
|
65
|
+
|
|
66
|
+
root.find(j.JSXOpeningElement).forEach((/** @type {any} */ jsxPath) => {
|
|
67
|
+
const name = jsxPath.node.name;
|
|
68
|
+
let localName;
|
|
69
|
+
let component;
|
|
70
|
+
let props;
|
|
71
|
+
|
|
72
|
+
if (name.type === 'JSXIdentifier') {
|
|
73
|
+
localName = name.name;
|
|
74
|
+
const binding = directBindings.get(localName);
|
|
75
|
+
if (!binding) return;
|
|
76
|
+
({component, props} = binding);
|
|
77
|
+
} else if (
|
|
78
|
+
name.type === 'JSXMemberExpression' &&
|
|
79
|
+
name.object?.type === 'JSXIdentifier' &&
|
|
80
|
+
name.property?.type === 'JSXIdentifier' &&
|
|
81
|
+
namespaceBindings.has(name.object.name)
|
|
82
|
+
) {
|
|
83
|
+
localName = name.object.name;
|
|
84
|
+
component = normalize(name.property.name);
|
|
85
|
+
props = options.components.get(component);
|
|
86
|
+
if (!props) return;
|
|
87
|
+
} else {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// A parameter or local declaration with the imported name shadows the
|
|
92
|
+
// module binding. Only JSX tags whose root binding resolves to Program are
|
|
93
|
+
// considered usages of the import discovered above.
|
|
94
|
+
const bindingScope = jsxPath.scope.lookup(localName);
|
|
95
|
+
if (!bindingScope || bindingScope.path.node.type !== 'Program') return;
|
|
96
|
+
|
|
97
|
+
for (let index = 0; index < jsxPath.node.attributes.length; index++) {
|
|
98
|
+
const attribute = jsxPath.node.attributes[index];
|
|
99
|
+
if (
|
|
100
|
+
attribute.type !== 'JSXAttribute' ||
|
|
101
|
+
attribute.name?.type !== 'JSXIdentifier' ||
|
|
102
|
+
!props.has(attribute.name.name)
|
|
103
|
+
) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
visit(jsxPath.get('attributes', index), {component, localName});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import jscodeshift from 'jscodeshift';
|
|
5
|
+
import {transformProp} from './transform-prop.mjs';
|
|
6
|
+
|
|
7
|
+
const j = jscodeshift.withParser('tsx');
|
|
8
|
+
const components = new Map([
|
|
9
|
+
['Widget', new Set(['tone'])],
|
|
10
|
+
['Group', new Set(['size'])],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function apply(source) {
|
|
14
|
+
const root = j(source);
|
|
15
|
+
transformProp(
|
|
16
|
+
root,
|
|
17
|
+
j,
|
|
18
|
+
{
|
|
19
|
+
matchesImport: value => /^@example\/core(?:\/|$)/.test(value),
|
|
20
|
+
components,
|
|
21
|
+
normalizeComponentName: name =>
|
|
22
|
+
name.startsWith('XDS') ? name.slice(3) : name,
|
|
23
|
+
},
|
|
24
|
+
propPath => {
|
|
25
|
+
propPath.node.name.name = `migrated${propPath.node.name.name}`;
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
return root.toSource({quote: 'single'});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('transformProp', () => {
|
|
32
|
+
it('maps named import aliases to their JSX usages', () => {
|
|
33
|
+
const output = apply(`import {Widget as Card} from '@example/core';
|
|
34
|
+
const view = <Card tone="old" other="old" />;`);
|
|
35
|
+
|
|
36
|
+
expect(output).toContain('<Card migratedtone="old" other="old"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('maps default subpath imports', () => {
|
|
40
|
+
const output = apply(`import Card from '@example/core/Widget';
|
|
41
|
+
const view = <Card tone="old" />;`);
|
|
42
|
+
|
|
43
|
+
expect(output).toContain('<Card migratedtone="old"');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('ignores declaration-level type imports', () => {
|
|
47
|
+
const output = apply(`import type {Widget} from '@example/core';
|
|
48
|
+
const Widget = props => <div {...props} />;
|
|
49
|
+
const view = <Widget tone="old" />;`);
|
|
50
|
+
|
|
51
|
+
expect(output).toContain('<Widget tone="old"');
|
|
52
|
+
expect(output).not.toContain('migratedtone');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('ignores type specifiers in mixed imports', () => {
|
|
56
|
+
const output = apply(`import {type Widget, Group} from '@example/core';
|
|
57
|
+
const Widget = props => <div {...props} />;
|
|
58
|
+
const view = <><Widget tone="old" /><Group size="old" /></>;`);
|
|
59
|
+
|
|
60
|
+
expect(output).toContain('<Widget tone="old"');
|
|
61
|
+
expect(output).toContain('<Group migratedsize="old"');
|
|
62
|
+
expect(output).not.toContain('<Widget migratedtone');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('maps namespace imports', () => {
|
|
66
|
+
const output = apply(`import * as Core from '@example/core';
|
|
67
|
+
const view = <Core.Group size="old" />;`);
|
|
68
|
+
|
|
69
|
+
expect(output).toContain('<Core.Group migratedsize="old"');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('ignores same-named imports from other packages', () => {
|
|
73
|
+
const source = `import {Widget} from '@other/core';
|
|
74
|
+
const view = <Widget tone="old" />;`;
|
|
75
|
+
expect(apply(source)).toBe(source);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('ignores a local binding that shadows an imported alias', () => {
|
|
79
|
+
const output = apply(`import {Widget as Card} from '@example/core';
|
|
80
|
+
const migrated = <Card tone="old" />;
|
|
81
|
+
function Local(Card) { return <Card tone="local" />; }`);
|
|
82
|
+
|
|
83
|
+
expect(output).toContain('<Card migratedtone="old"');
|
|
84
|
+
expect(output).toContain('<Card tone="local"');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('yields only configured props on configured components', () => {
|
|
88
|
+
const output = apply(`import {Widget, Group} from '@example/core';
|
|
89
|
+
const a = <Widget size="old" tone="old" />;
|
|
90
|
+
const b = <Group tone="old" size="old" />;`);
|
|
91
|
+
|
|
92
|
+
expect(output).toContain('<Widget size="old" migratedtone="old"');
|
|
93
|
+
expect(output).toContain('<Group tone="old" migratedsize="old"');
|
|
94
|
+
});
|
|
95
|
+
});
|