@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.dea6813

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.
Files changed (183) hide show
  1. package/README.md +32 -18
  2. package/api/build/build.doc.mjs +3 -3
  3. package/api/build/build.test.mjs +14 -3
  4. package/api/build/build.type.d.mts +35 -1
  5. package/api/build/build.type.mjs +24 -3
  6. package/api/build/help/help.d.mts +8 -5
  7. package/api/build/help/help.mjs +56 -6
  8. package/api/component/component.doc.mjs +11 -2
  9. package/api/component/component.mjs +29 -0
  10. package/api/component/component.test.mjs +38 -0
  11. package/api/component/component.type.d.mts +11 -1
  12. package/api/component/component.type.mjs +8 -1
  13. package/api/discover/discover.doc.mjs +1 -1
  14. package/api/gap-report/gap-report.doc.mjs +7 -4
  15. package/api/init/init.doc.mjs +4 -0
  16. package/api/init/init.test.mjs +41 -1
  17. package/api/init/remove/remove.mjs +1 -1
  18. package/api/init/run/run.mjs +20 -10
  19. package/api/integration/add-contribution.component-names.test.mjs +120 -0
  20. package/api/integration/add-contribution.mjs +11 -0
  21. package/api/integration/add-contribution.test.mjs +80 -0
  22. package/api/integration/integrationAdd.doc.mjs +7 -4
  23. package/api/integration/integrationAddComponent.doc.mjs +1 -1
  24. package/api/integration/validate-integration.mjs +2 -0
  25. package/api/integration/validate-integration.test.mjs +91 -0
  26. package/api/integration/validateIntegration.doc.mjs +1 -1
  27. package/api/json/envelope-types.test.mjs +76 -0
  28. package/api/json/parseResponse.doc.mjs +2 -2
  29. package/api/layout/expand/expand.mjs +7 -5
  30. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  31. package/api/layout/grammar/grammar.mjs +2 -1
  32. package/api/layout/layoutExpand.doc.mjs +1 -1
  33. package/api/search/search-return-type.test.mjs +54 -0
  34. package/api/search/search.d.mts +2 -9
  35. package/api/search/search.doc.mjs +5 -1
  36. package/api/search/search.mjs +10 -3
  37. package/api/search/search.test.mjs +34 -0
  38. package/api/swizzle/copy/copy.mjs +28 -11
  39. package/api/swizzle/swizzle.doc.mjs +1 -1
  40. package/api/template/copy/copy.mjs +17 -23
  41. package/api/template/copy/copy.test.mjs +17 -0
  42. package/api/template/template.doc.mjs +2 -1
  43. package/api/theme/add/add.binary.test.mjs +91 -0
  44. package/api/theme/add/add.mjs +20 -3
  45. package/api/theme/add/add.staging.test.mjs +66 -0
  46. package/api/theme/build/build.mjs +114 -37
  47. package/api/theme/build/build.public-component-vars.test.mjs +1 -1
  48. package/api/theme/build/build.receipt-doc.test.mjs +111 -0
  49. package/api/theme/build/font-warning.mjs +3 -3
  50. package/api/theme/build/font-warning.test.mjs +5 -2
  51. package/api/theme/palette/generate/generate.mjs +7 -2
  52. package/api/theme/palette/generate/generate.test.mjs +96 -0
  53. package/api/theme/palette/generate/generator.mjs +8 -1
  54. package/api/theme/palette/generate/generator.test.mjs +10 -0
  55. package/api/theme/template/template.mjs +11 -2
  56. package/api/theme/template/template.test.mjs +15 -0
  57. package/api/theme/themeBuild.doc.mjs +7 -4
  58. package/api/theme/themeTemplate.doc.mjs +4 -1
  59. package/api/upgrade/_adapter.mjs +27 -4
  60. package/api/upgrade/list/list.mjs +2 -1
  61. package/api/upgrade/list/list.test.mjs +73 -0
  62. package/api/upgrade/status/status.mjs +2 -2
  63. package/api/upgrade/upgrade.doc.mjs +2 -1
  64. package/api/upgrade/upgrade.type.d.mts +4 -0
  65. package/api/upgrade/upgrade.type.mjs +1 -0
  66. package/assets/codemods/term-log.mjs +32 -8
  67. package/assets/codemods/term-log.test.mjs +19 -1
  68. package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
  69. package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
  70. package/assets/templates/pages/table-tree/page.tsx +1704 -0
  71. package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
  72. package/authoring/config/config.doc.mjs +1 -1
  73. package/authoring/config/debug-composition.test.mjs +92 -0
  74. package/authoring/config/type.ts +15 -3
  75. package/clients/cli/command-load-failure.test.mjs +83 -0
  76. package/clients/cli/commands/blog.mjs +23 -8
  77. package/clients/cli/commands/blog.test.mjs +42 -1
  78. package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
  79. package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
  80. package/clients/cli/commands/build-theme.mjs +5 -5
  81. package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
  82. package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
  83. package/clients/cli/commands/build.doc.mjs +4 -1
  84. package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
  85. package/clients/cli/commands/build.mjs +32 -47
  86. package/clients/cli/commands/build.playbook.test.mjs +75 -0
  87. package/clients/cli/commands/build.text-fields.test.mjs +40 -0
  88. package/clients/cli/commands/component/index.mjs +2 -7
  89. package/clients/cli/commands/component-package.test.mjs +18 -0
  90. package/clients/cli/commands/component-resolution.test.mjs +21 -0
  91. package/clients/cli/commands/component.test.mjs +19 -0
  92. package/clients/cli/commands/detail-levels.test.mjs +2 -2
  93. package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
  94. package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
  95. package/clients/cli/commands/discover.doc.mjs +4 -2
  96. package/clients/cli/commands/discover.mjs +3 -3
  97. package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
  98. package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
  99. package/clients/cli/commands/doctor.mjs +6 -16
  100. package/clients/cli/commands/doctor.test.mjs +42 -0
  101. package/clients/cli/commands/gap-report.doc.mjs +16 -5
  102. package/clients/cli/commands/gap-report.test.mjs +72 -0
  103. package/clients/cli/commands/hook/index.mjs +7 -17
  104. package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
  105. package/clients/cli/commands/init.doc.mjs +19 -8
  106. package/clients/cli/commands/init.flag-help.test.mjs +153 -0
  107. package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
  108. package/clients/cli/commands/integration-add.doc.mjs +21 -5
  109. package/clients/cli/commands/interactive-guard.test.mjs +101 -24
  110. package/clients/cli/commands/json-contract.test.mjs +33 -0
  111. package/clients/cli/commands/layout-check.doc.mjs +14 -3
  112. package/clients/cli/commands/layout-expand.doc.mjs +21 -4
  113. package/clients/cli/commands/layout.doc.mjs +2 -2
  114. package/clients/cli/commands/layout.mjs +21 -9
  115. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  116. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  117. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  118. package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
  119. package/clients/cli/commands/search.doc.mjs +6 -3
  120. package/clients/cli/commands/search.mjs +17 -7
  121. package/clients/cli/commands/search.test.mjs +75 -0
  122. package/clients/cli/commands/swizzle.doc.mjs +2 -1
  123. package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
  124. package/clients/cli/commands/template.doc.mjs +28 -6
  125. package/clients/cli/commands/template.flag-help.test.mjs +117 -0
  126. package/clients/cli/commands/template.path-help.test.mjs +40 -0
  127. package/clients/cli/commands/theme-add.doc.mjs +2 -1
  128. package/clients/cli/commands/theme-build.doc.mjs +7 -6
  129. package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
  130. package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
  131. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  132. package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
  133. package/clients/cli/commands/upgrade.doc.mjs +17 -6
  134. package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
  135. package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
  136. package/clients/cli/index.mjs +13 -30
  137. package/clients/cli/latest-version-env.test.mjs +50 -0
  138. package/clients/cli/lib/cli-error.test.mjs +7 -0
  139. package/clients/cli/lib/component-format.mjs +9 -9
  140. package/clients/cli/lib/component-format.test.mjs +1 -1
  141. package/clients/cli/lib/define-command.mjs +32 -6
  142. package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
  143. package/clients/cli/lib/exit-codes.test.mjs +97 -0
  144. package/clients/cli/lib/hook-format.mjs +5 -5
  145. package/clients/cli/lib/json-shim.mjs +38 -2
  146. package/clients/cli/lib/json-shim.test.mjs +83 -0
  147. package/clients/cli/lib/manifest.d.ts +2 -0
  148. package/clients/cli/lib/manifest.mjs +22 -0
  149. package/clients/cli/lib/manifest.test.mjs +17 -0
  150. package/foundation/agent-docs/agent-docs.d.mts +4 -0
  151. package/foundation/agent-docs/agent-docs.mjs +69 -3
  152. package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
  153. package/foundation/config/integration-debug.test.mjs +28 -3
  154. package/foundation/config/project.mjs +123 -9
  155. package/foundation/config/project.test.mjs +126 -0
  156. package/foundation/discovery/template-adapter.d.mts +14 -5
  157. package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
  158. package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
  159. package/foundation/discovery/template-adapter.mjs +292 -25
  160. package/foundation/discovery/theming-targets.test.mjs +4 -0
  161. package/foundation/fs/module-loader.d.mts +1 -0
  162. package/foundation/fs/module-loader.mjs +50 -1
  163. package/foundation/fs/module-loader.stdout.test.mjs +332 -0
  164. package/foundation/fs/path-safety.d.mts +3 -2
  165. package/foundation/fs/path-safety.mjs +49 -19
  166. package/foundation/fs/path-safety.test.mjs +50 -0
  167. package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
  168. package/foundation/integrations/autolink.mjs +6 -1
  169. package/foundation/response/base.d.ts +8 -4
  170. package/foundation/response/error-codes.doc.mjs +1 -2
  171. package/foundation/response/error-codes.mjs +1 -1
  172. package/foundation/response/error-codes.test.mjs +83 -0
  173. package/foundation/response/json-contract.test.mjs +11 -0
  174. package/foundation/response/json.d.mts +4 -2
  175. package/foundation/response/json.mjs +8 -10
  176. package/foundation/response/response-types.doc.mjs +3 -3
  177. package/foundation/xle/expand.d.mts +2 -0
  178. package/foundation/xle/expand.mjs +3 -2
  179. package/foundation/xle/expand.test.mjs +54 -0
  180. package/package.json +9 -9
  181. package/clients/cli/lib/update-check.mjs +0 -83
  182. package/clients/cli/lib/update-check.test.mjs +0 -137
  183. 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, token- and component-override counts, output size in KB, the written outputs {css, js, dts, and variantsDts when custom prop values were augmented}, and validation warnings—including exact canonical replacements for deprecated component target keys. Resolves to null instead when the theme produced no CSS (nothing to build).',
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
- {code: 'ERR_THEME_INVALID', when: 'the resolved theme has no name'},
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: [{code: 'ERR_PATH_TRAVERSAL', when: 'the target path escapes cwd'}],
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
  {
@@ -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 hook.buildCommand({packageDir, files});
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(`✓ Post-codemod hook ${label} completed.`);
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 — AI agents have no component index. Run \`${formatCliCommand('astryx init --features agents')}\` to install it.`,
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(`✓ Agent docs refreshed → ${written.join(', ')}`);
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} — ${title}${optional ? ' (optional)' : ''} (${pr})`);
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('✓ Already up to date — no codemods to run.');
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('✓ No codemods available for this version range.');
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: 'Auto-install jscodeshift without prompting.',
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 -->`)
@@ -25,6 +25,7 @@
25
25
  * @property {string} name
