@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.ddb63c3

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 (236) hide show
  1. package/README.md +20 -1
  2. package/api/blog/blog.doc.mjs +1 -0
  3. package/api/build/build.doc.mjs +1 -0
  4. package/api/component/component.doc.mjs +1 -0
  5. package/api/discover/discover.doc.mjs +1 -0
  6. package/api/docs/_adapter.d.mts +3 -8
  7. package/api/docs/_adapter.mjs +13 -105
  8. package/api/docs/docs.doc.mjs +1 -0
  9. package/api/docs/list/list.mjs +3 -7
  10. package/api/doctor/doctor.d.mts +25 -3
  11. package/api/doctor/doctor.doc.mjs +1 -0
  12. package/api/doctor/doctor.mjs +185 -13
  13. package/api/doctor/doctor.test.mjs +326 -1
  14. package/api/gap-report/gap-report.doc.mjs +1 -0
  15. package/api/hook/_adapter.mjs +19 -5
  16. package/api/hook/hook.doc.mjs +1 -0
  17. package/api/hook/list/list.d.mts +1 -1
  18. package/api/hook/list/list.mjs +69 -17
  19. package/api/init/init.doc.mjs +1 -0
  20. package/api/integration/add-contribution.mjs +7 -7
  21. package/api/integration/add-contribution.test.mjs +37 -3
  22. package/api/integration/add-theme.mjs +34 -64
  23. package/api/integration/add-theme.test.mjs +105 -21
  24. package/api/integration/authoring-checks.test.mjs +8 -4
  25. package/api/integration/integration-block-exports.test.mjs +10 -6
  26. package/api/integration/integrationAdd.doc.mjs +1 -0
  27. package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
  28. package/api/integration/integrationAddCodemod.doc.mjs +1 -0
  29. package/api/integration/integrationAddComponent.doc.mjs +1 -0
  30. package/api/integration/integrationAddDoc.doc.mjs +1 -0
  31. package/api/integration/integrationAddTemplate.doc.mjs +1 -0
  32. package/api/integration/integrationAddTheme.doc.mjs +6 -5
  33. package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
  34. package/api/integration/integrationDocConflicts.doc.mjs +1 -0
  35. package/api/integration/integrationPackCheck.doc.mjs +1 -0
  36. package/api/integration/integrationTemplateConflicts.doc.mjs +1 -0
  37. package/api/integration/pack-check.test.mjs +17 -47
  38. package/api/integration/summarizeIssues.doc.mjs +1 -0
  39. package/api/integration/validate-integration-fixes.test.mjs +1389 -0
  40. package/api/integration/validate-integration.mjs +50 -102
  41. package/api/integration/validate-integration.test.mjs +85 -23
  42. package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
  43. package/api/integration/validateIntegration.doc.mjs +1 -0
  44. package/api/json/assertResponse.doc.mjs +1 -0
  45. package/api/json/isError.doc.mjs +1 -0
  46. package/api/json/parseResponse.doc.mjs +1 -0
  47. package/api/layout/layoutCheck.doc.mjs +1 -0
  48. package/api/layout/layoutExpand.doc.mjs +1 -0
  49. package/api/layout/layoutGrammar.doc.mjs +1 -0
  50. package/api/search/search.doc.mjs +1 -0
  51. package/api/search/search.mjs +16 -6
  52. package/api/swizzle/swizzle.doc.mjs +1 -0
  53. package/api/template/template-suffix.test.mjs +41 -21
  54. package/api/template/template.doc.mjs +1 -0
  55. package/api/theme/_adapter.d.mts +2 -3
  56. package/api/theme/_adapter.mjs +4 -5
  57. package/api/theme/add/add.binary.test.mjs +10 -17
  58. package/api/theme/add/add.test.mjs +14 -1
  59. package/api/theme/generateTonalPalette.doc.mjs +1 -0
  60. package/api/theme/integration-themes.test.mjs +39 -28
  61. package/api/theme/list/list.test.mjs +19 -20
  62. package/api/theme/listThemes.doc.mjs +6 -5
  63. package/api/theme/themeAdd.doc.mjs +4 -3
  64. package/api/theme/themeBuild.doc.mjs +1 -0
  65. package/api/theme/themeList.doc.mjs +6 -3
  66. package/api/theme/themeListAvailable.doc.mjs +5 -3
  67. package/api/theme/themePaletteGenerate.doc.mjs +1 -0
  68. package/api/theme/themeTargets.doc.mjs +1 -0
  69. package/api/theme/themeTemplate.doc.mjs +1 -0
  70. package/api/upgrade/_adapter.d.mts +12 -3
  71. package/api/upgrade/_adapter.mjs +81 -66
  72. package/api/upgrade/provider-agreement.test.mjs +152 -0
  73. package/api/upgrade/run/run.mjs +1 -0
  74. package/api/upgrade/upgrade.doc.mjs +1 -0
  75. package/assets/codemods/integration-discovery.mjs +8 -2
  76. package/assets/codemods/integration-discovery.test.mjs +15 -0
  77. package/assets/codemods/runner.mjs +115 -5
  78. package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +216 -0
  79. package/assets/codemods/transforms/next/index.mjs +11 -1
  80. package/assets/codemods/transforms/next/migrate-theme-catalog-to-descriptors.mjs +141 -0
  81. package/assets/docs/cli-integrations.doc.mjs +17 -30
  82. package/assets/docs/cli.doc.mjs +15 -0
  83. package/assets/docs/theme.doc.mjs +1 -1
  84. package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
  85. package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
  86. package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
  87. package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
  88. package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
  89. package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
  90. package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
  91. package/authoring/codemod/codemod.doc.mjs +1 -1
  92. package/authoring/config/config.doc.mjs +1 -1
  93. package/authoring/debug/debug.doc.d.mts +11 -0
  94. package/authoring/debug/debug.doc.mjs +182 -0
  95. package/authoring/doctypes/base/graph-fields.doc.mjs +1 -1
  96. package/authoring/doctypes/command/command.doc.mjs +1 -1
  97. package/authoring/doctypes/command/type.ts +1 -1
  98. package/authoring/doctypes/component/type.ts +2 -2
  99. package/authoring/doctypes/doctypes-new.test.mjs +48 -6
  100. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  101. package/authoring/doctypes/enum/type.ts +1 -1
  102. package/authoring/doctypes/function/function.doc.mjs +1 -1
  103. package/authoring/doctypes/function/type.ts +1 -1
  104. package/authoring/doctypes/hook/type.ts +2 -2
  105. package/authoring/doctypes/load-contract.test.mjs +2 -1
  106. package/authoring/doctypes/parse.d.mts +4 -2
  107. package/authoring/doctypes/parse.mjs +7 -3
  108. package/authoring/doctypes/reference/reference.doc.mjs +1 -1
  109. package/authoring/doctypes/reference/type.ts +2 -2
  110. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  111. package/authoring/doctypes/schema/type.ts +1 -2
  112. package/authoring/doctypes/template/template.doc.mjs +3 -3
  113. package/authoring/doctypes/theme/parse.d.mts +35 -0
  114. package/authoring/doctypes/theme/parse.mjs +76 -0
  115. package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
  116. package/authoring/doctypes/theme/theme.doc.mjs +79 -0
  117. package/authoring/doctypes/theme/type.ts +42 -0
  118. package/authoring/doctypes/types.ts +2 -1
  119. package/authoring/gap-report/gap-report.doc.d.mts +12 -0
  120. package/authoring/gap-report/gap-report.doc.mjs +183 -0
  121. package/authoring/index.d.mts +1 -0
  122. package/authoring/index.d.ts +6 -4
  123. package/authoring/index.mjs +2 -1
  124. package/authoring/integration/integration.doc.mjs +2 -2
  125. package/authoring/integration/type.ts +3 -9
  126. package/clients/cli/__tests__/cliManifest.test.ts +27 -29
  127. package/clients/cli/commands/blog.doc.mjs +1 -1
  128. package/clients/cli/commands/build.doc.mjs +1 -1
  129. package/clients/cli/commands/component.doc.mjs +1 -1
  130. package/clients/cli/commands/discover.doc.mjs +1 -1
  131. package/clients/cli/commands/docs.doc.mjs +1 -1
  132. package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
  133. package/clients/cli/commands/doctor-integration-docs.doc.mjs +1 -1
  134. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -1
  135. package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
  136. package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
  137. package/clients/cli/commands/doctor-integration.test.mjs +15 -7
  138. package/clients/cli/commands/doctor.doc.mjs +1 -1
  139. package/clients/cli/commands/gap-report.doc.mjs +1 -1
  140. package/clients/cli/commands/hook.doc.mjs +1 -1
  141. package/clients/cli/commands/init.doc.mjs +1 -1
  142. package/clients/cli/commands/integration-add.controls.test.mjs +1 -1
  143. package/clients/cli/commands/integration-add.doc.mjs +1 -1
  144. package/clients/cli/commands/integration-pack.doc.mjs +1 -1
  145. package/clients/cli/commands/integration-real-world.test.mjs +3 -9
  146. package/clients/cli/commands/integration.doc.mjs +1 -1
  147. package/clients/cli/commands/layout-check.doc.mjs +1 -1
  148. package/clients/cli/commands/layout-expand.doc.mjs +1 -1
  149. package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
  150. package/clients/cli/commands/layout.doc.mjs +1 -1
  151. package/clients/cli/commands/manifest.doc.mjs +1 -1
  152. package/clients/cli/commands/search.doc.mjs +1 -1
  153. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  154. package/clients/cli/commands/template.doc.mjs +1 -1
  155. package/clients/cli/commands/theme-add.doc.mjs +2 -2
  156. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  157. package/clients/cli/commands/theme-list.doc.mjs +2 -2
  158. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -3
  159. package/clients/cli/commands/theme-palette.doc.mjs +1 -1
  160. package/clients/cli/commands/theme-targets.doc.mjs +1 -1
  161. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  162. package/clients/cli/commands/theme.doc.mjs +1 -1
  163. package/clients/cli/commands/upgrade.doc.mjs +1 -1
  164. package/clients/cli/lib/hook-format.mjs +14 -5
  165. package/foundation/config/project-themes.test.mjs +11 -19
  166. package/foundation/config/project.d.mts +8 -0
  167. package/foundation/config/project.mjs +50 -21
  168. package/foundation/config/project.test.mjs +3 -16
  169. package/foundation/discovery/authoring-self-docs.d.mts +6 -0
  170. package/foundation/discovery/authoring-self-docs.mjs +17 -7
  171. package/foundation/discovery/authoring-surface.d.mts +74 -0
  172. package/foundation/discovery/authoring-surface.mjs +525 -0
  173. package/foundation/discovery/authoring-surface.test.mjs +392 -0
  174. package/foundation/discovery/cli-self-docs.d.mts +113 -0
  175. package/foundation/discovery/cli-self-docs.mjs +514 -0
  176. package/foundation/discovery/cli-self-docs.test.mjs +437 -0
  177. package/foundation/discovery/component-loader.d.mts +35 -38
  178. package/foundation/discovery/component-loader.mjs +53 -222
  179. package/foundation/discovery/docs-discovery.d.mts +3 -2
  180. package/foundation/discovery/docs-discovery.mjs +8 -12
  181. package/foundation/discovery/docs-discovery.test.mjs +8 -5
  182. package/foundation/discovery/template-adapter.d.mts +7 -0
  183. package/foundation/discovery/template-adapter.mjs +27 -40
  184. package/foundation/discovery/template-adapter.test.mjs +15 -0
  185. package/foundation/discovery/theme-discovery.d.mts +67 -7
  186. package/foundation/discovery/theme-discovery.mjs +916 -186
  187. package/foundation/discovery/theme-discovery.test.mjs +613 -219
  188. package/foundation/doc-compiler/bundle.d.mts +47 -0
  189. package/foundation/doc-compiler/bundle.mjs +218 -0
  190. package/foundation/doc-compiler/bundle.test.mjs +255 -0
  191. package/foundation/doc-compiler/compile.d.mts +200 -0
  192. package/foundation/doc-compiler/compile.mjs +254 -5
  193. package/foundation/doc-compiler/diagnostics.d.mts +126 -0
  194. package/foundation/doc-compiler/diagnostics.mjs +277 -0
  195. package/foundation/doc-compiler/doc-compiler.test.mjs +28 -1
  196. package/foundation/doc-compiler/doc-loads.test.mjs +1642 -0
  197. package/foundation/doc-compiler/import.d.mts +24 -0
  198. package/foundation/doc-compiler/import.mjs +59 -0
  199. package/foundation/doc-compiler/inputs.d.mts +102 -0
  200. package/foundation/doc-compiler/inputs.mjs +286 -0
  201. package/foundation/doc-compiler/inputs.test.mjs +299 -0
  202. package/foundation/doc-compiler/ir.d.mts +13 -0
  203. package/foundation/doc-compiler/ir.mjs +189 -12
  204. package/foundation/doc-compiler/lower-doc.test.mjs +492 -0
  205. package/foundation/doc-compiler/overlays.d.mts +37 -0
  206. package/foundation/doc-compiler/overlays.mjs +206 -0
  207. package/foundation/doc-compiler/parse-readable.d.mts +9 -0
  208. package/foundation/doc-compiler/parse-readable.mjs +29 -0
  209. package/foundation/doc-compiler/read.d.mts +126 -0
  210. package/foundation/doc-compiler/read.mjs +320 -0
  211. package/foundation/doc-compiler/read.test.mjs +313 -0
  212. package/foundation/doc-compiler/source.d.mts +33 -0
  213. package/foundation/doc-compiler/source.mjs +128 -0
  214. package/foundation/integrations/autolink.d.mts +58 -1
  215. package/foundation/integrations/autolink.mjs +143 -57
  216. package/foundation/integrations/contribution-fixes.d.mts +145 -0
  217. package/foundation/integrations/contribution-fixes.mjs +1284 -0
  218. package/foundation/integrations/contribution-inventory.d.mts +1 -1
  219. package/foundation/integrations/contribution-inventory.mjs +17 -20
  220. package/foundation/integrations/contribution-inventory.test.mjs +67 -27
  221. package/foundation/integrations/integrations.d.mts +3 -0
  222. package/foundation/integrations/integrations.mjs +11 -97
  223. package/foundation/integrations/provider-ledger.test.mjs +275 -0
  224. package/foundation/integrations/provider-resolution.d.mts +152 -0
  225. package/foundation/integrations/provider-resolution.mjs +576 -0
  226. package/foundation/integrations/provider-resolution.test.mjs +369 -0
  227. package/foundation/integrations/theme-descriptor.d.mts +8 -0
  228. package/foundation/integrations/theme-descriptor.mjs +44 -0
  229. package/foundation/integrations/validate-contributions.mjs +104 -10
  230. package/foundation/response/error-codes.doc.mjs +2 -2
  231. package/foundation/response/error-codes.mjs +1 -1
  232. package/foundation/response/response-types.doc.mjs +1 -1
  233. package/foundation/response/response.doc.mjs +1 -1
  234. package/foundation/text/string-utils.mjs +22 -10
  235. package/package.json +9 -9
  236. package/assets/templates/themes/manifest.json +0 -95
