@astryxdesign/cli 0.6.4-canary.ed2e54e → 0.6.4

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 (145) hide show
  1. package/README.md +66 -64
  2. package/api/component/_adapter.d.mts +0 -25
  3. package/api/component/_adapter.mjs +5 -59
  4. package/api/component/component.d.mts +3 -6
  5. package/api/component/component.doc.mjs +10 -23
  6. package/api/component/component.mjs +9 -249
  7. package/api/component/component.type.d.mts +0 -25
  8. package/api/component/component.type.mjs +0 -44
  9. package/api/discover/_adapter.d.mts +6 -114
  10. package/api/discover/_adapter.mjs +17 -372
  11. package/api/discover/detail/detail.d.mts +6 -18
  12. package/api/discover/detail/detail.mjs +13 -67
  13. package/api/discover/detail/detail.test.mjs +0 -85
  14. package/api/discover/discover.d.mts +9 -3
  15. package/api/discover/discover.doc.mjs +18 -61
  16. package/api/discover/discover.mjs +36 -220
  17. package/api/discover/discover.test.mjs +2 -11
  18. package/api/discover/discover.type.d.mts +8 -147
  19. package/api/discover/discover.type.mjs +12 -102
  20. package/api/discover/list/list.d.mts +6 -20
  21. package/api/discover/list/list.mjs +12 -45
  22. package/api/discover/list/list.test.mjs +0 -46
  23. package/api/discover/search/search.d.mts +16 -18
  24. package/api/discover/search/search.mjs +56 -102
  25. package/api/discover/search/search.test.mjs +10 -144
  26. package/api/docs/docs.test.mjs +0 -2
  27. package/api/doctor/doctor.d.mts +3 -8
  28. package/api/doctor/doctor.mjs +9 -90
  29. package/api/doctor/doctor.test.mjs +10 -122
  30. package/api/index.d.mts +2 -1
  31. package/api/index.mjs +4 -4
  32. package/api/integration/add-helpers.d.mts +2 -5
  33. package/api/integration/add-helpers.mjs +9 -36
  34. package/api/integration/pack-check.mjs +3 -28
  35. package/api/json/index.ts +1 -0
  36. package/api/layout/_adapter.d.mts +34 -0
  37. package/api/layout/_adapter.mjs +148 -0
  38. package/api/layout/check/check.d.mts +16 -0
  39. package/api/layout/check/check.mjs +40 -0
  40. package/api/layout/expand/expand.d.mts +22 -0
  41. package/api/layout/expand/expand.mjs +155 -0
  42. package/api/layout/expand/expand.path-safety.test.mjs +53 -0
  43. package/api/layout/grammar/grammar.d.mts +13 -0
  44. package/api/layout/grammar/grammar.mjs +87 -0
  45. package/api/layout/layout.d.mts +6 -0
  46. package/api/layout/layout.mjs +17 -0
  47. package/api/layout/layout.test.mjs +297 -0
  48. package/api/layout/layout.type.d.mts +89 -0
  49. package/api/layout/layout.type.mjs +103 -0
  50. package/api/layout/layoutCheck.doc.d.mts +11 -0
  51. package/api/layout/layoutCheck.doc.mjs +85 -0
  52. package/api/layout/layoutExpand.doc.d.mts +11 -0
  53. package/api/layout/layoutExpand.doc.mjs +107 -0
  54. package/api/layout/layoutGrammar.doc.d.mts +11 -0
  55. package/api/layout/layoutGrammar.doc.mjs +57 -0
  56. package/api/search/search.test.mjs +0 -18
  57. package/api/template/template-integration.test.mjs +65 -1
  58. package/api/template/template.mjs +1 -1
  59. package/api/theme/add/add.mjs +25 -17
  60. package/api/theme/add/add.staging.test.mjs +23 -40
  61. package/api/theme/build/build.family.test.mjs +12 -7
  62. package/api/theme/build/build.mjs +18 -8
  63. package/api/upgrade/run/run.mjs +4 -6
  64. package/api/upgrade/upgrade.type.mjs +2 -2
  65. package/assets/codemods/__tests__/runner.test.mjs +1 -3
  66. package/assets/codemods/integration-runner.mjs +3 -3
  67. package/assets/codemods/runner.mjs +4 -5
  68. package/assets/docs/internationalization.doc.mjs +5 -7
  69. package/assets/docs/tree/integrations.doc.mjs +1 -20
  70. package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
  71. package/authoring/config/config.doc.mjs +1 -9
  72. package/authoring/config/parse.d.mts +0 -2
  73. package/authoring/config/parse.mjs +0 -19
  74. package/authoring/config/parse.test.mjs +0 -8
  75. package/authoring/config/type.ts +2 -13
  76. package/authoring/doctypes/command/command.doc.mjs +1 -1
  77. package/authoring/doctypes/command/type.ts +1 -1
  78. package/authoring/index.d.mts +0 -1
  79. package/authoring/index.d.ts +0 -10
  80. package/authoring/index.mjs +0 -1
  81. package/clients/cli/command-result-coverage.test.mjs +7 -7
  82. package/clients/cli/commands/component/index.mjs +55 -152
  83. package/clients/cli/commands/component-ownership.test.mjs +0 -89
  84. package/clients/cli/commands/component.doc.mjs +6 -23
  85. package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
  86. package/clients/cli/commands/discover.doc.mjs +9 -53
  87. package/clients/cli/commands/discover.mjs +118 -393
  88. package/clients/cli/commands/docs.test.mjs +0 -29
  89. package/clients/cli/commands/layout-check.doc.mjs +65 -0
  90. package/clients/cli/commands/layout-expand.doc.mjs +83 -0
  91. package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
  92. package/clients/cli/commands/layout.doc.mjs +34 -0
  93. package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
  94. package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
  95. package/clients/cli/commands/layout.mjs +275 -0
  96. package/clients/cli/commands/layout.path-help.test.mjs +33 -0
  97. package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
  98. package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
  99. package/clients/cli/commands/text-json-parity.test.mjs +17 -0
  100. package/clients/cli/index.mjs +4 -0
  101. package/clients/cli/lib/exit-codes.test.mjs +8 -1
  102. package/clients/cli/lib/json-shim.mjs +14 -24
  103. package/clients/cli/lib/json-shim.test.mjs +20 -6
  104. package/clients/cli/lib/manifest.mjs +8 -3
  105. package/clients/cli/lib/manifest.test.mjs +2 -5
  106. package/foundation/discovery/authoring-self-docs.mjs +0 -1
  107. package/foundation/discovery/template-adapter.mjs +1 -1
  108. package/foundation/doc-compiler/doc-loads.test.mjs +12 -0
  109. package/foundation/doc-compiler/tree.test.mjs +1 -9
  110. package/foundation/integrations/integrations.d.mts +1 -14
  111. package/foundation/integrations/integrations.mjs +1 -41
  112. package/foundation/integrations/integrations.test.mjs +0 -31
  113. package/foundation/response/response-types.doc.mjs +21 -15
  114. package/foundation/response/response-types.doc.test.mjs +0 -23
  115. package/foundation/xle/browser.d.mts +3 -3
  116. package/foundation/xle/browser.mjs +3 -3
  117. package/foundation/xle/expand.mjs +2 -2
  118. package/foundation/xle/parse.mjs +1 -1
  119. package/foundation/xle/print.mjs +2 -2
  120. package/foundation/xle/splice.mjs +1 -1
  121. package/package.json +9 -9
  122. package/api/discover/_adapter.test.mjs +0 -215
  123. package/api/discover/_catalog-view.d.mts +0 -115
  124. package/api/discover/_catalog-view.mjs +0 -203
  125. package/api/discover/_catalog-view.test.mjs +0 -128
  126. package/api/discover/detail/item/item.d.mts +0 -26
  127. package/api/discover/detail/item/item.mjs +0 -78
  128. package/api/discover/detail/item/item.test.mjs +0 -73
  129. package/api/integration/pack-check.lifecycle-output.test.mjs +0 -105
  130. package/api/theme/add/add.rollback.test.mjs +0 -158
  131. package/api/theme/build/build.rollback.test.mjs +0 -148
  132. package/api/upgrade/run/files-changed.test.mjs +0 -111
  133. package/assets/codemods/file-count.test.mjs +0 -163
  134. package/assets/docs/tree/component-lookups.doc.mjs +0 -149
  135. package/authoring/discover/discover.doc.d.mts +0 -13
  136. package/authoring/discover/discover.doc.mjs +0 -138
  137. package/authoring/discover/parse.d.mts +0 -24
  138. package/authoring/discover/parse.mjs +0 -128
  139. package/authoring/discover/parse.test.mjs +0 -124
  140. package/authoring/discover/type.ts +0 -87
  141. package/clients/cli/commands/component-batch.test.mjs +0 -341
  142. package/clients/cli/commands/discover.sources.test.mjs +0 -267
  143. package/clients/cli/lib/parse-error-format.test.mjs +0 -81
  144. package/foundation/response/batch.type.d.mts +0 -33
  145. package/foundation/response/batch.type.mjs +0 -34