26
26
  * @property {string} title
27
27
  * @property {string} version
28
+ * @property {boolean} optional True when the codemod runs only if named with `--codemod`.
28
29
  */
29
30
 
30
31
  /**
@@ -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
- /** @param {unknown} msg */
22
- const toStr = (msg) => (msg === undefined || msg === null ? '' : String(msg));
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(toStr(msg)),
55
+ message: (msg) => humanLog(toAscii(msg)),
32
56
  /** @param {unknown} msg */
33
- info: (msg) => humanLog(toStr(msg)),
57
+ info: (msg) => humanLog(toAscii(msg)),
34
58
  /** @param {unknown} msg */
35
- step: (msg) => humanLog(toStr(msg)),
59
+ step: (msg) => humanLog(toAscii(msg)),
36
60
  /** @param {unknown} msg */
37
- success: (msg) => humanLog(`✓ ${toStr(msg)}`),
61
+ success: (msg) => humanLog(`[ok] ${toAscii(msg)}`),
38
62
  /** @param {unknown} msg */
39
- warn: (msg) => humanLog(`⚠ ${toStr(msg)}`),
63
+ warn: (msg) => humanLog(`! ${toAscii(msg)}`),
40
64
  /** @param {unknown} msg */
41
- error: (msg) => humanLog(`✗ ${toStr(msg)}`),
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('✓ done');
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
+ });
@@ -220,4 +220,51 @@ function wrapper(useResizable) {
220
220
  const output = await apply('rename-resizable-pixel-bounds', input);
221
221
  expect(output).toBe(input);
222
222
  });
