@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
@@ -0,0 +1,89 @@
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
+ * The input surface an expression was parsed as.
6
+ */
7
+ export type LayoutForm = "compact" | "outline" | "auto";
8
+ /**
9
+ * A validation issue with its formatted, human-readable rendering.
10
+ */
11
+ export type LayoutIssue = {
12
+ line?: number | undefined;
13
+ col?: number | undefined;
14
+ message: string;
15
+ formatted: string;
16
+ suggestions?: string[] | undefined;
17
+ };
18
+ /**
19
+ * A block referenced by a layout expression and how it was spliced in.
20
+ */
21
+ export type LayoutBlockReference = {
22
+ name: string;
23
+ mode: string;
24
+ };
25
+ /**
26
+ * astryx --json layout expand "<expr>" [path]
27
+ */
28
+ export type LayoutExpandResponse = {
29
+ type: "layout.expand";
30
+ data: {
31
+ form: LayoutForm;
32
+ code: string;
33
+ componentsUsed: string[];
34
+ states: number;
35
+ todos: string[];
36
+ blocksReferenced: LayoutBlockReference[];
37
+ warnings: string[];
38
+ written: string | null;
39
+ };
40
+ };
41
+ /**
42
+ * astryx --json layout check "<expr>"
43
+ */
44
+ export type LayoutCheckResponse = {
45
+ type: "layout.check";
46
+ data: {
47
+ valid: boolean;
48
+ form: LayoutForm;
49
+ errors: LayoutIssue[];
50
+ warnings: string[];
51
+ compact: string;
52
+ outline: string;
53
+ };
54
+ };
55
+ /**
56
+ * astryx --json layout grammar
57
+ */
58
+ export type LayoutGrammarResponse = {
59
+ type: "layout.grammar";
60
+ data: {
61
+ text: string;
62
+ aliases: Record<string, string>;
63
+ };
64
+ };
65
+ export type LayoutResponse = LayoutExpandResponse | LayoutCheckResponse | LayoutGrammarResponse;
66
+ /**
67
+ * Options for `layoutExpand()`.
68
+ */
69
+ export type LayoutExpandOptions = {
70
+ targetPath?: string | undefined;
71
+ form?: LayoutForm | undefined;
72
+ loose?: boolean | undefined;
73
+ name?: string | undefined;
74
+ cwd?: string | undefined;
75
+ };
76
+ /**
77
+ * Options for `layoutCheck()`.
78
+ */
79
+ export type LayoutCheckOptions = {
80
+ form?: LayoutForm | undefined;
81
+ loose?: boolean | undefined;
82
+ cwd?: string | undefined;
83
+ };
84
+ /**
85
+ * Options for `layoutGrammar()`.
86
+ */
87
+ export type LayoutGrammarOptions = {
88
+ cwd?: string | undefined;
89
+ };
@@ -0,0 +1,103 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Colocated types for the `layout` command — source of truth for its JSON
5
+ * responses. `astryx layout expand|check|grammar` turn compressed XLE/XLO
6
+ * expressions into validated XDS TSX (or echo canonical surfaces / a grammar
7
+ * cheatsheet). The `types/layout.d.ts` barrel re-exports from here.
8
+ *
9
+ * Invocation -> type discriminator
10
+ * -----------------------------------------------------------------
11
+ * astryx --json layout expand "<expr>" -> layout.expand
12
+ * astryx --json layout check "<expr>" -> layout.check
13
+ * astryx --json layout grammar -> layout.grammar
14
+ */
15
+
16
+ /**
17
+ * The input surface an expression was parsed as.
18
+ * @typedef {'compact' | 'outline' | 'auto'} LayoutForm
19
+ */
20
+
21
+ /**
22
+ * A validation issue with its formatted, human-readable rendering.
23
+ * @typedef {object} LayoutIssue
24
+ * @property {number} [line]
25
+ * @property {number} [col]
26
+ * @property {string} message
27
+ * @property {string} formatted
28
+ * @property {string[]} [suggestions]
29
+ */
30
+
31
+ /**
32
+ * A block referenced by a layout expression and how it was spliced in.
33
+ * @typedef {object} LayoutBlockReference
34
+ * @property {string} name
35
+ * @property {string} mode
36
+ */
37
+
38
+ /**
39
+ * astryx --json layout expand "<expr>" [path]
40
+ * @typedef {object} LayoutExpandResponse
41
+ * @property {'layout.expand'} type
42
+ * @property {object} data
43
+ * @property {LayoutForm} data.form
44
+ * @property {string} data.code
45
+ * @property {string[]} data.componentsUsed
46
+ * @property {number} data.states Number of useState hooks the expansion scaffolded.
47
+ * @property {string[]} data.todos
48
+ * @property {LayoutBlockReference[]} data.blocksReferenced
49
+ * @property {string[]} data.warnings
50
+ * @property {string | null} data.written
51
+ */
52
+
53
+ /**
54
+ * astryx --json layout check "<expr>"
55
+ * @typedef {object} LayoutCheckResponse
56
+ * @property {'layout.check'} type
57
+ * @property {object} data
58
+ * @property {boolean} data.valid
59
+ * @property {LayoutForm} data.form
60
+ * @property {LayoutIssue[]} data.errors
61
+ * @property {string[]} data.warnings
62
+ * @property {string} data.compact
63
+ * @property {string} data.outline
64
+ */
65
+
66
+ /**
67
+ * astryx --json layout grammar
68
+ * @typedef {object} LayoutGrammarResponse
69
+ * @property {'layout.grammar'} type
70
+ * @property {object} data
71
+ * @property {string} data.text
72
+ * @property {Record<string, string>} data.aliases Alias table (short name -> canonical component), from the registry.
73
+ */
74
+
75
+ /**
76
+ * @typedef {LayoutExpandResponse | LayoutCheckResponse | LayoutGrammarResponse} LayoutResponse
77
+ */
78
+
79
+ /**
80
+ * Options for `layoutExpand()`.
81
+ * @typedef {object} LayoutExpandOptions
82
+ * @property {string} [targetPath]
83
+ * @property {LayoutForm} [form]
84
+ * @property {boolean} [loose]
85
+ * @property {string} [name]
86
+ * @property {string} [cwd]
87
+ */
88
+
89
+ /**
90
+ * Options for `layoutCheck()`.
91
+ * @typedef {object} LayoutCheckOptions
92
+ * @property {LayoutForm} [form]
93
+ * @property {boolean} [loose]
94
+ * @property {string} [cwd]
95
+ */
96
+
97
+ /**
98
+ * Options for `layoutGrammar()`.
99
+ * @typedef {object} LayoutGrammarOptions
100
+ * @property {string} [cwd]
101
+ */
102
+
103
+ export {};
@@ -0,0 +1,11 @@
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
+ * @file FunctionDoc for `layoutCheck()` / `astryx layout check`. Colocated with
6
+ * the API function it documents; the response-shape source of truth stays in
7
+ * `layout.type.mjs`.
8
+ * @position packages/cli/api/layout — function documentation
9
+ */
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc: import("@astryxdesign/cli/authoring").FunctionDoc;
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file FunctionDoc for `layoutCheck()` / `astryx layout check`. Colocated with
5
+ * the API function it documents; the response-shape source of truth stays in
6
+ * `layout.type.mjs`.
7
+ * @position packages/cli/api/layout — function documentation
8
+ */
9
+
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc = {
12
+ type: 'function',
13
+ kind: 'api',
14
+ name: 'layoutCheck',
15
+ namespace: 'cli/api',
16
+ displayName: 'layoutCheck()',
17
+ summary: 'Validate a layout expression without expanding it.',
18
+ description:
19
+ 'The validator behind `astryx layout check`. Parses and validates a compressed XLE/XLO ' +
20
+ 'expression without generating any TSX, and echoes it back in both canonical surfaces ' +
21
+ '(compact and outline). Validation failures are reported in the layout.check envelope ' +
22
+ '(valid: false) with line/col and suggestions (not thrown) so callers can lint an ' +
23
+ 'expression and surface fixes.',
24
+ importPath: '@astryxdesign/cli/api',
25
+ signature:
26
+ 'layoutCheck(expression: string, options?: LayoutCheckOptions): Promise<LayoutCheckResponse>',
27
+ keywords: ['layout', 'check', 'validate', 'xle', 'xlo', 'lint'],
28
+ params: [
29
+ {
30
+ name: 'expression',
31
+ type: 'string',
32
+ description: 'The layout expression to validate.',
33
+ required: true,
34
+ },
35
+ {
36
+ name: 'options.form',
37
+ type: "'compact' | 'outline' | 'auto'",
38
+ description:
39
+ 'Force which input surface the expression is parsed as, or auto-detect it.',
40
+ default: "'auto'",
41
+ },
42
+ {
43
+ name: 'options.loose',
44
+ type: 'boolean',
45
+ description:
46
+ 'Treat unknown {hint} references as warnings instead of errors.',
47
+ default: 'false',
48
+ },
49
+ {
50
+ name: 'options.cwd',
51
+ type: 'string',
52
+ description: 'Directory the block catalog and registry resolve against.',
53
+ },
54
+ ],
55
+ returns: [
56
+ {
57
+ type: 'layout.check',
58
+ description:
59
+ 'The validation result: a valid flag, the detected form, errors (each with line/col, message, formatted text, and suggestions), warnings, and the expression re-printed in both canonical surfaces (compact and outline).',
60
+ },
61
+ ],
62
+ throws: [
63
+ {code: 'ERR_INVALID_ARGUMENT', when: 'the expression is empty'},
64
+ {
65
+ code: 'ERR_INVALID_OPTION',
66
+ when: 'form is not one of "compact", "outline", or "auto"',
67
+ },
68
+ {
69
+ code: 'ERR_LAYOUT_PARSE',
70
+ when: 'the expression has a syntax error (reported with line/col)',
71
+ },
72
+ ],
73
+ examples: [
74
+ {
75
+ label: 'Validate an expression',
76
+ code: 'const r = await layoutCheck(\'VStack[g4] > Text"Hi"\');',
77
+ },
78
+ {
79
+ label: 'Read the canonical forms',
80
+ code: 'const {data} = await layoutCheck(\'Card > Text"Hi"\');',
81
+ },
82
+ ],
83
+ command: 'layout check',
84
+ related: ['layoutExpand', 'layoutGrammar'],
85
+ };
@@ -0,0 +1,11 @@
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
+ * @file FunctionDoc for `layoutExpand()` / `astryx layout expand`. Colocated
6
+ * with the API function it documents; the response-shape source of truth stays
7
+ * in `layout.type.mjs`.
8
+ * @position packages/cli/api/layout — function documentation
9
+ */
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc: import("@astryxdesign/cli/authoring").FunctionDoc;
@@ -0,0 +1,107 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file FunctionDoc for `layoutExpand()` / `astryx layout expand`. Colocated
5
+ * with the API function it documents; the response-shape source of truth stays
6
+ * in `layout.type.mjs`.
7
+ * @position packages/cli/api/layout — function documentation
8
+ */
9
+
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc = {
12
+ type: 'function',
13
+ kind: 'api',
14
+ name: 'layoutExpand',
15
+ namespace: 'cli/api',
16
+ displayName: 'layoutExpand()',
17
+ summary: 'Expand a validated layout expression into XDS TSX.',
18
+ description:
19
+ 'The generator behind `astryx layout expand`. Parses and validates a compressed XLE/XLO ' +
20
+ 'expression, then expands it into ready-to-use XDS TSX, auto-routing structural children ' +
21
+ 'into the right slots, scaffolding typed useState for interactive controls, and splicing or ' +
22
+ 'importing any referenced template blocks. Returns the code (and metadata) in a layout.expand ' +
23
+ 'envelope, optionally writing it to a path within cwd.',
24
+ importPath: '@astryxdesign/cli/api',
25
+ signature:
26
+ 'layoutExpand(expression: string, options?: LayoutExpandOptions): Promise<LayoutExpandResponse>',
27
+ keywords: ['layout', 'expand', 'xle', 'xlo', 'tsx', 'scaffold', 'generate'],
28
+ params: [
29
+ {
30
+ name: 'expression',
31
+ type: 'string',
32
+ description:
33
+ 'The layout expression to expand (XLE compact or XLO outline form).',
34
+ required: true,
35
+ },
36
+ {
37
+ name: 'options.targetPath',
38
+ type: 'string',
39
+ description:
40
+ 'Write the generated TSX here (validated to stay within cwd). A path that ends in .tsx, .ts, .jsx, .js, .mjs, .cjs, .css, .scss, .json, .md or .html is used as-is; any other path is a directory that gets <name>.tsx. An existing file is replaced. Omit to return the code without writing.',
41
+ },
42
+ {
43
+ name: 'options.form',
44
+ type: "'compact' | 'outline' | 'auto'",
45
+ description:
46
+ 'Force which input surface the expression is parsed as, or auto-detect it.',
47
+ default: "'auto'",
48
+ },
49
+ {
50
+ name: 'options.loose',
51
+ type: 'boolean',
52
+ description:
53
+ 'Downgrade unknown {hint} references to TODO warnings instead of hard errors.',
54
+ default: 'false',
55
+ },
56
+ {
57
+ name: 'options.name',
58
+ type: 'string',
59
+ description: 'PascalCase name for the generated component.',
60
+ default: "'GeneratedLayout'",
61
+ },
62
+ {
63
+ name: 'options.cwd',
64
+ type: 'string',
65
+ description:
66
+ 'Directory the block catalog, registry, and target path resolve against.',
67
+ },
68
+ ],
69
+ returns: [
70
+ {
71
+ type: 'layout.expand',
72
+ description:
73
+ 'The expansion: the parsed form, the generated TSX code, componentsUsed, states (count of useState hooks scaffolded), todos, blocksReferenced (each {name, mode}), warnings, and written (the relative output path, or null when nothing was written).',
74
+ },
75
+ ],
76
+ throws: [
77
+ {
78
+ code: 'ERR_INVALID_ARGUMENT',
79
+ when: 'the expression is empty, or name is not a PascalCase identifier',
80
+ },
81
+ {
82
+ code: 'ERR_INVALID_OPTION',
83
+ when: 'form is not one of "compact", "outline", or "auto"',
84
+ },
85
+ {
86
+ code: 'ERR_LAYOUT_PARSE',
87
+ when: 'the expression has a syntax error (reported with line/col)',
88
+ },
89
+ {
90
+ code: 'ERR_LAYOUT_INVALID',
91
+ when: 'the expression parses but fails validation (unknown component/prop/enum/block)',
92
+ },
93
+ {code: 'ERR_PATH_TRAVERSAL', when: 'the target path escapes cwd'},
94
+ ],
95
+ examples: [
96
+ {
97
+ label: 'Expand to TSX',
98
+ code: 'const r = await layoutExpand(\'VStack[g4] > Heading"Title" + Text"Body"\');',
99
+ },
100
+ {
101
+ label: 'Write to a file',
102
+ code: "await layoutExpand('Card > Text\"Hi\"', {targetPath: 'src/Generated.tsx', name: 'Generated'});",
103
+ },
104
+ ],
105
+ command: 'layout expand',
106
+ related: ['layoutCheck', 'layoutGrammar'],
107
+ };
@@ -0,0 +1,11 @@
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
+ * @file FunctionDoc for `layoutGrammar()` / `astryx layout grammar`. Colocated
6
+ * with the API function it documents; the response-shape source of truth stays
7
+ * in `layout.type.mjs`.
8
+ * @position packages/cli/api/layout — function documentation
9
+ */
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc: import("@astryxdesign/cli/authoring").FunctionDoc;
@@ -0,0 +1,57 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file FunctionDoc for `layoutGrammar()` / `astryx layout grammar`. Colocated
5
+ * with the API function it documents; the response-shape source of truth stays
6
+ * in `layout.type.mjs`.
7
+ * @position packages/cli/api/layout — function documentation
8
+ */
9
+
10
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
11
+ export const doc = {
12
+ type: 'function',
13
+ kind: 'api',
14
+ name: 'layoutGrammar',
15
+ namespace: 'cli/api',
16
+ displayName: 'layoutGrammar()',
17
+ summary: 'Return the XLE/XLO grammar cheatsheet for this install.',
18
+ description:
19
+ 'The reference behind `astryx layout grammar`: the agent cheatsheet for writing XLE/XLO ' +
20
+ "layout expressions, with the alias table generated from this branch's registry rather than " +
21
+ 'hand-maintained, so short names always reflect the components actually installed.',
22
+ importPath: '@astryxdesign/cli/api',
23
+ signature:
24
+ 'layoutGrammar(options?: LayoutGrammarOptions): Promise<LayoutGrammarResponse>',
25
+ keywords: [
26
+ 'layout',
27
+ 'grammar',
28
+ 'cheatsheet',
29
+ 'xle',
30
+ 'xlo',
31
+ 'aliases',
32
+ 'reference',
33
+ ],
34
+ params: [
35
+ {
36
+ name: 'options.cwd',
37
+ type: 'string',
38
+ description:
39
+ 'Directory the component registry (and its alias table) resolves against.',
40
+ },
41
+ ],
42
+ returns: [
43
+ {
44
+ type: 'layout.grammar',
45
+ description:
46
+ "The cheatsheet: a text field with the full grammar reference, plus an aliases map (short name → canonical component) generated from this install's registry.",
47
+ },
48
+ ],
49
+ examples: [
50
+ {
51
+ label: 'Get the cheatsheet',
52
+ code: 'const {data} = await layoutGrammar();',
53
+ },
54
+ ],
55
+ command: 'layout grammar',
56
+ related: ['layoutExpand', 'layoutCheck'],
57
+ };
@@ -127,24 +127,6 @@ describe('search leaf — docs at the grain a reader reads them', () => {
127
127
  SLOW,
128
128
  );
129
129
 
130
- it(
131
- 'finds the component batch guide from task language',
132
- async () => {
133
- const result = await search('look up several components', {
134
- cwd,
135
- type: 'doc',
136
- });
137
- expect(result.data.results).toContainEqual(
138
- expect.objectContaining({
139
- name: 'cli/component-lookups',
140
- section: 'several',
141
- command: 'astryx docs cli/component-lookups several',
142
- }),
143
- );
144
- },
145
- SLOW,
146
- );
147
-
148
130
  it(
149
131
  'gives a top-level namespace hit the topic list as its parent',
150
132
  async () => {
@@ -19,6 +19,7 @@ import {
19
19
  } from './template.mjs';
20
20
  import {search} from '../search/search.mjs';
21
21
  import {build} from '../build/build.mjs';
22
+ import {layoutExpand} from '../layout/layout.mjs';
22
23
  import {runCli} from '../../test-utils/run-cli.mjs';
23
24
 
24
25
  let tmpDir;
@@ -517,7 +518,53 @@ describe('integration template discovery', () => {
517
518
  });
518
519
  });
519
520
 
520
- it('resolves chained block aliases consistently in template', async () => {
521
+ it('resolves a replacement block through the layout alias', async () => {
522
+ const pkgDir = installWidgets(tmpDir);
523
+ writeTemplate(pkgDir, 'acme-card-callout', {
524
+ kind: 'block',
525
+ source:
526
+ 'export default function AcmeCardCallout() { return <span>Acme replacement block</span>; }\n',
527
+ });
528
+ declareReplaces(pkgDir, {
529
+ 'acme-card-callout': 'CardCallout',
530
+ });
531
+
532
+ const accidental = path.join(tmpDir, 'node_modules', '@acme', 'extra');
533
+ fs.mkdirSync(path.join(accidental, 'templates'), {recursive: true});
534
+ fs.writeFileSync(
535
+ path.join(accidental, 'package.json'),
536
+ JSON.stringify({name: '@acme/extra', version: '1.0.0'}),
537
+ );
538
+ fs.writeFileSync(
539
+ path.join(accidental, 'astryx.integration.mjs'),
540
+ `export default {templates: './templates'};\n`,
541
+ );
542
+ writeTemplate(accidental, 'CardCallout', {
543
+ kind: 'block',
544
+ body: `export default {type: 'block', name: 'ZZZ accidental block', description: 'collision'};\n`,
545
+ source:
546
+ 'export default function Accidental() { return <span>Accidental block</span>; }\n',
547
+ });
548
+ fs.writeFileSync(
549
+ path.join(tmpDir, 'astryx.config.mjs'),
550
+ `export default { integrations: ['@acme/widgets', '@acme/extra'] };\n`,
551
+ );
552
+
553
+ const result = await layoutExpand('C{card-callout}', {
554
+ name: 'ReplacementLayout',
555
+ cwd: tmpDir,
556
+ });
557
+
558
+ expect(result.data.code).toContain('Acme replacement block');
559
+ expect(result.data.code).not.toContain('Accidental block');
560
+ expect(result.data.blocksReferenced).toEqual(
561
+ expect.arrayContaining([
562
+ expect.objectContaining({name: 'CardCallout', mode: 'splice'}),
563
+ ]),
564
+ );
565
+ }, 30_000);
566
+
567
+ it('resolves chained block aliases consistently in template and layout', async () => {
521
568
  const [firstTarget, secondTarget] = (await discoverCoreTemplates()).filter(
522
569
  candidate => candidate.type === 'block',
523
570
  );
@@ -545,12 +592,29 @@ describe('integration template discovery', () => {
545
592
  cwd: tmpDir,
546
593
  });
547
594
  expect(firstTemplate.data.source).toContain('First chain replacement');
595
+ const layoutId = id =>
596
+ id.replace(/([a-z0-9])([A-Z])/gu, '$1-$2').toLowerCase();
597
+ const firstLayout = await layoutExpand(
598
+ `C{${layoutId(firstTarget.dirName)}}`,
599
+ {
600
+ cwd: tmpDir,
601
+ },
602
+ );
603
+ expect(firstLayout.data.code).toContain('First chain replacement');
604
+ expect(firstLayout.data.code).not.toContain('Final chain replacement');
548
605
 
549
606
  const secondTemplate = await template(secondTarget.dirName, {
550
607
  show: true,
551
608
  cwd: tmpDir,
552
609
  });
553
610
  expect(secondTemplate.data.source).toContain('Final chain replacement');
611
+ const secondLayout = await layoutExpand(
612
+ `C{${layoutId(secondTarget.dirName)}}`,
613
+ {
614
+ cwd: tmpDir,
615
+ },
616
+ );
617
+ expect(secondLayout.data.code).toContain('Final chain replacement');
554
618
  }, 30_000);
555
619
 
556
620
  it('keeps the old discovery behavior when no replacement is declared', async () => {
@@ -8,7 +8,7 @@
8
8
  * the requested one, and routes to a leaf (list/show/skeleton/copy/cdn). The shared
9
9
  * discovery/IO + cross-command helpers live in `foundation/discovery/template-adapter.mjs` and are
10
10
  * RE-EXPORTED here so external import paths (`api/template/template.mjs`) —
11
- * used by component, search, init, discover, Doctor integration, and
11
+ * used by component, layout, search, init, discover, Doctor integration, and
12
12
  * lib/project — keep resolving unchanged.
13
13
  *
14
14
  * @position api/template — the template dispatcher + barrel; leaves live under
@@ -15,7 +15,6 @@ import {
15
15
  import {AstryxError} from '../../error.mjs';
16
16
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
17
17
  import {listAvailableThemes, findTheme} from '../_adapter.mjs';
18
- import {applyWrites} from '../../integration/add-helpers.mjs';
19
18
  // Scaffolded files must not carry our repo boilerplate into a consumer's tree.
20
19
  import {stripCopyrightHeader} from '../../../foundation/text/copyright-header.mjs';
21
20
 
@@ -137,28 +136,37 @@ export async function themeAdd(slug, options = {}) {
137
136
  }
138
137
  }
139
138
 
140
- // One transaction: every file publishes, or every replaced file gets its
141
- // previous bytes back and every created file is removed. mkdir is inside the
142
- // try so a failure (e.g. an ancestor is a file → EEXIST/ENOTDIR) surfaces as
143
- // a stable ERR_WRITE_FAILED rather than leaking a raw fs errno + absolute path.
139
+ // Stage to temp files then rename, rolling back partials on failure so a
140
+ // failed write never leaves a half-written theme. mkdir is inside the try so
141
+ // a failure (e.g. an ancestor is a file → EEXIST/ENOTDIR) surfaces as a
142
+ // stable ERR_WRITE_FAILED rather than leaking a raw fs errno + absolute path.
143
+ const staged = [];
144
144
  try {
145
145
  fs.mkdirSync(resolvedDir, {recursive: true});
146
- const plans = writes.map(w => {
147
- fs.mkdirSync(path.dirname(w.dest), {recursive: true});
148
- // Confine again once the directories exist: one may have been swapped
149
- // for a link since the destination was first checked.
146
+ for (const w of writes) {
150
147
  const dest = assertWithin(w.name, resolvedDir, {
151
148
  label: `theme destination for ${w.name}`,
152
149
  });
153
- return {
154
- path: dest,
155
- contents: scaffoldContents(fs.readFileSync(w.src)),
156
- createOnly: !overwrite,
157
- };
158
- });
159
- applyWrites(plans);
150
+ fs.mkdirSync(path.dirname(dest), {recursive: true});
151
+ // The staging file is an output path too: confine it, and never write
152
+ // through an entry that already exists under its name.
153
+ const tmp = assertWithin(`${w.name}.${process.pid}.tmp`, resolvedDir, {
154
+ label: `theme staging file for ${w.name}`,
155
+ });
156
+ fs.writeFileSync(tmp, scaffoldContents(fs.readFileSync(w.src)), {flag: 'wx'});
157
+ staged.push({tmp, dest});
158
+ }
159
+ for (const s of staged) {
160
+ fs.renameSync(s.tmp, s.dest);
161
+ }
160
162
  } catch (err) {
161
- if (err instanceof AstryxError) throw err;
163
+ for (const s of staged) {
164
+ try {
165
+ fs.rmSync(s.tmp, {force: true});
166
+ } catch {
167
+ /* best-effort */
168
+ }
169
+ }
162
170
  if (err instanceof PathSafetyError) {
163
171
  throw new AstryxError(
164
172
  err.message,