@@ -354,31 +354,16 @@ describe('checkImplicitIntegrations', () => {
354
354
  });
355
355
 
356
356
  it('names the package, the field, and what it contributes', () => {
357
- // Roots count only when they exist, so this one gives them real folders.
358
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-implicit-'));
359
- try {
360
- const dir = name => {
361
- fs.mkdirSync(path.join(root, name));
362
- return path.join(root, name);
363
- };
364
- const c = checkImplicitIntegrations({
365
- integrations: [
366
- autolinked({
367
- components: dir('components'),
368
- templates: dir('templates'),
369
- themes: dir('themes'),
370
- }),
371
- ],
372
- });
373
- expect(c.message).toContain('@acme/widgets@1.0.0');
374
- expect(c.message).toContain('from dependencies');
375
- expect(c.message).toContain(
376
- 'contributing components, templates, themes',
377
- );
378
- expect(c.message).not.toContain('missing on disk');
379
- } finally {
380
- fs.rmSync(root, {recursive: true, force: true});
381
- }
357
+ const c = checkImplicitIntegrations({
358
+ integrations: [
359
+ autolinked({templates: '/abs/templates', themes: '/abs/themes'}),
360
+ ],
361
+ });
362
+ expect(c.message).toContain('@acme/widgets@1.0.0');
363
+ expect(c.message).toContain('from dependencies');
364
+ expect(c.message).toContain(
365
+ 'contributing components, templates, themes',
366
+ );
382
367
  });
383
368
 
384
369
  it('names the declared key too when an npm alias makes them differ', () => {
@@ -921,100 +906,3 @@ describe('checkDocsProgressiveDisclosure languages', () => {
921
906
  expect(c.message).not.toMatch(/deploying overview:/);
922
907
  });
923
908
  });
924
-
925
- describe('doctor says what it could not check', () => {
926
- const CORE = {
927
- 'node_modules/@astryxdesign/core/package.json': JSON.stringify({
928
- name: '@astryxdesign/core',
929
- version: '0.6.3',
930
- }),
931
- };
932
-
933
- /** Installed dependency whose manifest declares roots that do not exist. */
934
- const dangling = {
935
- 'node_modules/@acme/dangling/package.json': JSON.stringify({
936
- name: '@acme/dangling',
937
- version: '2.0.0',
938
- }),
939
- 'node_modules/@acme/dangling/astryx.integration.mjs':
940
- "export default {providerId: 'acme-dangling', components: './components', templates: './templates', docs: './docs'};\n",
941
- };
942
-
943
- /** Installed dependency whose manifest cannot be parsed at all. */
944
- const broken = {
945
- 'node_modules/@acme/broken/package.json': JSON.stringify({
946
- name: '@acme/broken',
947
- version: '1.0.0',
948
- }),
949
- 'node_modules/@acme/broken/astryx.integration.mjs':
950
- 'export default { this is not valid javascript ((\n',
951
- };
952
-
953
- /** @param {Record<string, string>} extra @param {string[]} deps */
954
- const project = (extra, deps) =>
955
- mkProject({
956
- 'package.json': JSON.stringify({
957
- name: 'consumer',
958
- version: '1.0.0',
959
- dependencies: Object.fromEntries(
960
- ['@astryxdesign/core', ...deps].map(d => [d, '1.0.0']),
961
- ),
962
- }),
963
- ...CORE,
964
- ...extra,
965
- });
966
-
967
- // An unparseable manifest is kept out of the loaded set on purpose, and
968
- // doctor used to report that no installed dependency ships a manifest.
969
- it('names an installed dependency whose manifest cannot be loaded', async () => {
970
- const report = (await doctor({cwd: project(broken, ['@acme/broken'])})).data;
971
- const check = report.checks.find(c => c.id === 'implicit-integrations');
972
-
973
- expect(check.status).toBe('info');
974
- expect(check.message).toContain('@acme/broken');
975
- expect(check.message).toContain('could not be loaded');
976
- expect(check.message).not.toContain('no installed dependency ships');
977
- expect(report.summary.fail).toBe(0);
978
- }, SLOW);
979
-
980
- it('does not claim contributions from roots that are missing on disk', async () => {
981
- const report = (await doctor({cwd: project(dangling, ['@acme/dangling'])}))
982
- .data;
983
- const implicit = report.checks.find(c => c.id === 'implicit-integrations');
984
- const issues = report.checks.find(c => c.id === 'integration-issues');
985
-
986
- expect(implicit.message).toContain('contributing nothing');
987
- expect(implicit.message).toContain('missing on disk');
988
- expect(implicit.message).not.toContain('contributing components');
989
- expect(issues.status).toBe('warn');
990
- expect(issues.message).toContain('@acme/dangling');
991
- }, SLOW);
992
-
993
- it('still says no dependency ships a manifest when none does', async () => {
994
- const report = (await doctor({cwd: project({}, [])})).data;
995
- const check = report.checks.find(c => c.id === 'implicit-integrations');
996
-
997
- expect(check.message).toBe(
998
- 'None — no installed dependency ships an astryx.integration.* manifest.',
999
- );
1000
- }, SLOW);
1001
-
1002
- it('says how many integrations could not be read, rather than counting silently', () => {
1003
- /** @type {any} */
1004
- const ctx = {
1005
- cwd: '/x',
1006
- nodeVersion: process.versions.node,
1007
- coreDir: null,
1008
- configPath: null,
1009
- configTheme: null,
1010
- integrations: [
1011
- {name: '@acme/ok', __spec: '@acme/ok', providerId: 'ok'},
1012
- {name: '@acme/bad', __spec: '@acme/bad', __loadError: 'boom'},
1013
- ],
1014
- };
1015
- const check = checkProviderIdentity(ctx);
1016
-
1017
- expect(check.message).toContain('1 loaded integration has its own provider ID.');
1018
- expect(check.message).toContain('could not be read');
1019
- });
1020
- });
package/api/index.d.mts CHANGED
@@ -18,7 +18,6 @@ export { integrationAddTheme } from "./integration/add-theme.mjs";
18
18
  export { integrationPackCheck } from "./integration/pack-check.mjs";
19
19
  export { AstryxError } from "./error.mjs";
20
20
  export { logger } from "./logger.mjs";
21
- export * from "../foundation/response/batch.type.mjs";
22
21
  export * from "./component/component.type.mjs";
23
22
  export * from "./docs/docs.type.mjs";
24
23
  export * from "./blog/blog.type.mjs";
@@ -33,12 +32,14 @@ export * from "./gap-report/gap-report.type.mjs";
33
32
  export * from "./upgrade/upgrade.type.mjs";
34
33
  export * from "./init/init.type.mjs";
35
34
  export * from "./doctor/doctor.type.mjs";
35
+ export * from "./layout/layout.type.mjs";
36
36
  export * from "./integration/integration-authoring.type.mjs";
37
37
  export * from "./integration/pack-check.type.mjs";
38
38
  export * from "./integration/validate-integration.type.mjs";
39
39
  export * from "./integration/authoring-checks.type.mjs";
40
40
  export type Logger = import("./logger.mjs").Logger;
41
41
  export { themeBuild, themeAdd, themeTemplate, themeList, themeListAvailable, themeTargets, themePaletteGenerate, generateTonalPalette, listThemes } from "./theme/theme.mjs";
42
+ export { layoutExpand, layoutCheck, layoutGrammar } from "./layout/layout.mjs";
42
43
  export { integrationAdd, integrationAddAgentDoc, integrationAddCodemod, integrationAddComponent, integrationAddDoc, integrationAddTemplate } from "./integration/add-contribution.mjs";
43
44
  export { validateIntegration, summarizeIssues } from "./integration/validate-integration.mjs";
44
45
  export { integrationTemplateConflicts, integrationComponentConflicts, integrationDocConflicts } from "./integration/authoring-checks.mjs";
package/api/index.mjs CHANGED
@@ -44,6 +44,7 @@ export {gapReport} from './gap-report/gap-report.mjs';
44
44
  export {upgrade} from './upgrade/upgrade.mjs';
45
45
  export {init} from './init/init.mjs';
46
46
  export {doctor} from './doctor/doctor.mjs';
47
+ export {layoutExpand, layoutCheck, layoutGrammar} from './layout/layout.mjs';
47
48
  export {
48
49
  integrationAdd,
49
50
  integrationAddAgentDoc,
@@ -72,12 +73,10 @@ export {logger} from './logger.mjs';
72
73
  * @typedef {import('./logger.mjs').Logger} Logger
73
74
  */
74
75
 
75
- // ── Types (re-exported from the shared response foundation and each command's
76
- // colocated `.type.mjs`) ──────────────────────────────────────────────────
76
+ // ── Types (re-exported from each command's colocated `.type.mjs`) ─────
77
77
  // Runtime no-ops (the .type.mjs files are `export {}`); tsc carries these
78
78
  // through to the generated api/index.d.mts so the public type surface exposes
79
- // the shared receipt vocabulary and every command's Options + response types.
80
- export * from '../foundation/response/batch.type.mjs';
79
+ // every command's Options + response types by name.
81
80
  export * from './component/component.type.mjs';
82
81
  export * from './docs/docs.type.mjs';
83
82
  export * from './blog/blog.type.mjs';
@@ -92,6 +91,7 @@ export * from './gap-report/gap-report.type.mjs';
92
91
  export * from './upgrade/upgrade.type.mjs';
93
92
  export * from './init/init.type.mjs';
94
93
  export * from './doctor/doctor.type.mjs';
94
+ export * from './layout/layout.type.mjs';
95
95
  export * from './integration/integration-authoring.type.mjs';
96
96
  export * from './integration/pack-check.type.mjs';
97
97
  export * from './integration/validate-integration.type.mjs';
@@ -8,7 +8,7 @@ export function findPackageDir(startDir: string): string;
8
8
  /**
9
9
  * @typedef {object} WritePlan
10
10
  * @property {string} path
11
- * @property {string | Buffer} contents a Buffer is written byte for byte
11
+ * @property {string} contents
12
12
  * @property {boolean} createOnly
13
13
  * @property {Buffer} [expectedOriginal] bytes captured before validation;
14
14
  * a different current file is a concurrent edit and must never be overwritten
@@ -56,10 +56,7 @@ export function packageJsonUpdate(packageFile: string, rootPath: string, manifes
56
56
  export function applyWrites(plans: WritePlan[]): () => void;
57
57
  export type WritePlan = {
58
58
  path: string;
59
- /**
60
- * a Buffer is written byte for byte
61
- */
62
- contents: string | Buffer;
59
+ contents: string;
63
60
  createOnly: boolean;
64
61
  /**
65
62
  * bytes captured before validation;
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * Hosts the atomic staged-write transaction, package.json files-array
7
7
  * maintenance, package-dir resolution, and project-path normalization that
8
- * every `integration add <kind>` command needs. `theme add` and `theme build`
9
- * write through the same transaction. Stateless and side-effect-free
8
+ * every `integration add <kind>` command needs. Stateless and side-effect-free
10
9
  * outside of {@link applyWrites}.
11
10
  */
12
11
 
@@ -41,7 +40,7 @@ export function findPackageDir(startDir) {
41
40
  /**
42
41
  * @typedef {object} WritePlan
43
42
  * @property {string} path
44
- * @property {string | Buffer} contents a Buffer is written byte for byte
43
+ * @property {string} contents
45
44
  * @property {boolean} createOnly
46
45
  * @property {Buffer} [expectedOriginal] bytes captured before validation;
47
46
  * a different current file is a concurrent edit and must never be overwritten
@@ -193,29 +192,21 @@ export function packageJsonUpdate(
193
192
 
194
193
  // ── Atomic staged-write transaction ─────────────────────────────────
195
194
 
196
- /**
197
- * @param {string} file
198
- * @returns {boolean} false when the file is still there
199
- */
195
+ /** @param {string} file */
200
196
  function removeTemporary(file) {
201
197
  try {
202
198
  fs.rmSync(file, {force: true});
203
- return true;
204
199
  } catch {
205
200
  // Best effort. The transaction error remains the actionable failure.
206
- return false;
207
201
  }
208
202
  }
209
203
 
210
204
  /**
211
205
  * Restore writes that already published. Best-effort so callers preserve
212
- * the original actionable error; returns every path it could not put back.
206
+ * the original actionable error.
213
207
  * @param {Array<WritePlan & {temporary: string, original: Buffer|null, mode: number}>} published
214
- * @returns {string[]}
215
208
  */
216
209
  function rollbackWrites(published) {
217
- /** @type {string[]} */
218
- const unrestored = [];
219
210
  for (const plan of [...published].reverse()) {
220
211
  let restore = null;
221
212
  try {
@@ -234,18 +225,12 @@ function rollbackWrites(published) {
234
225
  fs.renameSync(restore, plan.path);
235
226
  restore = null;
236
227
  }
237
- } catch (error) {
238
- // A created file that is already gone needs nothing. Any other failure
239
- // leaves this call's bytes, or no bytes, where the original was.
240
- const gone =
241
- error instanceof Error &&
242
- /** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT';
243
- if (!(gone && plan.original == null)) unrestored.push(plan.path);
228
+ } catch {
229
+ // Best effort. A concurrent edit belongs to its writer, not this rollback.
244
230
  } finally {
245
231
  if (restore != null) removeTemporary(restore);
246
232
  }
247
233
  }
248
- return unrestored;
249
234
  }
250
235
 
251
236
  /** @param {string} file */
@@ -340,22 +325,10 @@ export function applyWrites(plans) {
340
325
  }
341
326
  published.push(plan);
342
327
  }
343
- return () => {
344
- rollbackWrites(published);
345
- };
328
+ return () => rollbackWrites(published);
346
329
  } catch (error) {
347
- const leftovers = staged
348
- .filter(plan => !removeTemporary(plan.temporary))
349
- .map(plan => plan.temporary);
350
- const unrestored = rollbackWrites(published);
351
- if (error instanceof Error) {
352
- if (unrestored.length > 0) {
353
- error.message += ` Could not restore: ${unrestored.join(', ')}.`;
354
- }
355
- if (leftovers.length > 0) {
356
- error.message += ` Could not remove temporary files: ${leftovers.join(', ')}.`;
357
- }
358
- }
330
+ for (const plan of staged) removeTemporary(plan.temporary);
331
+ rollbackWrites(published);
359
332
  throw error;
360
333
  }
361
334
  }
@@ -162,22 +162,6 @@ export function parseNpmPackOutput(output) {
162
162
  };
163
163
  }
164
164
 
165
- /**
166
- * Summarize the JSON error npm prints on stdout when `--json` pack fails.
167
- * @param {string} output
168
- * @returns {string}
169
- */
170
- function npmPackErrorDetail(output) {
171
- try {
172
- const error = JSON.parse(output)?.error;
173
- return [error?.summary, error?.detail]
174
- .filter(part => typeof part === 'string' && part.trim() !== '')
175
- .join(': ');
176
- } catch {
177
- return (output || '').trim();
178
- }
179
- }
180
-
181
165
  /**
182
166
  * Run `npm pack --json` with output directed to `destDir` so no preexisting
183
167
  * tgz is overwritten. This intentionally runs the package lifecycle, matching
@@ -191,25 +175,16 @@ function npmPackErrorDetail(output) {
191
175
  function runNpmPack(packageDir, destDir) {
192
176
  const result = spawnSync(
193
177
  'npm',
194
- [
195
- 'pack',
196
- '--json',
197
- '--silent',
198
- // Lifecycle scripts still run; background mode keeps their output off
199
- // the stdout that carries the JSON result.
200
- '--foreground-scripts=false',
201
- `--pack-destination=${destDir}`,
202
- ],
178
+ ['pack', '--json', '--silent', `--pack-destination=${destDir}`],
203
179
  {cwd: packageDir, encoding: 'utf-8', timeout: 60_000},
204
180
  );
205
181
  if (result.error) {
206
182
  throw new Error(`Could not start npm pack: ${result.error.message}`);
207
183
  }
208
184
  if (result.status !== 0) {
209
- const detail =
210
- (result.stderr || '').trim() || npmPackErrorDetail(result.stdout);
185
+ const stderr = (result.stderr || '').trim();
211
186
  throw new Error(
212
- `npm pack failed (exit ${result.status})${detail ? `: ${detail}` : ''}.`,
187
+ `npm pack failed (exit ${result.status})${stderr ? `: ${stderr}` : ''}.`,
213
188
  );
214
189
  }
215
190
  return parseNpmPackOutput(result.stdout);
package/api/json/index.ts CHANGED
@@ -26,6 +26,7 @@ export type * from '../gap-report/gap-report.type.mjs';
26
26
  export type * from '../upgrade/upgrade.type.mjs';
27
27
  export type * from '../init/init.type.mjs';
28
28
  export type * from '../doctor/doctor.type.mjs';
29
+ export type * from '../layout/layout.type.mjs';
29
30
  export type * from '../integration/validate-integration.type.mjs';
30
31
  export type * from '../integration/authoring-checks.type.mjs';
31
32
  export type * from '../integration/pack-check.type.mjs';
@@ -0,0 +1,34 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /** @param {import('../../foundation/xle/xle-ast').RawIssue} issue */
5
+ export function formatIssue(issue: import("../../foundation/xle/xle-ast").RawIssue): string;
6
+ /**
7
+ * Parse + validate, throwing structured XDSErrors on failure.
8
+ * Returns {doc, registry, blocks, warnings}.
9
+ *
10
+ * @param {string} expression
11
+ * @param {{form?: 'compact'|'outline'|'auto', loose?: boolean, cwd?: string}} [options]
12
+ */
13
+ export function analyze(expression: string, { form, loose, cwd }?: {
14
+ form?: "compact" | "outline" | "auto";
15
+ loose?: boolean;
16
+ cwd?: string;
17
+ }): Promise<{
18
+ doc: import("../../foundation/xle/xle-ast").XLEDoc;
19
+ registry: import("../../foundation/xle/xle-ast").Registry;
20
+ blocks: LayoutBlock[];
21
+ errors: import("../../foundation/xle/xle-ast").RawIssue[];
22
+ warnings: import("../../foundation/xle/xle-ast").RawIssue[];
23
+ }>;
24
+ export type LayoutBlock = {
25
+ dirName: string;
26
+ name: string;
27
+ kind: "template" | "component";
28
+ type?: string | undefined;
29
+ description?: string | undefined;
30
+ category?: string | undefined;
31
+ importPath?: string | undefined;
32
+ filePath?: string | undefined;
33
+ isDefault?: boolean | undefined;
34
+ };
@@ -0,0 +1,148 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Shared analysis layer for the `layout` command's expand/check leaves.
5
+ *
6
+ * Loads the block catalog (template blocks + app-registered components), builds
7
+ * the branch registry, and parses+validates a layout expression into a doc +
8
+ * issues. Both the expand and check leaves sit on `analyze`; neither parses or
9
+ * validates directly. (The grammar leaf needs none of this — it only reads the
10
+ * registry alias table.)
11
+ *
12
+ * @input expression string (+ options)
13
+ * @output analyze() -> {doc, registry, blocks, errors, warnings}; formatIssue()
14
+ * @position api — shared core over lib/xle; leaves in expand/ + check/ project it
15
+ */
16
+
17
+ import {AstryxError} from '../error.mjs';
18
+ import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
19
+ import {parse, XLEParseError} from '../../foundation/xle/parse.mjs';
20
+ import {validate} from '../../foundation/xle/validate.mjs';
21
+ import {buildRegistry} from '../../foundation/xle/registry.mjs';
22
+ import {discoverTemplates} from '../template/template.mjs';
23
+ import {Project} from '../../foundation/config/project.mjs';
24
+
25
+ /**
26
+ * @typedef {object} LayoutBlock
27
+ * @property {string} dirName
28
+ * @property {string} name
29
+ * @property {'template'|'component'} kind
30
+ * @property {string} [type]
31
+ * @property {string} [description]
32
+ * @property {string} [category]
33
+ * @property {string} [importPath]
34
+ * @property {string} [filePath]
35
+ * @property {boolean} [isDefault]
36
+ */
37
+
38
+ /**
39
+ * The catalog a `{hint}` can resolve to: template blocks (spliced inline) plus
40
+ * any app-registered local components from astryx.config.mjs
41
+ * `experimental.xle.components` (imported by name). App components are how XLE
42
+ * reaches domain pieces — the KpiCard/chart/drawer set that the
43
+ * @astryxdesign/core registry can't see.
44
+ *
45
+ * @param {string} cwd
46
+ * @returns {Promise<LayoutBlock[]>}
47
+ */
48
+ async function loadBlocks(cwd) {
49
+ /** @type {LayoutBlock[]} */
50
+ const blocks = [];
51
+ try {
52
+ const all = await discoverTemplates(cwd);
53
+ const templates = all.filter(template => template.type === 'block');
54
+ /** @type {Map<string, import('../../foundation/discovery/template-adapter.mjs').DiscoveredTemplate>} */
55
+ const byId = new Map();
56
+ // Exact ids are the fallback. Active replacement aliases override them in a
57
+ // second pass, matching template() regardless of discovery/display order.
58
+ for (const template of templates) byId.set(template.dirName, template);
59
+ for (const template of templates) {
60
+ if (template.replaces != null) byId.set(template.replaces, template);
61
+ }
62
+ for (const [id, template] of byId) {
63
+ blocks.push({...template, dirName: id, kind: 'template'});
64
+ }
65
+ } catch {
66
+ // discovery is best-effort
67
+ }
68
+ try {
69
+ const project = await Project.load(cwd);
70
+ /** @type {Record<string, {from?: string, description?: string, default?: boolean}>} */
71
+ const components = project.config.experimental?.xle?.components ?? {};
72
+ for (const [name, spec] of Object.entries(components)) {
73
+ const importPath = spec.from;
74
+ if (!importPath) continue;
75
+ blocks.push({
76
+ type: 'block',
77
+ kind: 'component',
78
+ dirName: name,
79
+ name,
80
+ description: spec.description ?? '',
81
+ category: 'app',
82
+ importPath,
83
+ isDefault: Boolean(spec.default),
84
+ });
85
+ }
86
+ } catch {
87
+ // config is optional
88
+ }
89
+ return blocks;
90
+ }
91
+
92
+ /** @param {import('../../foundation/xle/xle-ast').RawIssue} issue */
93
+ export function formatIssue(issue) {
94
+ const where = issue.line != null ? `line ${issue.line}: ` : '';
95
+ return `${where}${issue.message}`;
96
+ }
97
+
98
+ /**
99
+ * Parse + validate, throwing structured XDSErrors on failure.
100
+ * Returns {doc, registry, blocks, warnings}.
101
+ *
102
+ * @param {string} expression
103
+ * @param {{form?: 'compact'|'outline'|'auto', loose?: boolean, cwd?: string}} [options]
104
+ */
105
+ export async function analyze(
106
+ expression,
107
+ {form = 'auto', loose = false, cwd = process.cwd()} = {},
108
+ ) {
109
+ // Validate inputs in the API (not just the CLI): an empty expression or an
110
+ // unknown --form must error, not silently parse as an empty/compact layout.
111
+ if (typeof expression !== 'string' || expression.trim() === '') {
112
+ throw new AstryxError(
113
+ 'Layout expression is empty.',
114
+ undefined,
115
+ ERROR_CODES.ERR_INVALID_ARGUMENT,
116
+ );
117
+ }
118
+ if (form !== 'compact' && form !== 'outline' && form !== 'auto') {
119
+ throw new AstryxError(
120
+ `Invalid form "${form}". Must be one of: compact, outline, auto.`,
121
+ undefined,
122
+ ERROR_CODES.ERR_INVALID_OPTION,
123
+ );
124
+ }
125
+ const registry =
126
+ /** @type {import('../../foundation/xle/xle-ast').Registry} */ (
127
+ /** @type {unknown} */ (await buildRegistry({cwd}))
128
+ );
129
+ const blocks = await loadBlocks(cwd);
130
+
131
+ /** @type {import('../../foundation/xle/xle-ast').XLEDoc} */
132
+ let doc;
133
+ try {
134
+ doc = parse(expression, {form});
135
+ } catch (e) {
136
+ if (e instanceof XLEParseError) {
137
+ throw new AstryxError(
138
+ `Layout expression syntax error at line ${e.line}, col ${e.col}: ${e.message}`,
139
+ undefined,
140
+ ERROR_CODES.ERR_LAYOUT_PARSE,
141
+ );
142
+ }
143
+ throw e;
144
+ }
145
+
146
+ const {errors, warnings} = validate(doc, registry, blocks, {loose});
147
+ return {doc, registry, blocks, errors, warnings};
148
+ }
@@ -0,0 +1,16 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * `astryx layout check "<expr>" [--form compact|outline]`
6
+ * Validates without expanding; echoes both canonical surfaces.
7
+ *
8
+ * @param {string} expression
9
+ * @param {{form?: 'compact'|'outline'|'auto', loose?: boolean, cwd?: string}} [options]
10
+ * @returns {Promise<import('../layout.type.mjs').LayoutCheckResponse>}
11
+ */
12
+ export function layoutCheck(expression: string, options?: {
13
+ form?: "compact" | "outline" | "auto";
14
+ loose?: boolean;
15
+ cwd?: string;
16
+ }): Promise<import("../layout.type.mjs").LayoutCheckResponse>;
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file `astryx layout check` leaf — validate a layout expression without
5
+ * expanding, echoing both canonical surfaces (compact + outline). Resolution +
6
+ * validation come from ../_adapter.mjs (analyze); this leaf projects the
7
+ * `layout.check` envelope.
8
+ *
9
+ * @input expression string (+ options)
10
+ * @output {type:'layout.check', data}
11
+ * @position api — leaf over ../_adapter.mjs + lib/xle/print
12
+ */
13
+
14
+ import {toCompact, toOutline} from '../../../foundation/xle/print.mjs';
15
+ import {analyze, formatIssue} from '../_adapter.mjs';
16
+
17
+ /**
18
+ * `astryx layout check "<expr>" [--form compact|outline]`
19
+ * Validates without expanding; echoes both canonical surfaces.
20
+ *
21
+ * @param {string} expression
22
+ * @param {{form?: 'compact'|'outline'|'auto', loose?: boolean, cwd?: string}} [options]
23
+ * @returns {Promise<import('../layout.type.mjs').LayoutCheckResponse>}
24
+ */
25
+ export async function layoutCheck(expression, options = {}) {
26
+ const {form = 'auto', loose = false, cwd = process.cwd()} = options;
27
+ const {doc, errors, warnings} = await analyze(expression, {form, loose, cwd});
28
+
29
+ return {
30
+ type: 'layout.check',
31
+ data: {
32
+ valid: errors.length === 0,
33
+ form: doc.form,
34
+ errors: errors.map(e => ({...e, formatted: formatIssue(e)})),
35
+ warnings: warnings.map(formatIssue),
36
+ compact: toCompact(doc),
37
+ outline: toOutline(doc),
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,22 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * `astryx layout expand "<expr>" [path]`
6
+ *
7
+ * @param {string} expression
8
+ * @param {object} [options]
9
+ * @param {string} [options.targetPath] - write TSX here (validated against cwd)
10
+ * @param {'compact'|'outline'|'auto'} [options.form]
11
+ * @param {boolean} [options.loose] - downgrade unknown {hints} to TODO warnings
12
+ * @param {string} [options.name] - generated component name
13
+ * @param {string} [options.cwd]
14
+ * @returns {Promise<import('../layout.type.mjs').LayoutExpandResponse>}
15
+ */
16
+ export function layoutExpand(expression: string, options?: {
17
+ targetPath?: string | undefined;
18
+ form?: "compact" | "auto" | "outline" | undefined;
19
+ loose?: boolean | undefined;
20
+ name?: string | undefined;
21
+ cwd?: string | undefined;
22
+ }): Promise<import("../layout.type.mjs").LayoutExpandResponse>;