223
+
224
+ it('renames bounds when useResizable comes from a namespace import', async () => {
225
+ const input = `import * as Astryx from '@astryxdesign/core';
226
+ const region = Astryx.useResizable({defaultSize: 240, minSizePx: 120, maxSizePx: 480});`;
227
+ const output = await apply('rename-resizable-pixel-bounds', input);
228
+ expect(output).toContain('minSize: 120');
229
+ expect(output).toContain('maxSize: 480');
230
+ expect(output).not.toContain('minSizePx');
231
+ expect(output).not.toContain('maxSizePx');
232
+ });
233
+
234
+ it('leaves a namespace that does not come from Astryx alone', async () => {
235
+ const input = `import * as Other from 'other-resizable';
236
+ const region = Other.useResizable({minSizePx: 120});`;
237
+ const output = await apply('rename-resizable-pixel-bounds', input);
238
+ expect(output).toContain('minSizePx: 120');
239
+ });
240
+
241
+ it('does not rewrite a parameter that shadows the namespace import', async () => {
242
+ const input = `import * as Astryx from '@astryxdesign/core';
243
+ function build(Astryx) {
244
+ return Astryx.useResizable({minSizePx: 120});
245
+ }`;
246
+ const output = await apply('rename-resizable-pixel-bounds', input);
247
+ expect(output).toContain('minSizePx: 120');
248
+ });
249
+
250
+ it.each([
251
+ ['as const', '{defaultSize: 240, minSizePx: 120} as const'],
252
+ ['satisfies', '{defaultSize: 240, minSizePx: 120} satisfies object'],
253
+ ])('renames a static configuration wrapped in %s', async (_label, config) => {
254
+ const input = `import {useResizable} from '@astryxdesign/core/Resizable';
255
+ const region = useResizable(${config});`;
256
+ const output = await apply('rename-resizable-pixel-bounds', input);
257
+ expect(output).toContain('minSize: 120');
258
+ expect(output).not.toContain('minSizePx');
259
+ });
260
+
261
+ it('renames wrapped multi-region configurations', async () => {
262
+ const input = `import {useResizable} from '@astryxdesign/core';
263
+ const regions = useResizable({regions: {
264
+ nav: {defaultSize: 240, minSizePx: 120} as const,
265
+ } as const});`;
266
+ const output = await apply('rename-resizable-pixel-bounds', input);
267
+ expect(output).toContain('minSize: 120');
268
+ expect(output).not.toContain('minSizePx');
269
+ });
223
270
  });
@@ -8,6 +8,9 @@
8
8
  * released conflict rule (the unified property wins when both are present).
9
9
  * A spread or computed property can change precedence invisibly, so dynamic
10
10
  * objects stay unchanged and receive a manual-migration TODO instead.
11
+ * Named and namespace imports both count, and a type-only wrapper (`as`,
12
+ * `satisfies`, a non-null assertion, parentheses) does not make a
13
+ * configuration dynamic.
11
14
  */
12
15
 
13
16
  export const meta = {
@@ -33,6 +36,26 @@ const MANUAL_MIGRATION_COMMENT =
33
36
  'or computed property. Rename minSizePx/maxSizePx to minSize/maxSize ' +
34
37
  'manually without changing property precedence. ';
35
38
 
39
+ const TYPE_WRAPPERS = new Set([
40
+ 'TSAsExpression',
41
+ 'TSSatisfiesExpression',
42
+ 'TSTypeAssertion',
43
+ 'TSNonNullExpression',
44
+ 'ParenthesizedExpression',
45
+ ]);
46
+
47
+ /**
48
+ * The expression under type-only wrappers such as `{...} as const`.
49
+ * @param {any} node
50
+ */
51
+ function unwrap(node) {
52
+ let current = node;
53
+ while (current && TYPE_WRAPPERS.has(current.type)) {
54
+ current = current.expression;
55
+ }
56
+ return current;
57
+ }
58
+
36
59
  /** @param {any} property */
37
60
  function staticPropertyName(property) {
38
61
  if (property.computed) return null;
@@ -128,6 +151,8 @@ export default function transformer(file, api) {
128
151
  const root = j(file.source);
129
152
  /** @type {Map<string, any>} */
130
153
  const bindings = new Map();
154
+ /** @type {Map<string, any>} */
155
+ const namespaces = new Map();
131
156
 
132
157
  root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
133
158
  if (!IMPORT_SOURCES.has(path.node.source.value)) return;
@@ -138,17 +163,40 @@ export default function transformer(file, api) {
138
163
  ) {
139
164
  const local = specifier.local?.name ?? 'useResizable';
140
165
  bindings.set(local, path.scope.lookup(local) ?? path.scope);
166
+ } else if (specifier.type === 'ImportNamespaceSpecifier') {
167
+ const local = specifier.local.name;
168
+ namespaces.set(local, path.scope.lookup(local) ?? path.scope);
141
169
  }
142
170
  }
143
171
  });
