@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
@@ -33,8 +33,19 @@
33
33
 
34
34
  import * as fs from 'node:fs';
35
35
  import * as path from 'node:path';
36
- import jscodeshift from 'jscodeshift';
37
36
  import {assertWithin} from '../../foundation/fs/path-safety.mjs';
37
+ import {
38
+ THEME_SLUG_RE,
39
+ unreadThemeFolders,
40
+ } from '../../foundation/discovery/theme-discovery.mjs';
41
+ import {
42
+ createFixContext,
43
+ DOC_CANDIDATE_RE,
44
+ pathIsInside,
45
+ stampOf,
46
+ TEMPLATE_CANDIDATE_RE,
47
+ unreachableFix,
48
+ } from '../../foundation/integrations/contribution-fixes.mjs';
38
49
  import {
39
50
  findManifestPaths,
40
51
  loadManifest,
@@ -90,106 +101,35 @@ const UNREACHABLE_SKIP_DIRS = new Set([
90
101
  '.next',
91
102
  'out',
92
103
  ]);
93
- const DOC_CANDIDATE_RE = /\.doc\.(?:ts|mjs|js)$/u;
94
- const TEMPLATE_CANDIDATE_RE = /\.template\.(?:ts|mjs|js)$/u;
95
- const STATIC_DOC_TYPES = new Set(['component', 'generic', 'page', 'block']);
96
- const STATIC_TEMPLATE_TYPES = new Set(['page', 'block']);
97
- const j = jscodeshift.withParser('tsx');
98
-
99
- /** @param {any} node @returns {any} */
100
- function unwrapStaticExpression(node) {
101
- let current = node;
102
- while (
103
- current &&
104
- [
105
- 'TSSatisfiesExpression',
106
- 'TSAsExpression',
107
- 'TypeCastExpression',
108
- 'ParenthesizedExpression',
109
- ].includes(current.type)
110
- ) {
111
- current = current.expression;
112
- }
113
- if (current?.type === 'CallExpression' && current.arguments.length > 0) {
114
- current = unwrapStaticExpression(current.arguments[0]);
115
- }
116
- return current;
117
- }
118
-
119
- /** @param {any} property @param {string} name */
120
- function staticPropertyNamed(property, name) {
121
- if (
122
- !property ||
123
- !['ObjectProperty', 'Property'].includes(property.type) ||
124
- property.computed
125
- ) {
126
- return false;
127
- }
128
- return (
129
- (property.key?.type === 'Identifier' && property.key.name === name) ||
130
- (['Literal', 'StringLiteral'].includes(property.key?.type) &&
131
- property.key.value === name)
132
- );
133
- }
134
-
135
104
  /**
136
- * Identify contribution metadata without importing it. Doctor scans files that
137
- * the manifest does not declare, so executing those files would run code the
138
- * package never asked Astryx to load.
139
- *
140
- * @param {string} file
141
- * @param {boolean} templateOnly
105
+ * A warning for each folder under the themes root that holds modules but is
106
+ * not read as a theme. Either branch of its fix leaves nothing to warn about:
107
+ * the folder becomes a theme, or a dot-folder discovery skips.
108
+ * @param {string} packageDir
109
+ * @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration} loaded
110
+ * @returns {Issue[]}
142
111
  */
143
- function isStaticContributionMetadata(file, templateOnly) {
144
- let ast;
145
- try {
146
- ast = j(fs.readFileSync(file, 'utf-8'));
147
- } catch {
148
- return false;
149
- }
150
- /** @type {any[]} */
151
- const candidates = [];
152
- ast
153
- .find(j.ExportDefaultDeclaration)
154
- .forEach((/** @type {any} */ exportPath) => {
155
- candidates.push(exportPath.value.declaration);
156
- });
157
- ast
158
- .find(j.ExportNamedDeclaration)
159
- .forEach((/** @type {any} */ exportPath) => {
160
- const declaration = exportPath.value.declaration;
161
- if (declaration?.type !== 'VariableDeclaration') return;
162
- for (const declarator of declaration.declarations) {
163
- if (
164
- declarator.id?.type === 'Identifier' &&
165
- declarator.id.name === 'docs'
166
- ) {
167
- candidates.push(declarator.init);
168
- }
169
- }
170
- });
171
-
172
- const allowedTypes = templateOnly ? STATIC_TEMPLATE_TYPES : STATIC_DOC_TYPES;
173
- for (const candidate of candidates) {
174
- const object = unwrapStaticExpression(candidate);
175
- if (object?.type !== 'ObjectExpression') continue;
176
- const typeProperty = object.properties.find((/** @type {any} */ property) =>
177
- staticPropertyNamed(property, 'type'),
178
- );
179
- const value = unwrapStaticExpression(typeProperty?.value);
180
- if (
181
- ['Literal', 'StringLiteral'].includes(value?.type) &&
182
- allowedTypes.has(value.value)
183
- ) {
184
- return true;
112
+ function unreadThemeFolderIssues(packageDir, loaded) {
113
+ const themes = /** @type {string} */ (loaded.themes);
114
+ return unreadThemeFolders(themes, {
115
+ packageDir,
116
+ packageName: loaded.name,
117
+ }).map(folder => {
118
+ const name = path.basename(folder);
119
+ const shown = `${path.relative(packageDir, folder).split(path.sep).join('/')}/`;
120
+ const hide = `rename it to .${name} so Astryx skips it`;
121
+ let fix;
122
+ if (THEME_SLUG_RE.test(name)) {
123
+ const stem = `${name.replace(/-([a-z0-9])/gu, (_, character) => character.toUpperCase())}Theme`;
124
+ fix = `if it is a theme, add ${stem}.ts and ${stem}.doc.mjs to it (\`astryx integration add theme ${name}\` writes both) and move its code into ${stem}.ts; if not, ${hide}.`;
125
+ } else {
126
+ fix = `${hide}; a theme folder needs a lower-kebab name.`;
185
127
  }
186
- }
187
- return false;
188
- }
189
-
190
- /** @param {string} candidate @param {string} root */
191
- function pathIsInside(candidate, root) {
192
- return candidate === root || candidate.startsWith(`${root}${path.sep}`);
128
+ return warning(
129
+ 'unread_theme_folder',
130
+ `Folder "${shown}" holds modules but no <name>Theme source or .doc.mjs descriptor, so Astryx does not read it as a theme. Fix: ${fix}`,
131
+ );
132
+ });
193
133
  }
194
134
 
195
135
  /**
@@ -214,6 +154,7 @@ async function findUnreachableContributionIssues(packageDir, loaded) {
214
154
  );
215
155
  /** @type {Issue[]} */
216
156
  const issues = [];
157
+ const context = createFixContext(packageDir, loaded);
217
158
  let scanned = 0;
218
159
  let truncated = false;
219
160
 
@@ -237,13 +178,18 @@ async function findUnreachableContributionIssues(packageDir, loaded) {
237
178
  truncated = true;
238
179
  return;
239
180
  }
240
- const isTemplate = TEMPLATE_CANDIDATE_RE.test(entry.name);
241
- if (!isTemplate && !DOC_CANDIDATE_RE.test(entry.name)) continue;
242
- if (!isStaticContributionMetadata(full, isTemplate)) continue;
181
+ if (
182
+ !TEMPLATE_CANDIDATE_RE.test(entry.name) &&
183
+ !DOC_CANDIDATE_RE.test(entry.name)
184
+ ) {
185
+ continue;
186
+ }
187
+ const stamp = stampOf(context, full);
188
+ if (stamp == null) continue;
243
189
  issues.push(
244
190
  warning(
245
191
  'unreachable_contribution',
246
- `Found contribution metadata "${path.relative(packageDir, full)}" outside every declared integration root, so it contributes nothing. Move it under the matching root or update the manifest root.`,
192
+ `Found contribution metadata "${path.relative(packageDir, full)}" outside every declared integration root, so it contributes nothing. ${unreachableFix(context, full, stamp)}`,
247
193
  ),
248
194
  );
249
195
  }
@@ -366,6 +312,8 @@ async function validateAtPackageDir(
366
312
  // Roots + contribution checks are shared with validateLoadedIntegration so
367
313
  // the everyday-command nudge runs the exact same validators.
368
314
  issues.push(...(await validateLoadedIntegration(loaded)));
315
+ if (loaded.themes)
316
+ issues.push(...unreadThemeFolderIssues(packageDir, loaded));
369
317
  if (scanUnreachable) {
370
318
  issues.push(
371
319
  ...(await findUnreachableContributionIssues(packageDir, loaded)),
@@ -193,6 +193,39 @@ describe('validate-integration API', () => {
193
193
  expect(byCode(result.issues, 'invalid_codemod')).toHaveLength(1);
194
194
  });
195
195
 
196
+ it('names a broken codemod of an installed package by its path inside the package', async () => {
197
+ const consumer = path.join(tmpDir, 'consumer');
198
+ fs.mkdirSync(consumer, {recursive: true});
199
+ fs.writeFileSync(
200
+ path.join(consumer, 'package.json'),
201
+ JSON.stringify({name: 'consumer'}),
202
+ );
203
+ const pkgDir = path.join(consumer, 'node_modules', '@acme', 'kit');
204
+ writePackage(pkgDir, {
205
+ name: '@acme/kit',
206
+ manifest: `export default { codemods: './codemods' };\n`,
207
+ });
208
+ fs.mkdirSync(path.join(pkgDir, 'codemods', '1.1.0'), {recursive: true});
209
+ fs.writeFileSync(
210
+ path.join(pkgDir, 'codemods', '1.1.0', 'no-default.mjs'),
211
+ 'export const nope = 1;\n',
212
+ );
213
+ fs.writeFileSync(
214
+ path.join(pkgDir, 'codemods', 'stray.mjs'),
215
+ `export default {type: 'code', title: 'Stray', transform: file => file.source};\n`,
216
+ );
217
+
218
+ const result = await validateInstalledIntegration('@acme/kit', consumer);
219
+
220
+ const [broken] = byCode(result.issues, 'invalid_codemod');
221
+ expect(broken.message).toContain('(codemods/1.1.0/no-default.mjs)');
222
+ const [stray] = byCode(result.issues, 'codemod_outside_version');
223
+ expect(stray.message).toContain('"codemods/stray.mjs"');
224
+ for (const issue of result.issues) {
225
+ expect(issue.message).not.toContain(tmpDir);
226
+ }
227
+ });
228
+
196
229
  it('flags a broken template as invalid_template error', async () => {
197
230
  const pkgDir = path.join(tmpDir, 'pkg');
198
231
  writePackage(pkgDir, {
@@ -210,33 +243,24 @@ describe('validate-integration API', () => {
210
243
  expect(byCode(result.issues, 'invalid_template')).toHaveLength(1);
211
244
  });
212
245
 
213
- it('reports no errors for a valid source-theme catalog', async () => {
246
+ it('reports no errors for a valid source-theme descriptor', async () => {
214
247
  const pkgDir = path.join(tmpDir, 'pkg');
215
248
  writePackage(pkgDir, {
216
- manifest: `export default { themes: './themes' };\n`,
249
+ manifest: `export default { themes: './themes' };
250
+ `,
217
251
  });
218
252
  const themeDir = path.join(pkgDir, 'themes', 'ocean');
219
253
  fs.mkdirSync(themeDir, {recursive: true});
220
254
  fs.writeFileSync(
221
- path.join(pkgDir, 'themes', 'manifest.json'),
222
- JSON.stringify({
223
- version: 1,
224
- themes: [
225
- {
226
- slug: 'ocean',
227
- displayName: 'Ocean',
228
- description: 'Blue and calm.',
229
- maintained: true,
230
- entry: 'oceanTheme.ts',
231
- exportName: 'oceanTheme',
232
- files: ['oceanTheme.ts'],
233
- },
234
- ],
235
- }),
255
+ path.join(themeDir, 'oceanTheme.doc.mjs'),
256
+ `/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */
257
+ export default {type: 'theme', name: 'ocean', displayName: 'Ocean', description: 'Blue and calm.', maintained: true};
258
+ `,
236
259
  );
237
260
  fs.writeFileSync(
238
261
  path.join(themeDir, 'oceanTheme.ts'),
239
- `export const oceanTheme = {};\n`,
262
+ `export const oceanTheme = {};
263
+ `,
240
264
  );
241
265
 
242
266
  const result = await validateLocalIntegration(pkgDir);
@@ -244,13 +268,17 @@ describe('validate-integration API', () => {
244
268
  expect(summarizeIssues(result.issues).errors).toBe(0);
245
269
  });
246
270
 
247
- it('flags an unreadable source-theme catalog as invalid_theme', async () => {
271
+ it('flags an obsolete source-theme catalog as invalid_theme', async () => {
248
272
  const pkgDir = path.join(tmpDir, 'pkg');
249
273
  writePackage(pkgDir, {
250
- manifest: `export default { themes: './themes' };\n`,
274
+ manifest: `export default { themes: './themes' };
275
+ `,
251
276
  });
252
277
  fs.mkdirSync(path.join(pkgDir, 'themes'), {recursive: true});
253
- fs.writeFileSync(path.join(pkgDir, 'themes', 'manifest.json'), '{not-json');
278
+ fs.writeFileSync(
279
+ path.join(pkgDir, 'themes', 'manifest.json'),
280
+ '{"version":1}',
281
+ );
254
282
 
255
283
  const result = await validateLocalIntegration(pkgDir);
256
284
  expect(byCode(result.issues, 'invalid_theme')).toHaveLength(1);
@@ -275,6 +303,11 @@ describe('validate-integration API', () => {
275
303
  message: expect.stringContaining('InvisibleWidget.doc.mjs'),
276
304
  }),
277
305
  ]);
306
+ expect(
307
+ byCode(result.issues, 'source_without_component_doc')[0].message,
308
+ ).toContain(
309
+ "Fix: add InvisibleWidget.doc.mjs beside it with type: 'component'",
310
+ );
278
311
  });
279
312
 
280
313
  it('reports codemods outside semver folders and invalid folder names', async () => {
@@ -290,8 +323,12 @@ describe('validate-integration API', () => {
290
323
 
291
324
  const result = await validateLocalIntegration(pkgDir);
292
325
 
293
- expect(byCode(result.issues, 'codemod_outside_version')).toHaveLength(1);
294
- expect(byCode(result.issues, 'invalid_codemod_version')).toHaveLength(1);
326
+ const [stray] = byCode(result.issues, 'codemod_outside_version');
327
+ expect(stray.message).toContain('"codemods/forgotten.mjs"');
328
+ expect(stray.message).toContain('codemods/1.2.0/forgotten.mjs');
329
+ expect(stray.message).not.toContain(tmpDir);
330
+ const [folder] = byCode(result.issues, 'invalid_codemod_version');
331
+ expect(folder.message).toContain('Fix: rename it');
295
332
  });
296
333
 
297
334
  it('warns about valid contribution metadata outside every declared root', async () => {
@@ -333,6 +370,31 @@ describe('validate-integration API', () => {
333
370
  const unreachable = byCode(result.issues, 'unreachable_contribution');
334
371
  expect(unreachable).toHaveLength(1);
335
372
  expect(unreachable[0].message).toContain('src/orphan.doc.mjs');
373
+ // No docs root is declared, but src/ also holds a .doc.mjs that is not a
374
+ // topic, which a docs root at src/ would read too; so the fix moves it.
375
+ expect(unreachable[0].message).toContain(
376
+ "Fix: move it into docs/ and set `docs: './docs'` in astryx.integration.mjs.",
377
+ );
378
+ });
379
+
380
+ it('names the root that reads misplaced metadata when one is declared', async () => {
381
+ const pkgDir = path.join(tmpDir, 'pkg');
382
+ writePackage(pkgDir, {
383
+ manifest: `export default { templates: './templates' };\n`,
384
+ });
385
+ fs.mkdirSync(path.join(pkgDir, 'templates'));
386
+ fs.mkdirSync(path.join(pkgDir, 'src', 'blocks'), {recursive: true});
387
+ fs.writeFileSync(
388
+ path.join(pkgDir, 'src', 'blocks', 'Carousel.doc.mjs'),
389
+ `export default {type: 'block', name: 'Carousel', displayName: 'Carousel', aspectRatio: 1};\n`,
390
+ );
391
+
392
+ const result = await validateLocalIntegration(pkgDir);
393
+
394
+ const [issue] = byCode(result.issues, 'unreachable_contribution');
395
+ expect(issue.message).toContain(
396
+ "Fix: move it under templates/ (the templates root), or set `templates: './src/blocks'` in astryx.integration.mjs.",
397
+ );
336
398
  });
337
399
 
338
400
  it('never executes unreachable metadata while diagnosing it', async () => {
@@ -0,0 +1,110 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `doctor integration validate` warns about a folder under the themes
5
+ * root that holds modules but is not read as a theme, and both branches of
6
+ * its fix leave nothing to warn about.
7
+ *
8
+ * @input a local integration package with a themes root
9
+ * @output the unread_theme_folder warnings validation reports
10
+ * @position packages/cli/api/integration — validation regression tests
11
+ */
12
+
13
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import {validateLocalIntegration} from './validate-integration.mjs';
17
+ import {integrationAddTheme} from './add-theme.mjs';
18
+
19
+ let tmpDir;
20
+
21
+ beforeEach(() => {
22
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-unread-theme-'));
23
+ fs.writeFileSync(
24
+ path.join(tmpDir, 'package.json'),
25
+ JSON.stringify({name: '@acme/kit', version: '1.0.0'}),
26
+ );
27
+ fs.writeFileSync(
28
+ path.join(tmpDir, 'astryx.integration.mjs'),
29
+ "export default {themes: './themes'};\n",
30
+ );
31
+ });
32
+
33
+ afterEach(() => {
34
+ fs.rmSync(tmpDir, {recursive: true, force: true});
35
+ });
36
+
37
+ /** @param {Record<string, string>} files paths under themes/ */
38
+ function writeThemes(files) {
39
+ for (const [file, contents] of Object.entries(files)) {
40
+ const target = path.join(tmpDir, 'themes', file);
41
+ fs.mkdirSync(path.dirname(target), {recursive: true});
42
+ fs.writeFileSync(target, contents);
43
+ }
44
+ }
45
+
46
+ const OCEAN = {
47
+ 'ocean/oceanTheme.doc.mjs':
48
+ "/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */\nexport default {type: 'theme', name: 'ocean', displayName: 'Ocean', description: 'Blue.', maintained: true};\n",
49
+ 'ocean/oceanTheme.ts':
50
+ "import {blue} from '@acme/kit/themes/shared/palette';\nexport const oceanTheme = {blue};\n",
51
+ 'shared/palette.ts': 'export const blue = 1;\n',
52
+ };
53
+
54
+ async function unread() {
55
+ const {issues} = await validateLocalIntegration(tmpDir);
56
+ return issues.filter(issue => issue.code === 'unread_theme_folder');
57
+ }
58
+
59
+ describe('unread theme folders', () => {
60
+ it('warns about each folder that looks like a theme but is not read as one', async () => {
61
+ writeThemes({
62
+ ...OCEAN,
63
+ 'sand/sand.ts': 'export const sand = {};\n',
64
+ 'sand/sand.doc.ts': "export default {type: 'theme', name: 'sand'};\n",
65
+ 'reef/index.ts': 'export const reefTheme = {};\n',
66
+ 'Tide_Pool/index.ts': 'export const tide = {};\n',
67
+ 'fonts/inter.woff2': 'x',
68
+ 'utils/format.ts': 'export const format = String;\n',
69
+ '.cache/index.ts': 'export {};\n',
70
+ });
71
+ const issues = await unread();
72
+ expect(issues.map(issue => issue.severity)).toEqual([
73
+ 'warning',
74
+ 'warning',
75
+ 'warning',
76
+ ]);
77
+ expect(issues.map(issue => issue.message)).toEqual([
78
+ 'Folder "themes/Tide_Pool/" holds modules but no <name>Theme source or .doc.mjs descriptor, so Astryx does not read it as a theme. Fix: rename it to .Tide_Pool so Astryx skips it; a theme folder needs a lower-kebab name.',
79
+ 'Folder "themes/reef/" holds modules but no <name>Theme source or .doc.mjs descriptor, so Astryx does not read it as a theme. Fix: if it is a theme, add reefTheme.ts and reefTheme.doc.mjs to it (`astryx integration add theme reef` writes both) and move its code into reefTheme.ts; if not, rename it to .reef so Astryx skips it.',
80
+ 'Folder "themes/sand/" holds modules but no <name>Theme source or .doc.mjs descriptor, so Astryx does not read it as a theme. Fix: if it is a theme, add sandTheme.ts and sandTheme.doc.mjs to it (`astryx integration add theme sand` writes both) and move its code into sandTheme.ts; if not, rename it to .sand so Astryx skips it.',
81
+ ]);
82
+ });
83
+
84
+ it('does not warn about a helper folder a module imports by a relative path', async () => {
85
+ writeThemes({
86
+ ...OCEAN,
87
+ 'index.ts':
88
+ "export * from './ocean/oceanTheme';\nexport * from './util';\n",
89
+ 'util/index.ts': 'export const util = 1;\n',
90
+ });
91
+ expect(await unread()).toEqual([]);
92
+ });
93
+
94
+ it('holds when either branch of the fix is followed', async () => {
95
+ writeThemes({
96
+ ...OCEAN,
97
+ 'sand/sand.ts': 'export const sand = {};\n',
98
+ 'reef/index.ts': 'export const reefTheme = {};\n',
99
+ 'Tide_Pool/index.ts': 'export const tide = {};\n',
100
+ });
101
+ await integrationAddTheme('sand', {cwd: tmpDir});
102
+ await integrationAddTheme('reef', {cwd: tmpDir});
103
+ fs.renameSync(
104
+ path.join(tmpDir, 'themes', 'Tide_Pool'),
105
+ path.join(tmpDir, 'themes', '.Tide_Pool'),
106
+ );
107
+ const {issues} = await validateLocalIntegration(tmpDir);
108
+ expect(issues).toEqual([]);
109
+ });
110
+ });
@@ -12,6 +12,7 @@ export const doc = {
12
12
  type: 'function',
13
13
  kind: 'api',
14
14
  name: 'validateIntegration',
15
+ namespace: 'cli/api',
15
16
  displayName: 'validateIntegration()',
16
17
  summary:
17
18
  'Validate a single Astryx integration package and report its issues.',
@@ -11,6 +11,7 @@ export const doc = {
11
11
  type: 'function',
12
12
  kind: 'api',
13
13
  name: 'assertResponse',
14
+ namespace: 'cli/api',
14
15
  displayName: 'assertResponse()',
15
16
  summary:
16
17
  'Parse and assert one expected response type, throwing on an error or a mismatch.',
@@ -11,6 +11,7 @@ export const doc = {
11
11
  type: 'function',
12
12
  kind: 'api',
13
13
  name: 'isError',
14
+ namespace: 'cli/api',
14
15
  displayName: 'isError()',
15
16
  summary: 'Did the CLI return an error envelope?',
16
17
  description:
@@ -11,6 +11,7 @@ export const doc = {
11
11
  type: 'function',
12
12
  kind: 'api',
13
13
  name: 'parseResponse',
14
+ namespace: 'cli/api',
14
15
  displayName: 'parseResponse()',
15
16
  summary: 'Parse `astryx --json` stdout into the structural response envelope.',
16
17
  description:
@@ -12,6 +12,7 @@ export const doc = {
12
12
  type: 'function',
13
13
  kind: 'api',
14
14
  name: 'layoutCheck',
15
+ namespace: 'cli/api',
15
16
  displayName: 'layoutCheck()',
16
17
  summary: 'Validate a layout expression without expanding it.',
17
18
  description:
@@ -12,6 +12,7 @@ export const doc = {
12
12
  type: 'function',
13
13
  kind: 'api',
14
14
  name: 'layoutExpand',
15
+ namespace: 'cli/api',
15
16
  displayName: 'layoutExpand()',
16
17
  summary: 'Expand a validated layout expression into XDS TSX.',
17
18
  description:
@@ -12,6 +12,7 @@ export const doc = {
12
12
  type: 'function',
13
13
  kind: 'api',
14
14
  name: 'layoutGrammar',
15
+ namespace: 'cli/api',
15
16
  displayName: 'layoutGrammar()',
16
17
  summary: 'Return the XLE/XLO grammar cheatsheet for this install.',
17
18
  description:
@@ -11,6 +11,7 @@ export const doc = {
11
11
  type: 'function',
12
12
  kind: 'api',
13
13
  name: 'search',
14
+ namespace: 'cli/api',
14
15
  displayName: 'search()',
15
16
  summary:
16
17
  'Unified ranked search across components, hooks, docs, and templates.',
@@ -50,7 +50,7 @@
50
50
  * queries they have nothing to do with.
51
51
  */
52
52
 
53
- import {pathToFileURL} from 'node:url';
53
+ import {readDocView} from '../../foundation/doc-compiler/read.mjs';
54
54
  import {findCoreDir} from '../../foundation/fs/paths.mjs';
55
55
  import {
56
56
  discoverComponents,
@@ -528,16 +528,26 @@ export function scoreCandidate(
528
528
  }
529
529
 
530
530
  /**
531
- * Load a doc module's `docs`/`doc` export, swallowing errors.
531
+ * A component or hook doc, compiled, or null when it cannot be read.
532
532
  * @param {string} docPath
533
533
  * @param {string} [exportName]
534
+ * @param {'components' | 'hooks'} [root]
534
535
  * @returns {Promise<any>}
535
536
  */
536
- async function loadModuleDoc(docPath, exportName = 'docs') {
537
+ async function loadModuleDoc(
538
+ docPath,
539
+ exportName = 'docs',
540
+ root = 'components',
541
+ ) {
537
542
  try {
538
- const mod = await import(pathToFileURL(docPath).href);
539
543
  // Support both the stamped default export and the legacy named export.
540
- return mod?.default ?? mod[exportName] ?? null;
544
+ return (
545
+ (await readDocView(docPath, {
546
+ root,
547
+ loader: 'native',
548
+ exports: ['default', exportName],
549
+ })) ?? null
550
+ );
541
551
  } catch {
542
552
  return null;
543
553
  }
@@ -701,7 +711,7 @@ async function gatherHooks(coreDir) {
701
711
  let description = '';
702
712
  let importPath = '@astryxdesign/core/hooks';
703
713
  if (docPath) {
704
- const doc = await loadModuleDoc(docPath);
714
+ const doc = await loadModuleDoc(docPath, 'docs', 'hooks');
705
715
  if (doc) {
706
716
  keywords = Array.isArray(doc.keywords) ? doc.keywords : [];
707
717
  description = doc.usage?.description || doc.description || '';
@@ -11,6 +11,7 @@ export const doc = {
11
11
  type: 'function',
12
12
  kind: 'api',
13
13
  name: 'swizzle',
14
+ namespace: 'cli/api',
14
15
  displayName: 'swizzle()',
15
16
  summary: "Eject a component's source into your project for customization.",
16
17
  description: