@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
@@ -15,7 +15,7 @@
15
15
  */
16
16
  /**
17
17
  * Compute the files the tarball must contain for an integration's
18
- * contributions to be visible. Reads catalogs and walks directories;
18
+ * contributions to be visible. Walks descriptor-owned directories;
19
19
  * never imports or executes authored modules.
20
20
  *
21
21
  * @param {import('./integrations.mjs').LoadedIntegration} loaded
@@ -14,8 +14,11 @@
14
14
  import * as fs from 'node:fs';
15
15
  import * as path from 'node:path';
16
16
  import {isDeepStrictEqual} from 'node:util';
17
- import {THEME_MANIFEST_BASENAME} from '../discovery/theme-discovery.mjs';
18
- import {discoverIntegrationThemes} from '../discovery/theme-discovery.mjs';
17
+ import {
18
+ discoverIntegrationThemes,
19
+ isThemeFolder,
20
+ listThemeFiles,
21
+ } from '../discovery/theme-discovery.mjs';
19
22
  import {discoverIntegrationComponents} from '../discovery/component-discovery.mjs';
20
23
  import {loadComponentDoc} from '../discovery/component-loader.mjs';
21
24
  import {discoverIntegrationTemplatesForOne} from '../discovery/template-adapter.mjs';
@@ -65,27 +68,21 @@ function rel(packageDir, file) {
65
68
 
66
69
  /** @param {string} root @param {string} pkgDir */
67
70
  function enumerateThemeFiles(root, pkgDir) {
71
+ if (!root || !fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
72
+ return [];
73
+ }
74
+ // Exactly what `theme add` copies: each theme folder's files, including
75
+ // tests and fixtures, but no dot entry or file npm never publishes. Files
76
+ // beside the theme folders belong to no theme.
68
77
  /** @type {string[]} */
69
78
  const files = [];
70
- const catalog = path.join(root, THEME_MANIFEST_BASENAME);
71
- if (!fs.existsSync(catalog)) return files;
72
- files.push(rel(pkgDir, catalog));
73
- try {
74
- const data = JSON.parse(fs.readFileSync(catalog, 'utf-8'));
75
- if (data?.version !== 1 || !Array.isArray(data.themes)) return files;
76
- for (const theme of data.themes) {
77
- if (typeof theme.slug !== 'string' || !Array.isArray(theme.files))
78
- continue;
79
- for (const f of theme.files) {
80
- if (typeof f === 'string') {
81
- files.push(rel(pkgDir, path.join(root, theme.slug, f)));
82
- }
83
- }
79
+ for (const entry of fs.readdirSync(root, {withFileTypes: true})) {
80
+ const full = path.join(root, entry.name);
81
+ if (entry.isDirectory() && isThemeFolder(full)) {
82
+ files.push(...listThemeFiles(full).map(file => path.join(full, file)));
84
83
  }
85
- } catch {
86
- /* Phase 1 catches structural issues */
87
84
  }
88
- return files;
85
+ return files.sort().map(file => rel(pkgDir, file));
89
86
  }
90
87
 
91
88
  /** @param {string} root @param {string} pkgDir */
@@ -158,7 +155,7 @@ const ROOT_KINDS = /** @type {const} */ ([
158
155
 
159
156
  /**
160
157
  * Compute the files the tarball must contain for an integration's
161
- * contributions to be visible. Reads catalogs and walks directories;
158
+ * contributions to be visible. Walks descriptor-owned directories;
162
159
  * never imports or executes authored modules.
163
160
  *
164
161
  * @param {import('./integrations.mjs').LoadedIntegration} loaded
@@ -37,30 +37,37 @@ function loaded(overrides = {}) {
37
37
 
38
38
  function writeTheme(slug, files = [`${slug}Theme.ts`]) {
39
39
  const root = path.join(tmpDir, 'themes');
40
- fs.mkdirSync(path.join(root, slug), {recursive: true});
41
- for (const f of files) {
42
- fs.writeFileSync(path.join(root, slug, f), `// ${f}\n`);
40
+ const themeDir = path.join(root, slug);
41
+ fs.mkdirSync(themeDir, {recursive: true});
42
+ for (const file of files) {
43
+ const target = path.join(themeDir, file);
44
+ fs.mkdirSync(path.dirname(target), {recursive: true});
45
+ fs.writeFileSync(target, `// ${file}\n`);
43
46
  }
44
- const catalogPath = path.join(root, 'manifest.json');
45
- let catalog = {version: 1, themes: []};
46
- if (fs.existsSync(catalogPath)) {
47
- catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf-8'));
48
- }
49
- catalog.themes.push({
50
- slug,
51
- displayName: slug,
52
- description: `${slug} theme.`,
53
- maintained: true,
54
- entry: files[0],
55
- exportName: `${slug}Theme`,
56
- files,
57
- });
58
- fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2) + '\n');
47
+ fs.writeFileSync(
48
+ path.join(themeDir, `${slug}Theme.doc.mjs`),
49
+ `/** @type {import('@astryxdesign/cli/authoring').ThemeDoc} */\nexport default {type: 'theme', name: '${slug}', displayName: '${slug}', description: '${slug} theme.', maintained: true};\n`,
50
+ );
59
51
  return root;
60
52
  }
61
53
 
54
+ const UNPUBLISHED = [
55
+ '.DS_Store',
56
+ '._oceanTheme.ts',
57
+ '.gitignore',
58
+ '.npmignore',
59
+ '.npmrc',
60
+ 'oceanTheme.ts.orig',
61
+ 'npm-debug.log',
62
+ 'palette/.DS_Store',
63
+ 'palette/npm-debug.log',
64
+ '.git/HEAD',
65
+ 'CVS/Entries',
66
+ 'node_modules/dep/index.js',
67
+ ];
68
+
62
69
  describe('computeRequiredFiles', () => {
63
- it('enumerates manifest + theme catalog files', () => {
70
+ it('enumerates each complete descriptor-owned theme directory', () => {
64
71
  const root = writeTheme('ocean', ['oceanTheme.ts', 'tokens.ts']);
65
72
  const inv = computeRequiredFiles(loaded({themes: root}));
66
73
 
@@ -69,13 +76,46 @@ describe('computeRequiredFiles', () => {
69
76
  expect(inv.roots[0].kind).toBe('themes');
70
77
  expect(inv.roots[0].files).toEqual(
71
78
  expect.arrayContaining([
72
- 'themes/manifest.json',
79
+ 'themes/ocean/oceanTheme.doc.mjs',
73
80
  'themes/ocean/oceanTheme.ts',
74
81
  'themes/ocean/tokens.ts',
75
82
  ]),
76
83
  );
77
84
  expect(inv.allFiles).toContain('astryx.integration.mjs');
78
- expect(inv.allFiles).toContain('themes/manifest.json');
85
+ expect(inv.allFiles).toContain('themes/ocean/oceanTheme.doc.mjs');
86
+ expect(inv.allFiles).not.toContain('themes/manifest.json');
87
+ });
88
+
89
+ it('requires no file npm leaves out, at any depth, and none beside the themes', () => {
90
+ const root = writeTheme('ocean', ['oceanTheme.ts', 'palette/tokens.ts']);
91
+ for (const file of [
92
+ ...UNPUBLISHED.map(name => `ocean/${name}`),
93
+ 'README.md',
94
+ 'npm-debug.log',
95
+ ]) {
96
+ fs.mkdirSync(path.dirname(path.join(root, file)), {recursive: true});
97
+ fs.writeFileSync(path.join(root, file), 'x\n');
98
+ }
99
+ expect(computeRequiredFiles(loaded({themes: root})).roots[0].files).toEqual(
100
+ [
101
+ 'themes/ocean/oceanTheme.doc.mjs',
102
+ 'themes/ocean/oceanTheme.ts',
103
+ 'themes/ocean/palette/tokens.ts',
104
+ ],
105
+ );
106
+ });
107
+
108
+ it('requires only the folders theme discovery reads', () => {
109
+ const root = writeTheme('ocean');
110
+ for (const file of ['shared/palette.ts', '.cache/state.json']) {
111
+ fs.mkdirSync(path.dirname(path.join(root, file)), {recursive: true});
112
+ fs.writeFileSync(path.join(root, file), '{}\n');
113
+ }
114
+ const inv = computeRequiredFiles(loaded({themes: root}));
115
+ expect(inv.roots[0].files).toEqual([
116
+ 'themes/ocean/oceanTheme.doc.mjs',
117
+ 'themes/ocean/oceanTheme.ts',
118
+ ]);
79
119
  });
80
120
 
81
121
  it('enumerates paired component metadata and source files', () => {
@@ -139,31 +179,31 @@ describe('computeRequiredFiles', () => {
139
179
  const root = path.join(tmpDir, 'templates');
140
180
  fs.mkdirSync(path.join(root, 'dashboard'), {recursive: true});
141
181
  fs.writeFileSync(
142
- path.join(root, 'dashboard', 'dashboard.template.mjs'),
182
+ path.join(root, 'dashboard', 'dashboard.doc.mjs'),
143
183
  '// tmpl\n',
144
184
  );
145
185
  fs.writeFileSync(path.join(root, 'dashboard', 'dashboard.tsx'), '// src\n');
146
186
  const inv = computeRequiredFiles(loaded({templates: root}));
147
187
 
148
188
  expect(inv.roots[0].files).toContain(
149
- 'templates/dashboard/dashboard.template.mjs',
189
+ 'templates/dashboard/dashboard.doc.mjs',
150
190
  );
151
191
  expect(inv.roots[0].files).toContain('templates/dashboard/dashboard.tsx');
152
192
  });
153
193
 
154
- it('enumerates legacy .doc template metadata with paired sources', () => {
194
+ it('enumerates released .template compatibility metadata with paired sources', () => {
155
195
  const root = path.join(tmpDir, 'templates');
156
196
  fs.mkdirSync(path.join(root, 'dashboard'), {recursive: true});
157
197
  fs.writeFileSync(
158
- path.join(root, 'dashboard', 'dashboard.doc.mjs'),
159
- '// legacy template\n',
198
+ path.join(root, 'dashboard', 'dashboard.template.mjs'),
199
+ '// released compatibility template\n',
160
200
  );
161
201
  fs.writeFileSync(path.join(root, 'dashboard', 'dashboard.tsx'), '// src\n');
162
202
 
163
203
  const inv = computeRequiredFiles(loaded({templates: root}));
164
204
 
165
205
  expect(inv.roots[0].files).toEqual([
166
- 'templates/dashboard/dashboard.doc.mjs',
206
+ 'templates/dashboard/dashboard.template.mjs',
167
207
  'templates/dashboard/dashboard.tsx',
168
208
  ]);
169
209
  });
@@ -14,6 +14,9 @@
14
14
  * issues, Doctor, and the per-command warning name the one set aside instead of
15
15
  * dropping it without a word.
16
16
  *
17
+ * The conflict pass of {@link resolveProviders}, for a list that is already
18
+ * assembled.
19
+ *
17
20
  * @param {LoadedIntegration[]} integrations in precedence order
18
21
  * @returns {LoadedIntegration[]}
19
22
  */
@@ -29,6 +29,7 @@ import {
29
29
  } from '../../authoring/integration/schema.mjs';
30
30
  import {importUserModule, findPresentFiles} from '../fs/module-loader.mjs';
31
31
  import {parseGapReportHandler} from '../../authoring/gap-report/parse.mjs';
32
+ import {resolveProviders} from './provider-resolution.mjs';
32
33
 
33
34
  /**
34
35
  * A fully-resolved, loaded integration. Identity (`name`, `version`) comes from
@@ -116,107 +117,20 @@ function providerIdForPackage(name) {
116
117
  * issues, Doctor, and the per-command warning name the one set aside instead of
117
118
  * dropping it without a word.
118
119
  *
120
+ * The conflict pass of {@link resolveProviders}, for a list that is already
121
+ * assembled.
122
+ *
119
123
  * @param {LoadedIntegration[]} integrations in precedence order
120
124
  * @returns {LoadedIntegration[]}
121
125
  */
122
126
  export function markProviderConflicts(integrations) {
123
- /** @type {Map<string, LoadedIntegration>} */
124
- const claims = new Map();
125
- for (const integration of integrations) {
126
- if (integration?.__local && claimsProvider(integration)) {
127
- const providerId = /** @type {string} */ (integration.providerId);
128
- if (!claims.has(providerId)) claims.set(providerId, integration);
129
- }
130
- }
131
- /** @type {LoadedIntegration[]} */
132
- const resolved = [];
133
- for (const integration of integrations) {
134
- if (!claimsProvider(integration)) {
135
- resolved.push(integration);
136
- continue;
137
- }
138
- const providerId = /** @type {string} */ (integration.providerId);
139
- const claimant = claims.get(providerId);
140
- if (claimant == null) {
141
- claims.set(providerId, integration);
142
- resolved.push(integration);
143
- } else if (claimant === integration) {
144
- resolved.push(integration);
145
- } else if (
146
- claimant.name !== integration.name ||
147
- claimant.version !== integration.version
148
- ) {
149
- resolved.push(providerConflict(integration, claimant));
150
- }
151
- }
152
- return resolved;
153
- }
154
-
155
- /**
156
- * Whether an entry contributes under its provider ID: it has one, its manifest
157
- * loaded, and it has not already been set aside.
158
- * @param {LoadedIntegration | undefined} integration
159
- * @returns {boolean}
160
- */
161
- function claimsProvider(integration) {
162
- return (
163
- integration?.providerId != null &&
164
- integration.__loadError == null &&
165
- integration.__providerConflict == null
166
- );
167
- }
168
-
169
- /**
170
- * @param {LoadedIntegration} integration
171
- * @returns {string}
172
- */
173
- function describeIntegration(integration) {
174
- const id = integration.version
175
- ? `${integration.name}@${integration.version}`
176
- : integration.name;
177
- return integration.__spec && integration.__spec !== integration.name
178
- ? `${id} (from "${integration.__spec}")`
179
- : id;
180
- }
181
-
182
- /**
183
- * An inert record for a package whose provider ID is already claimed. It keeps
184
- * the package's identity and location for reporting, and drops every
185
- * contribution root and handler.
186
- * @param {LoadedIntegration} integration
187
- * @param {LoadedIntegration} claimant
188
- * @returns {LoadedIntegration}
189
- */
190
- function providerConflict(integration, claimant) {
191
- const providerId =
192
- /** @type {import('../../authoring/identity/type').ProviderId} */ (
193
- integration.providerId
194
- );
195
- const winner = describeIntegration(claimant);
196
- const setAside = describeIntegration(integration);
197
- const reason = claimant.__local
198
- ? 'is the package being authored, so it is used'
199
- : 'loads first and is used';
200
- return {
201
- name: integration.name,
202
- providerId,
203
- version: integration.version,
204
- __spec: integration.__spec,
205
- __packageDir: integration.__packageDir,
206
- __manifestFile: integration.__manifestFile,
207
- ...(integration.__local ? {__local: true} : {}),
208
- ...(integration.__autolinked
209
- ? {__autolinked: true, __dependencyField: integration.__dependencyField}
210
- : {}),
211
- __providerConflict: {
212
- providerId,
213
- claimedBy: claimant.name,
214
- message:
215
- `${setAside} and ${winner} both claim provider ID "${providerId}". ` +
216
- `${winner} ${reason}; ${setAside} contributes nothing ` +
217
- 'until one package changes its providerId.',
218
- },
219
- };
127
+ return resolveProviders(
128
+ integrations.map(integration => ({
129
+ source: /** @type {const} */ ('configured'),
130
+ integration,
131
+ spec: integration?.__spec,
132
+ })),
133
+ ).integrations;
220
134
  }
221
135
 
222
136
  /**
@@ -0,0 +1,275 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file The provider ledger property: over seeded, generated projects, the
5
+ * real Project.load and the real `upgrade` context account for every candidate
6
+ * exactly once, and agree on the winner of every provider ID except where the
7
+ * codemod policy says otherwise.
8
+ *
9
+ * The expected candidates come from the scenario description, never from the
10
+ * resolver. Fixtures live under a repo-local temp dir, not /tmp, because Vite
11
+ * refuses to dynamically import a module from outside the project root.
12
+ */
13
+
14
+ import {afterAll, beforeAll, describe, expect, it} from 'vitest';
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import {Project, providerLedgerOf} from '../config/project.mjs';
18
+ import {loadProjectContext} from '../../api/upgrade/_adapter.mjs';
19
+ import {
20
+ expectedProjectInputs,
21
+ generateScenario,
22
+ isRunnable,
23
+ writeScenario,
24
+ } from '../../test-utils/provider-scenarios.mjs';
25
+
26
+ // 200 seeds keep this near 4 s; any seed range must hold.
27
+ const SEEDS = Array.from({length: 200}, (_, index) => index + 1);
28
+ const OUTCOMES = ['contributes', 'set-aside', 'load-failed', 'alias-of'];
29
+
30
+ /** @type {string} */
31
+ let root;
32
+ /** @type {string} */
33
+ let originalCwd;
34
+
35
+ beforeAll(() => {
36
+ originalCwd = process.cwd();
37
+ root = fs.mkdtempSync(path.join(process.cwd(), '.astryx-provider-ledger-'));
38
+ });
39
+
40
+ afterAll(() => {
41
+ process.chdir(originalCwd);
42
+ fs.rmSync(root, {recursive: true, force: true});
43
+ });
44
+
45
+ /**
46
+ * The contributing package for each provider ID. Fails when two contribute
47
+ * under one ID.
48
+ * @param {Map<string, any>} ledger
49
+ * @returns {Map<string, any>}
50
+ */
51
+ function winners(ledger) {
52
+ /** @type {Map<string, any>} */
53
+ const byProvider = new Map();
54
+ for (const entry of ledger.values()) {
55
+ if (entry.outcome !== 'contributes' || entry.providerId == null) continue;
56
+ expect(byProvider.has(entry.providerId)).toBe(false);
57
+ byProvider.set(entry.providerId, entry);
58
+ }
59
+ return byProvider;
60
+ }
61
+
62
+ /**
63
+ * Assert the ledger records one outcome for exactly the given inputs.
64
+ * @param {Map<string, any>} ledger
65
+ * @param {Array<{source: string, spec: string, key: string}>} inputs
66
+ */
67
+ function expectAccounted(ledger, inputs) {
68
+ const refs = [...ledger.values()].flatMap(entry =>
69
+ entry.refs.map(ref => ({entry, ref})),
70
+ );
71
+ for (const input of inputs) {
72
+ const holders = refs.filter(
73
+ ({ref}) => ref.source === input.source && ref.spec === input.spec,
74
+ );
75
+ expect(holders.map(({entry}) => entry.key)).toEqual([input.key]);
76
+ }
77
+ expect(refs).toHaveLength(inputs.length);
78
+ expect(new Set(ledger.keys())).toEqual(
79
+ new Set(inputs.map(input => input.key)),
80
+ );
81
+ for (const [key, entry] of ledger) {
82
+ expect(entry.key).toBe(key);
83
+ expect(OUTCOMES).toContain(entry.outcome);
84
+ if (entry.outcome === 'set-aside') {
85
+ expect(ledger.has(entry.winner)).toBe(true);
86
+ expect(entry.reason).toEqual(expect.any(String));
87
+ }
88
+ if (entry.outcome === 'alias-of') {
89
+ expect(ledger.has(entry.aliasOf)).toBe(true);
90
+ }
91
+ }
92
+ }
93
+
94
+ describe('provider ledger over generated projects', () => {
95
+ it('accounts for every candidate once and agrees across Project and upgrade', async () => {
96
+ const coverage = {
97
+ scenarios: 0,
98
+ setAside: 0,
99
+ aliasOf: 0,
100
+ loadFailed: 0,
101
+ replaced: 0,
102
+ agreed: 0,
103
+ standIn: 0,
104
+ extraReordered: 0,
105
+ };
106
+
107
+ for (const seed of SEEDS) {
108
+ const scenario = generateScenario(seed);
109
+ if (!isRunnable(scenario)) continue;
110
+ coverage.scenarios++;
111
+ const dir = path.join(root, `seed-${seed}`);
112
+ fs.mkdirSync(dir);
113
+ writeScenario(dir, scenario);
114
+ const context = `seed ${seed}: ${JSON.stringify(scenario)}`;
115
+
116
+ const project = await Project.load(dir, {fresh: true});
117
+ const ledger = providerLedgerOf(project);
118
+ const inputs = expectedProjectInputs(dir, scenario);
119
+ try {
120
+ expectAccounted(ledger, inputs);
121
+
122
+ // Nothing leaves the list without a recorded reason.
123
+ const listed = new Set(project.loadedIntegrations);
124
+ for (const integration of project.loadedIntegrations) {
125
+ expect(ledger.has(fs.realpathSync(integration.__packageDir))).toBe(
126
+ true,
127
+ );
128
+ }
129
+ for (const entry of ledger.values()) {
130
+ if (entry.integration != null) {
131
+ expect(listed.has(entry.integration)).toBe(true);
132
+ continue;
133
+ }
134
+ expect(entry.outcome).not.toBe('contributes');
135
+ if (entry.outcome === 'set-aside') {
136
+ expect(entry.reason).toBe('replaced-by-authored');
137
+ }
138
+ if (entry.outcome === 'load-failed') {
139
+ expect(entry.error).toEqual(expect.any(String));
140
+ }
141
+ }
142
+ } catch (error) {
143
+ throw new Error(`${context}\n${error}`, {cause: error});
144
+ }
145
+
146
+ for (const entry of ledger.values()) {
147
+ if (entry.outcome === 'set-aside') coverage.setAside++;
148
+ if (entry.outcome === 'alias-of') coverage.aliasOf++;
149
+ if (entry.outcome === 'load-failed') coverage.loadFailed++;
150
+ if (entry.reason === 'replaced-by-authored') coverage.replaced++;
151
+ }
152
+
153
+ // `--integration` specs resolve from the process cwd.
154
+ process.chdir(dir);
155
+ const upgrade = await loadProjectContext(dir, scenario.extras);
156
+ process.chdir(originalCwd);
157
+
158
+ const modules = path.join(dir, 'node_modules');
159
+ /** @param {string} spec */
160
+ const installedKey = spec =>
161
+ fs.realpathSync(path.join(modules, ...spec.split('/')));
162
+ /** @param {string} spec */
163
+ const installedName = spec =>
164
+ JSON.parse(
165
+ fs.readFileSync(
166
+ path.join(modules, ...spec.split('/'), 'package.json'),
167
+ 'utf8',
168
+ ),
169
+ ).name;
170
+ const configured = new Set(scenario.config ?? []);
171
+ const extras = [...new Set(scenario.extras)].filter(
172
+ spec => !configured.has(spec),
173
+ );
174
+ const {local} = scenario;
175
+ const localKey = fs.realpathSync(dir);
176
+ const selfListed = local != null && configured.has(local.name);
177
+ // The codemod policy: the installed copy stands in for the package being
178
+ // authored when it lists itself or is named with --integration.
179
+ const localStandsIn =
180
+ local != null &&
181
+ (selfListed || extras.some(spec => installedName(spec) === local.name));
182
+
183
+ try {
184
+ // Upgrade accounts for everything Project did, plus its own inputs.
185
+ expectAccounted(upgrade.ledger, [
186
+ ...inputs,
187
+ ...(selfListed
188
+ ? [
189
+ {
190
+ source: 'installed',
191
+ spec: local.name,
192
+ key: installedKey(local.name),
193
+ },
194
+ ]
195
+ : []),
196
+ ...extras.map(spec => ({
197
+ source: 'extra',
198
+ spec,
199
+ key: installedKey(spec),
200
+ })),
201
+ ]);
202
+ } catch (error) {
203
+ throw new Error(`${context}\n${error}`, {cause: error});
204
+ }
205
+
206
+ const projectWinners = winners(ledger);
207
+ const upgradeWinners = winners(upgrade.ledger);
208
+ for (const providerId of new Set([
209
+ ...projectWinners.keys(),
210
+ ...upgradeWinners.keys(),
211
+ ])) {
212
+ const inProject = projectWinners.get(providerId);
213
+ const inUpgrade = upgradeWinners.get(providerId);
214
+ const same =
215
+ inProject != null &&
216
+ inUpgrade != null &&
217
+ (inProject.key === inUpgrade.key ||
218
+ upgrade.ledger.get(inProject.key)?.aliasOf === inUpgrade.key);
219
+ if (same) {
220
+ coverage.agreed++;
221
+ continue;
222
+ }
223
+ // The one documented exception, asserted explicitly: an installed
224
+ // copy took the authored package's place, so either upgrade's winner
225
+ // is an installed copy of that package, or Project's winner is the
226
+ // authored package it replaced.
227
+ const upgradeName =
228
+ inUpgrade?.integration?.name ??
229
+ inUpgrade?.candidate.integration?.name;
230
+ const standInWins =
231
+ localStandsIn && inUpgrade != null && upgradeName === local?.name;
232
+ const localReplaced =
233
+ localStandsIn && inProject != null && inProject.key === localKey;
234
+ // Project never saw an extra, so an ID only an extra claims has no
235
+ // Project winner to agree with.
236
+ const extraOnly =
237
+ inProject == null && inUpgrade?.candidate.source === 'extra';
238
+ // Known disagreement, pinned by the `it.fails` repros in
239
+ // api/upgrade/provider-agreement.test.mjs: upgrade replaces an
240
+ // autolinked winner with any extra of the same name@version, from
241
+ // any directory, and orders it with the extras. Another extra can
242
+ // then win, or nothing does, in upgrade only.
243
+ const projectWinnerInUpgrade =
244
+ inProject == null ? undefined : upgrade.ledger.get(inProject.key);
245
+ const repeatedByExtra =
246
+ projectWinnerInUpgrade != null &&
247
+ (projectWinnerInUpgrade.refs.some(ref => ref.source === 'extra') ||
248
+ (projectWinnerInUpgrade.outcome === 'alias-of' &&
249
+ upgrade.ledger
250
+ .get(projectWinnerInUpgrade.aliasOf)
251
+ ?.refs.some(ref => ref.source === 'extra')));
252
+ const extraReordered =
253
+ inProject?.candidate.source === 'autolinked' && repeatedByExtra;
254
+ if (extraReordered) {
255
+ coverage.extraReordered++;
256
+ continue;
257
+ }
258
+ expect(
259
+ standInWins || localReplaced || extraOnly,
260
+ `${context}\nprovider ${providerId}: Project ${inProject?.label ?? 'none'} vs upgrade ${inUpgrade?.label ?? 'none'}`,
261
+ ).toBe(true);
262
+ if (standInWins || localReplaced) coverage.standIn++;
263
+ }
264
+ }
265
+
266
+ // The generator must keep reaching every shape, or the property is vacuous.
267
+ expect(coverage.scenarios).toBeGreaterThanOrEqual(190);
268
+ expect(coverage.setAside).toBeGreaterThan(0);
269
+ expect(coverage.aliasOf).toBeGreaterThan(0);
270
+ expect(coverage.loadFailed).toBeGreaterThan(0);
271
+ expect(coverage.replaced).toBeGreaterThan(0);
272
+ expect(coverage.agreed).toBeGreaterThan(0);
273
+ expect(coverage.standIn).toBeGreaterThan(0);
274
+ }, 120_000);
275
+ });