@@ -0,0 +1,369 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Colocated tests for the provider resolver: one outcome per real package
5
+ * directory, precedence, and the wrappers that keep their old results.
6
+ *
7
+ * Fixtures live under a repo-local temp dir, not /tmp, because Vite refuses to
8
+ * dynamically import a module from outside the project root.
9
+ */
10
+
11
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import {packageKey, resolveProviders} from './provider-resolution.mjs';
15
+ import {markProviderConflicts} from './integrations.mjs';
16
+ import {autolinkIntegrations} from './autolink.mjs';
17
+ import {Project, providerLedgerOf} from '../config/project.mjs';
18
+
19
+ let tmpDir;
20
+
21
+ beforeEach(() => {
22
+ tmpDir = fs.mkdtempSync(
23
+ path.join(process.cwd(), '.astryx-provider-resolution-'),
24
+ );
25
+ });
26
+
27
+ afterEach(() => {
28
+ fs.rmSync(tmpDir, {recursive: true, force: true});
29
+ });
30
+
31
+ /**
32
+ * A loaded-integration record in a real directory.
33
+ * @param {string} dir directory under the temp root
34
+ * @param {Record<string, unknown>} fields
35
+ * @returns {any}
36
+ */
37
+ function record(dir, fields) {
38
+ const packageDir = path.join(tmpDir, dir);
39
+ fs.mkdirSync(packageDir, {recursive: true});
40
+ return {
41
+ providerId: fields.name,
42
+ __spec: fields.name,
43
+ __packageDir: packageDir,
44
+ __manifestFile: path.join(packageDir, 'astryx.integration.mjs'),
45
+ ...fields,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {string} source
51
+ * @param {any} integration
52
+ */
53
+ const candidate = (source, integration) => ({
54
+ source,
55
+ integration,
56
+ spec: integration.__spec,
57
+ });
58
+
59
+ /** @param {Map<string, any>} ledger */
60
+ function outcomes(ledger) {
61
+ return [...ledger.values()].map(entry => [entry.label, entry.outcome]);
62
+ }
63
+
64
+ describe('resolveProviders ledger', () => {
65
+ it('records one outcome for every candidate, keyed by real directory', () => {
66
+ const widgets = record('widgets', {
67
+ name: '@acme/widgets',
68
+ version: '1.0.0',
69
+ });
70
+ const renamed = record('renamed', {
71
+ name: '@acme/renamed',
72
+ version: '2.0.0',
73
+ providerId: '@acme/widgets',
74
+ });
75
+ const broken = record('broken', {
76
+ name: '@acme/broken',
77
+ version: '1.0.0',
78
+ __loadError: 'boom',
79
+ });
80
+ const {integrations, ledger} = resolveProviders([
81
+ candidate('configured', widgets),
82
+ candidate('configured', renamed),
83
+ candidate('configured', broken),
84
+ ]);
85
+
86
+ expect([...ledger.keys()]).toEqual(
87
+ [widgets, renamed, broken].map(entry => packageKey(entry.__packageDir)),
88
+ );
89
+ expect(outcomes(ledger)).toEqual([
90
+ ['@acme/widgets@1.0.0', 'contributes'],
91
+ ['@acme/renamed@2.0.0', 'set-aside'],
92
+ ['@acme/broken@1.0.0', 'load-failed'],
93
+ ]);
94
+ const setAside = ledger.get(packageKey(renamed.__packageDir));
95
+ expect(setAside).toMatchObject({
96
+ winner: packageKey(widgets.__packageDir),
97
+ reason: 'claimed-earlier',
98
+ integration: integrations[1],
99
+ });
100
+ expect(integrations[1].__providerConflict.claimedBy).toBe('@acme/widgets');
101
+ });
102
+
103
+ it('keeps two packages that share a name apart', () => {
104
+ const old = record('widgets-old', {
105
+ name: '@acme/widgets',
106
+ version: '1.0.0',
107
+ __spec: 'widgets-old',
108
+ });
109
+ const next = record('widgets-next', {
110
+ name: '@acme/widgets',
111
+ version: '2.0.0',
112
+ __spec: 'widgets-next',
113
+ });
114
+ const {ledger} = resolveProviders([
115
+ candidate('configured', old),
116
+ candidate('configured', next),
117
+ ]);
118
+
119
+ expect(outcomes(ledger)).toEqual([
120
+ ['@acme/widgets@1.0.0', 'contributes'],
121
+ ['@acme/widgets@2.0.0', 'set-aside'],
122
+ ]);
123
+ });
124
+
125
+ it('folds every spelling of one real directory into one entry', () => {
126
+ const ui = record('ui', {name: '@acme/ui', version: '0.1.22'});
127
+ const link = path.join(tmpDir, 'legacy-ui');
128
+ fs.symlinkSync(ui.__packageDir, link, 'dir');
129
+ const legacy = {...ui, __spec: '@acme/legacy-ui', __packageDir: link};
130
+ const {integrations, ledger} = resolveProviders([
131
+ candidate('configured', ui),
132
+ candidate('configured', legacy),
133
+ ]);
134
+
135
+ expect(integrations).toEqual([ui]);
136
+ expect([...ledger.keys()]).toEqual([packageKey(ui.__packageDir)]);
137
+ const [entry] = ledger.values();
138
+ expect(entry.outcome).toBe('contributes');
139
+ expect(entry.refs.map(ref => ref.spec)).toEqual([
140
+ '@acme/ui',
141
+ '@acme/legacy-ui',
142
+ ]);
143
+ });
144
+
145
+ it('records an autolinked copy of a loaded package as alias-of it', () => {
146
+ const ui = record('ui', {name: '@acme/ui', version: '0.1.22'});
147
+ const copy = record('legacy-ui', {
148
+ name: '@acme/ui',
149
+ version: '0.1.22',
150
+ __spec: '@acme/legacy-ui',
151
+ __autolinked: true,
152
+ });
153
+ const {integrations, ledger} = resolveProviders([
154
+ candidate('configured', ui),
155
+ candidate('autolinked', copy),
156
+ ]);
157
+
158
+ expect(integrations).toEqual([ui]);
159
+ expect(ledger.get(packageKey(copy.__packageDir))).toMatchObject({
160
+ outcome: 'alias-of',
161
+ aliasOf: packageKey(ui.__packageDir),
162
+ });
163
+ });
164
+
165
+ it('records an autolinked package that failed to load, and lists it nowhere', () => {
166
+ const packageDir = path.join(tmpDir, 'unreadable');
167
+ const {integrations, ledger} = resolveProviders([
168
+ {
169
+ source: 'autolinked',
170
+ spec: '@acme/unreadable',
171
+ packageDir,
172
+ error: 'bad json',
173
+ },
174
+ ]);
175
+
176
+ expect(integrations).toEqual([]);
177
+ expect(ledger.get(packageKey(packageDir))).toMatchObject({
178
+ outcome: 'load-failed',
179
+ error: 'bad json',
180
+ });
181
+ });
182
+
183
+ it('lets the package being authored replace its installed copy and claim first', () => {
184
+ const installed = record('installed', {
185
+ name: '@acme/widgets',
186
+ version: '1.0.0',
187
+ });
188
+ const rival = record('rival', {
189
+ name: '@acme/rival',
190
+ version: '1.0.0',
191
+ providerId: '@acme/widgets',
192
+ });
193
+ const local = record('local', {
194
+ name: '@acme/widgets',
195
+ version: '2.0.0-dev',
196
+ __local: true,
197
+ });
198
+ const {integrations, ledger} = resolveProviders([
199
+ candidate('configured', rival),
200
+ candidate('configured', installed),
201
+ candidate('local', local),
202
+ ]);
203
+
204
+ expect(integrations.map(entry => entry.name)).toEqual([
205
+ '@acme/rival',
206
+ '@acme/widgets',
207
+ ]);
208
+ expect(integrations[1]).toBe(local);
209
+ expect(ledger.get(packageKey(installed.__packageDir))).toMatchObject({
210
+ outcome: 'set-aside',
211
+ reason: 'replaced-by-authored',
212
+ winner: packageKey(local.__packageDir),
213
+ });
214
+ expect(ledger.get(packageKey(rival.__packageDir))).toMatchObject({
215
+ outcome: 'set-aside',
216
+ reason: 'authored-claims',
217
+ winner: packageKey(local.__packageDir),
218
+ });
219
+ });
220
+
221
+ it('keeps configured precedence ahead of autolinked', () => {
222
+ const configured = record('configured', {
223
+ name: '@acme/one',
224
+ version: '1.0.0',
225
+ });
226
+ const autolinked = record('autolinked', {
227
+ name: '@acme/two',
228
+ version: '1.0.0',
229
+ providerId: '@acme/one',
230
+ __autolinked: true,
231
+ });
232
+ const {ledger} = resolveProviders([
233
+ candidate('configured', configured),
234
+ candidate('autolinked', autolinked),
235
+ ]);
236
+
237
+ expect(outcomes(ledger)).toEqual([
238
+ ['@acme/one@1.0.0', 'contributes'],
239
+ ['@acme/two@1.0.0', 'set-aside'],
240
+ ]);
241
+ });
242
+
243
+ it('under the codemod policy, records the installed copy standing in', () => {
244
+ const local = record('local', {
245
+ name: '@acme/widgets',
246
+ version: '2.0.0-dev',
247
+ __local: true,
248
+ });
249
+ const installed = record('installed', {
250
+ name: '@acme/widgets',
251
+ version: '1.0.0',
252
+ });
253
+ const {integrations, ledger} = resolveProviders(
254
+ [candidate('configured', local), candidate('installed', installed)],
255
+ {codemods: true},
256
+ );
257
+
258
+ expect(integrations).toEqual([installed]);
259
+ expect(ledger.get(packageKey(local.__packageDir))).toMatchObject({
260
+ outcome: 'set-aside',
261
+ reason: 'installed-stands-in',
262
+ winner: packageKey(installed.__packageDir),
263
+ });
264
+ });
265
+ });
266
+
267
+ describe('markProviderConflicts', () => {
268
+ const a = {name: 'a', version: '1', providerId: 'x', __spec: 'a'};
269
+ const b = {name: 'b', version: '1', providerId: 'x', __spec: 'b'};
270
+ const alias = {name: 'a', version: '1', providerId: 'x', __spec: 'a-alias'};
271
+ const local = {
272
+ name: 'l',
273
+ version: '1',
274
+ providerId: 'x',
275
+ __spec: 'l',
276
+ __local: true,
277
+ };
278
+ /** @param {any[]} list */
279
+ const claimsOf = list =>
280
+ markProviderConflicts(list).map(entry => [
281
+ entry?.name ?? null,
282
+ entry?.__providerConflict?.claimedBy ?? null,
283
+ ]);
284
+
285
+ it('drops an alias of the winner and keeps a record listed twice', () => {
286
+ expect(claimsOf([a, b, alias, a, undefined])).toEqual([
287
+ ['a', null],
288
+ ['b', 'a'],
289
+ ['a', null],
290
+ [null, null],
291
+ ]);
292
+ });
293
+
294
+ it('lets the package being authored claim first', () => {
295
+ expect(claimsOf([a, undefined, b, alias, a, local])).toEqual([
296
+ ['a', 'l'],
297
+ [null, null],
298
+ ['b', 'l'],
299
+ ['a', 'l'],
300
+ ['a', 'l'],
301
+ ['l', null],
302
+ ]);
303
+ });
304
+ });
305
+
306
+ describe('Project.load keeps a ledger', () => {
307
+ /**
308
+ * @param {string} dir node_modules key
309
+ * @param {{name: string, version: string}} pkg
310
+ */
311
+ function install(dir, pkg) {
312
+ const pkgDir = path.join(tmpDir, 'node_modules', ...dir.split('/'));
313
+ fs.mkdirSync(pkgDir, {recursive: true});
314
+ fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify(pkg));
315
+ fs.writeFileSync(
316
+ path.join(pkgDir, 'astryx.integration.mjs'),
317
+ 'export default {};\n',
318
+ );
319
+ return pkgDir;
320
+ }
321
+
322
+ it('records the autolink alias it drops as alias-of the loaded copy', async () => {
323
+ fs.writeFileSync(
324
+ path.join(tmpDir, 'package.json'),
325
+ JSON.stringify({
326
+ name: 'consumer',
327
+ dependencies: {
328
+ '@acme/legacy-ui': 'npm:@acme/ui@0.1.22',
329
+ '@acme/ui': '0.1.22',
330
+ },
331
+ }),
332
+ );
333
+ const legacy = install('@acme/legacy-ui', {
334
+ name: '@acme/ui',
335
+ version: '0.1.22',
336
+ });
337
+ const ui = install('@acme/ui', {name: '@acme/ui', version: '0.1.22'});
338
+
339
+ const project = await Project.load(tmpDir, {fresh: true});
340
+ expect(project.loadedIntegrations.map(entry => entry.__spec)).toEqual([
341
+ '@acme/legacy-ui',
342
+ ]);
343
+ const ledger = providerLedgerOf(project);
344
+ expect(ledger.get(packageKey(ui))).toMatchObject({
345
+ outcome: 'alias-of',
346
+ aliasOf: packageKey(legacy),
347
+ });
348
+ expect(await autolinkIntegrations({projectDir: tmpDir})).toHaveLength(1);
349
+ });
350
+
351
+ it('records a dependency key that repeats a configured package', async () => {
352
+ fs.writeFileSync(
353
+ path.join(tmpDir, 'package.json'),
354
+ JSON.stringify({name: 'consumer', dependencies: {'@acme/ui': '0.1.22'}}),
355
+ );
356
+ fs.writeFileSync(
357
+ path.join(tmpDir, 'astryx.config.mjs'),
358
+ "export default {integrations: ['@acme/ui']};\n",
359
+ );
360
+ const ui = install('@acme/ui', {name: '@acme/ui', version: '0.1.22'});
361
+
362
+ const ledger = providerLedgerOf(await Project.load(tmpDir, {fresh: true}));
363
+ expect([...ledger.keys()]).toEqual([packageKey(ui)]);
364
+ expect(ledger.get(packageKey(ui))?.refs.map(ref => ref.source)).toEqual([
365
+ 'configured',
366
+ 'autolinked',
367
+ ]);
368
+ });
369
+ });
@@ -0,0 +1,8 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * @param {import('../../authoring/doctypes/theme/type.js').ThemeDoc} doc
6
+ * @returns {string}
7
+ */
8
+ export function themeDescriptorSource(doc: import("../../authoring/doctypes/theme/type.js").ThemeDoc): string;
@@ -0,0 +1,44 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The source text of a typed integration theme descriptor.
5
+ *
6
+ * @input A theme's descriptor fields.
7
+ * @output The `.doc.mjs` source that declares them under their ThemeDoc type.
8
+ * @position Shared by `integration add theme`, which scaffolds a descriptor,
9
+ * and the codemod that turns a 0.6 theme catalog into descriptors, so both
10
+ * write the same bytes.
11
+ */
12
+
13
+ /**
14
+ * A single-quoted JavaScript string literal for any text.
15
+ * @param {string} value
16
+ */
17
+ function quote(value) {
18
+ const escaped = value
19
+ .replace(/\\/gu, '\\\\')
20
+ .replace(/'/gu, "\\'")
21
+ .replace(/\n/gu, '\\n')
22
+ .replace(/\r/gu, '\\r')
23
+ .replace(/\u2028/gu, '\\u2028')
24
+ .replace(/\u2029/gu, '\\u2029');
25
+ return `'${escaped}'`;
26
+ }
27
+
28
+ /**
29
+ * @param {import('../../authoring/doctypes/theme/type.js').ThemeDoc} doc
30
+ * @returns {string}
31
+ */
32
+ export function themeDescriptorSource(doc) {
33
+ return [
34
+ "/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */",
35
+ 'export default {',
36
+ " type: 'theme',",
37
+ ` name: ${quote(doc.name)},`,
38
+ ` displayName: ${quote(doc.displayName)},`,
39
+ ` description: ${quote(doc.description)},`,
40
+ ` maintained: ${doc.maintained},`,
41
+ '};',
42
+ '',
43
+ ].join('\n');
44
+ }
@@ -25,6 +25,23 @@ import * as fs from 'node:fs';
25
25
  import * as path from 'node:path';
26
26
  import {isValidSemver} from '../env/semver.mjs';
27
27
  import {findSourceOnlyCandidates} from './contribution-inventory.mjs';
28
+ import {
29
+ CODEMOD_FILE_RE,
30
+ CODEMOD_SKIP_DIRS,
31
+ CODEMOD_TEST_RE,
32
+ createFixContext,
33
+ DOC_CANDIDATE_RE,
34
+ heldRootFix,
35
+ newComponentDocNote,
36
+ notACodemodFix,
37
+ notAComponentFix,
38
+ readContributionStamp,
39
+ sharedCodemodsRootFix,
40
+ stampOf,
41
+ themeImporting,
42
+ TEMPLATE_CANDIDATE_RE,
43
+ } from './contribution-fixes.mjs';
44
+ import {MANIFEST_BASENAMES} from './integrations.mjs';
28
45
  import {discoverIntegrationCodemods} from '../../assets/codemods/integration-discovery.mjs';
29
46
  import {discoverIntegrationTemplatesForOne} from '../discovery/template-adapter.mjs';
30
47
  import * as componentDiscovery from '../discovery/component-discovery.mjs';
@@ -119,32 +136,82 @@ function checkRoots(resolved, issues) {
119
136
  */
120
137
  async function checkCodemods(integration, issues) {
121
138
  if (!integration.codemods || !fs.existsSync(integration.codemods)) return;
139
+ const context = integration.__packageDir
140
+ ? createFixContext(integration.__packageDir, integration)
141
+ : null;
142
+ const shared = context && sharedCodemodsRootFix(context);
122
143
  for (const entry of fs.readdirSync(integration.codemods, {
123
144
  withFileTypes: true,
124
145
  })) {
125
146
  if (
126
147
  entry.isFile() &&
127
- /\.(?:ts|mjs|js)$/u.test(entry.name) &&
128
- !/\.(?:test|spec|fixture)\.(?:ts|mjs|js)$/u.test(entry.name)
148
+ CODEMOD_FILE_RE.test(entry.name) &&
149
+ !CODEMOD_TEST_RE.test(entry.name) &&
150
+ // The manifest sits beside package.json and is never a codemod.
151
+ !(
152
+ integration.codemods === integration.__packageDir &&
153
+ MANIFEST_BASENAMES.includes(entry.name)
154
+ )
129
155
  ) {
156
+ const shown = path
157
+ .relative(
158
+ integration.__packageDir ?? path.dirname(integration.codemods),
159
+ path.join(integration.codemods, entry.name),
160
+ )
161
+ .split(path.sep)
162
+ .join('/');
163
+ const root = path.dirname(shown);
164
+ const full = path.join(integration.codemods, entry.name);
165
+ const stamp =
166
+ context &&
167
+ (DOC_CANDIDATE_RE.test(entry.name) ||
168
+ TEMPLATE_CANDIDATE_RE.test(entry.name))
169
+ ? stampOf(context, full)
170
+ : null;
171
+ // A theme's entry module may sit beside its descriptor.
172
+ const themeDoc = ['.doc.mjs', '.doc.ts', '.doc.js']
173
+ .map(suffix => full.replace(/\.(?:ts|mjs|js)$/u, suffix))
174
+ .find(candidate => candidate !== full && fs.existsSync(candidate));
175
+ const themeStamp =
176
+ context && !stamp && themeDoc ? stampOf(context, themeDoc) : null;
177
+ let fix = `Fix: move it into the folder named for the version it migrates to, for example ${root}/1.2.0/${entry.name}.`;
178
+ if (shared) fix = sharedCodemodsRootFix(context, entry.name) ?? fix;
179
+ else if (context && stamp) fix = notACodemodFix(context, full, stamp);
180
+ else if (context && themeDoc && themeStamp?.type === 'theme') {
181
+ fix = notACodemodFix(context, themeDoc, themeStamp, entry.name);
182
+ } else if (context) {
183
+ const owner = themeImporting(context, full);
184
+ if (owner) {
185
+ fix = notACodemodFix(
186
+ context,
187
+ owner.doc,
188
+ owner.stamp,
189
+ owner.source,
190
+ entry.name,
191
+ );
192
+ }
193
+ }
130
194
  issues.push(
131
195
  issueWarning(
132
196
  'codemod_outside_version',
133
- `Codemod file "${path.join(integration.codemods, entry.name)}" is outside a version folder, so upgrade will never load it.`,
197
+ `Codemod file "${shown}" is outside a version folder, so upgrade will never load it. ${fix}`,
134
198
  ),
135
199
  );
136
200
  }
137
201
  if (
138
202
  entry.isDirectory() &&
139
- !['node_modules', '.git', '__tests__', '__fixtures__'].includes(
140
- entry.name,
141
- ) &&
203
+ !CODEMOD_SKIP_DIRS.has(entry.name) &&
142
204
  !isValidSemver(entry.name)
143
205
  ) {
206
+ const fix =
207
+ shared ??
208
+ (context &&
209
+ heldRootFix(context, path.join(integration.codemods, entry.name))) ??
210
+ 'Fix: rename it to the exact version its codemods migrate to.';
144
211
  issues.push(
145
212
  issueError(
146
213
  'invalid_codemod_version',
147
- `Codemod folder "${entry.name}" is not an exact semver version such as 1.2.0.`,
214
+ `Codemod folder "${entry.name}" is not an exact semver version such as 1.2.0. ${fix}`,
148
215
  ),
149
216
  );
150
217
  }
@@ -196,24 +263,51 @@ async function checkComponents(integration, issues) {
196
263
  if (typeof discover !== 'function') return; // feature not present yet
197
264
  try {
198
265
  const records = (await discover(integration)) ?? [];
266
+ /** @type {import('./contribution-fixes.mjs').FixContext | undefined} */
267
+ let context;
199
268
  for (const record of records) {
200
269
  if (record?.sourcePath == null) {
270
+ const docPath = record?.docPath;
271
+ let fix = `Fix: add ${record?.name}.tsx beside ${docPath ? path.basename(docPath) : `${record?.name}.doc.mjs`}.`;
272
+ const stamp = docPath ? readContributionStamp(docPath, false) : null;
273
+ if (stamp && stamp.type !== 'component' && integration.__packageDir) {
274
+ context ??= createFixContext(integration.__packageDir, integration);
275
+ fix = notAComponentFix(context, docPath, stamp);
276
+ }
201
277
  issues.push(
202
278
  issueError(
203
279
  'invalid_component',
204
- `Component "${record?.name}" is missing its same-stem source file ${record?.name}.tsx.`,
280
+ `Component "${record?.name}" is missing its same-stem source file ${record?.name}.tsx. ${fix}`,
205
281
  ),
206
282
  );
207
283
  }
208
284
  }
285
+ const overlap = integration.__packageDir
286
+ ? newComponentDocNote(
287
+ (context ??= createFixContext(integration.__packageDir, integration)),
288
+ )
289
+ : '';
209
290
  for (const name of findSourceOnlyCandidates(
210
291
  integration.components,
211
292
  records.map(record => record.name),
212
293
  )) {
294
+ // A hidden doc still pairs with its source; discovery just skips it.
295
+ if (
296
+ ['.doc.ts', '.doc.mjs', '.doc.js'].some(suffix =>
297
+ fs.existsSync(
298
+ path.join(
299
+ /** @type {string} */ (integration.components),
300
+ `${name}${suffix}`,
301
+ ),
302
+ ),
303
+ )
304
+ ) {
305
+ continue;
306
+ }
213
307
  issues.push(
214
308
  issueWarning(
215
309
  'source_without_component_doc',
216
- `Component source "${name}.tsx" has no same-stem metadata file ${name}.doc.mjs, so Astryx ignores it.`,
310
+ `Component source "${name}.tsx" has no same-stem metadata file ${name}.doc.mjs, so Astryx ignores it. Fix: add ${name}.doc.mjs beside it with type: 'component'; \`astryx docs authoring component-doc\` lists its fields.${overlap}`,
217
311
  ),
218
312
  );
219
313
  }
@@ -250,7 +344,7 @@ async function checkDocs(integration, issues) {
250
344
  }
251
345
 
252
346
  /**
253
- * Validate the integration's source-theme catalog. The same discovery function
347
+ * Validate the integration's source-theme descriptors. The same discovery function
254
348
  * powers Project and theme list/add, so validation cannot accept a shape that
255
349
  * consumers later fail to use.
256
350
  * @param {LoadedIntegration} integration
@@ -12,7 +12,7 @@ export const doc = {
12
12
  type: 'enum',
13
13
  name: 'error-codes',
14
14
  displayName: 'Error Codes',
15
- namespace: 'cli',
15
+ namespace: 'cli/api',
16
16
  description:
17
17
  'Stable, append-only machine-readable error codes on the --json error envelope (envelope.code). Codes never change or get removed; branch on these, not on prose.',
18
18
  members: [
@@ -203,7 +203,7 @@ export const doc = {
203
203
  {
204
204
  value: 'ERR_THEME_INVALID',
205
205
  description:
206
- 'A theme definition or contributed theme catalog is invalid.',
206
+ 'A theme definition or contributed theme descriptor is invalid.',
207
207
  },
208
208
  {
209
209
  value: 'ERR_THEME_LOAD',
@@ -185,7 +185,7 @@ export const ERROR_CODES = Object.freeze({
185
185
  ERR_WRITE_FAILED: 'ERR_WRITE_FAILED',
186
186
 
187
187
  // ── Theme build ──────────────────────────────────────────────────
188
- /** A theme definition or contributed theme catalog is invalid. */
188
+ /** A theme definition or contributed theme descriptor is invalid. */
189
189
  ERR_THEME_INVALID: 'ERR_THEME_INVALID',
190
190
  /** A theme file could not be loaded / parsed into a defineTheme result. */
191
191
  ERR_THEME_LOAD: 'ERR_THEME_LOAD',
@@ -13,7 +13,7 @@ export const doc = {
13
13
  type: 'enum',
14
14
  name: 'response-types',
15
15
  displayName: 'Response Types',
16
- namespace: 'cli',
16
+ namespace: 'cli/api',
17
17
  description:
18
18
  'The `type` discriminant present on every --json success envelope. Consumers switch on it to narrow `data`.',
19
19
  members: [
@@ -12,7 +12,7 @@ export const doc = {
12
12
  type: 'schema',
13
13
  name: 'output',
14
14
  displayName: 'Output Envelope',
15
- namespace: 'cli',
15
+ namespace: 'cli/api',
16
16
  description:
17
17
  'The single JSON envelope every command emits under --json. Success is ' +
18
18
  '{ apiVersion, type, data, meta? }; failure is { apiVersion, error, code, ' +