144
- if (bindings.size === 0) return undefined;
172
+ if (bindings.size === 0 && namespaces.size === 0) return undefined;
173
+
174
+ /**
175
+ * @param {any} callee
176
+ * @param {any} scope
177
+ */
178
+ const isUseResizable = (callee, scope) => {
179
+ if (callee.type === 'Identifier') {
180
+ return (
181
+ bindings.has(callee.name) &&
182
+ scope.lookup(callee.name) === bindings.get(callee.name)
183
+ );
184
+ }
185
+ return (
186
+ callee.type === 'MemberExpression' &&
187
+ !callee.computed &&
188
+ callee.object.type === 'Identifier' &&
189
+ callee.property.type === 'Identifier' &&
190
+ callee.property.name === 'useResizable' &&
191
+ namespaces.has(callee.object.name) &&
192
+ scope.lookup(callee.object.name) === namespaces.get(callee.object.name)
193
+ );
194
+ };
145
195
 
146
196
  let changed = false;
147
197
  root.find(j.CallExpression).forEach((/** @type {any} */ path) => {
148
- const callee = path.node.callee;
149
- if (callee.type !== 'Identifier' || !bindings.has(callee.name)) return;
150
- if (path.scope.lookup(callee.name) !== bindings.get(callee.name)) return;
151
- const config = path.node.arguments[0];
198
+ if (!isUseResizable(path.node.callee, path.scope)) return;
199
+ const config = unwrap(path.node.arguments[0]);
152
200
  if (config?.type !== 'ObjectExpression') return;
153
201
 
154
202
  changed = migrateObject(j, config) || changed;
@@ -158,13 +206,12 @@ export default function transformer(file, api) {
158
206
  staticPropertyName(property) === 'regions',
159
207
  );
160
208
  if (!regions || !('value' in regions)) return;
161
- const regionsObject = regions.value;
209
+ const regionsObject = unwrap(regions.value);
162
210
  if (regionsObject?.type !== 'ObjectExpression') return;
163
211
  for (const region of regionsObject.properties) {
164
- if (!('value' in region) || region.value?.type !== 'ObjectExpression') {
165
- continue;
166
- }
167
- changed = migrateObject(j, region.value) || changed;
212
+ const regionConfig = 'value' in region ? unwrap(region.value) : null;
213
+ if (regionConfig?.type !== 'ObjectExpression') continue;
214
+ changed = migrateObject(j, regionConfig) || changed;
168
215
  }
169
216
  });
170
217