@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.e26723d

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 (142) hide show
  1. package/README.md +7 -7
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +41 -1
  4. package/api/component/_adapter.mjs +93 -7
  5. package/api/component/component.mjs +22 -5
  6. package/api/component/component.test.mjs +420 -167
  7. package/api/component/integration-export-name.test.mjs +644 -0
  8. package/api/component/list/list.mjs +42 -4
  9. package/api/docs/_adapter.mjs +207 -1
  10. package/api/docs/integrationDocs.test.mjs +124 -0
  11. package/api/doctor/theme-checks.mjs +1 -1
  12. package/api/gap-report/gap-report.mjs +67 -4
  13. package/api/init/run/run.mjs +2 -2
  14. package/api/integration/_adapter.d.mts +12 -0
  15. package/api/integration/_adapter.mjs +54 -0
  16. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  17. package/api/integration/add-contribution.mjs +7 -3
  18. package/api/integration/authoring-checks.mjs +11 -2
  19. package/api/integration/authoring-checks.test.mjs +39 -0
  20. package/api/integration/authoring-checks.type.d.mts +9 -0
  21. package/api/integration/authoring-checks.type.mjs +14 -1
  22. package/api/integration/integrationComponentConflicts.doc.mjs +21 -5
  23. package/api/integration/pack-check.mjs +61 -107
  24. package/api/integration/pack-check.test.mjs +359 -0
  25. package/api/integration/validate-integration.test.mjs +11 -0
  26. package/api/search/search.d.mts +9 -0
  27. package/api/search/search.mjs +108 -16
  28. package/api/search/search.test.mjs +36 -0
  29. package/api/swizzle/copy/copy.mjs +28 -14
  30. package/api/template/template.test.mjs +120 -3
  31. package/api/theme/build/build.family.test.mjs +16 -0
  32. package/api/theme/build/build.mjs +81 -22
  33. package/api/theme/themeBuild.doc.mjs +3 -3
  34. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  35. package/assets/codemods/transforms/next/index.mjs +11 -1
  36. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  37. package/assets/docs/README.md +10 -0
  38. package/assets/docs/author-a-theme.doc.mjs +1 -1
  39. package/assets/docs/getting-started.doc.mjs +3 -3
  40. package/assets/docs/theme.doc.dense.mjs +1 -1
  41. package/assets/docs/theme.doc.mjs +1 -1
  42. package/assets/docs/theme.doc.zh.mjs +1 -1
  43. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  44. package/assets/docs/tree/check-the-props.doc.mjs +145 -0
  45. package/assets/docs/tree/checks.doc.mjs +4 -4
  46. package/assets/docs/tree/component-family.doc.mjs +5 -1
  47. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  48. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  49. package/assets/docs/tree/links.doc.mjs +2 -2
  50. package/assets/docs/tree/single-component.doc.mjs +3 -2
  51. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  52. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  53. package/assets/docs/tree/versioning.doc.mjs +1 -0
  54. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  55. package/assets/docs/use-a-theme.doc.mjs +2 -2
  56. package/assets/docs/working-with-ai.doc.mjs +43 -4
  57. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  58. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  59. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  60. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  61. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  62. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  63. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  64. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  65. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  66. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  67. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  68. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  69. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  70. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  71. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  72. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  73. package/assets/templates/pages/detail-page/page.tsx +3 -3
  74. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  75. package/assets/theme.template.ts +1 -1
  76. package/authoring/doctypes/base/type.ts +6 -0
  77. package/authoring/doctypes/component/component.doc.mjs +4 -4
  78. package/authoring/doctypes/component/type.ts +6 -3
  79. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  80. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  81. package/authoring/doctypes/template/parse.d.mts +6 -2
  82. package/authoring/doctypes/template/parse.mjs +3 -1
  83. package/authoring/doctypes/template/type.ts +22 -0
  84. package/authoring/index.d.ts +1 -0
  85. package/authoring/integration/integration.doc.mjs +8 -1
  86. package/authoring/integration/parse.test.mjs +10 -0
  87. package/authoring/integration/schema.d.mts +2 -0
  88. package/authoring/integration/schema.mjs +1 -0
  89. package/authoring/integration/type.ts +4 -0
  90. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  91. package/clients/cli/commands/build-theme.mjs +1 -1
  92. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  93. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  94. package/clients/cli/commands/doctor-integration-components.doc.mjs +9 -3
  95. package/clients/cli/commands/doctor-integration.test.mjs +392 -0
  96. package/clients/cli/commands/doctor.mjs +74 -0
  97. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  98. package/clients/cli/lib/component-format.mjs +5 -1
  99. package/foundation/config/project.mjs +82 -0
  100. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  101. package/foundation/discovery/component-discovery.d.mts +190 -10
  102. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  103. package/foundation/discovery/component-discovery.mjs +864 -56
  104. package/foundation/discovery/component-discovery.test.mjs +96 -0
  105. package/foundation/discovery/component-replacement.mjs +18 -0
  106. package/foundation/discovery/component-replacement.test.mjs +40 -0
  107. package/foundation/discovery/doc-types.d.mts +157 -0
  108. package/foundation/discovery/doc-types.mjs +1224 -0
  109. package/foundation/discovery/doc-types.test.mjs +652 -0
  110. package/foundation/discovery/docs-discovery.d.mts +81 -0
  111. package/foundation/discovery/docs-discovery.mjs +57 -0
  112. package/foundation/discovery/module-exports.d.mts +27 -0
  113. package/foundation/discovery/module-exports.mjs +356 -31
  114. package/foundation/discovery/module-exports.test.mjs +172 -0
  115. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  116. package/foundation/discovery/package-tsconfig.mjs +228 -0
  117. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  118. package/foundation/discovery/project-typescript.d.mts +23 -0
  119. package/foundation/discovery/project-typescript.mjs +61 -0
  120. package/foundation/discovery/prop-drift.d.mts +220 -0
  121. package/foundation/discovery/prop-drift.mjs +1765 -0
  122. package/foundation/discovery/prop-drift.test.mjs +1197 -0
  123. package/foundation/discovery/template-adapter.mjs +74 -5
  124. package/foundation/discovery/type-match.d.mts +53 -0
  125. package/foundation/discovery/type-match.mjs +660 -0
  126. package/foundation/discovery/type-match.test.mjs +169 -0
  127. package/foundation/discovery/type-text.d.mts +68 -0
  128. package/foundation/discovery/type-text.mjs +658 -0
  129. package/foundation/discovery/type-text.test.mjs +196 -0
  130. package/foundation/discovery/unresolved-references.d.mts +62 -0
  131. package/foundation/discovery/unresolved-references.mjs +790 -0
  132. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  133. package/foundation/doc-compiler/doc-loads.test.mjs +9 -1
  134. package/foundation/doc-compiler/inputs.mjs +23 -0
  135. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  136. package/foundation/doc-compiler/links.d.mts +9 -5
  137. package/foundation/doc-compiler/links.mjs +4 -2
  138. package/foundation/integrations/contribution-inventory.mjs +6 -1
  139. package/foundation/response/response-types.doc.mjs +1 -1
  140. package/foundation/xle/registry.mjs +4 -0
  141. package/foundation/xle/registry.test.mjs +28 -0
  142. package/package.json +10 -10
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @input Controlled open state for a modal Drawer
7
+ * @output A Drawer whose DrawerHeader renders a close button because it receives onOpenChange
8
+ * @position Copyable DrawerHeader example
9
+ */
10
+
11
+ import {useState} from 'react';
12
+ import {Drawer, DrawerHeader} from '@astryxdesign/core/Drawer';
13
+ import {Button} from '@astryxdesign/core/Button';
14
+ import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
15
+ import {Text} from '@astryxdesign/core/Text';
16
+
17
+ export default function DrawerHeaderWithCloseButton() {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+
20
+ return (
21
+ <>
22
+ <Button label="Open drawer" onClick={() => setIsOpen(true)} />
23
+ <Drawer
24
+ isOpen={isOpen}
25
+ onOpenChange={setIsOpen}
26
+ label="Details"
27
+ width={360}>
28
+ <Layout
29
+ header={<DrawerHeader title="Details" onOpenChange={setIsOpen} />}
30
+ content={
31
+ <LayoutContent>
32
+ <Text type="body">
33
+ Passing onOpenChange renders the close button, which calls it
34
+ with false.
35
+ </Text>
36
+ </LayoutContent>
37
+ }
38
+ />
39
+ </Drawer>
40
+ </>
41
+ );
42
+ }
@@ -600,9 +600,9 @@ export default function DetailPage2Template() {
600
600
  }
601
601
  end={!isNarrow && showSidePanel ? <RightPanel /> : undefined}
602
602
  />
603
- {/* Mobile: the side panel content opens as a full-screen dialog. (A
604
- side drawer/sheet would be more idiomatic, but Astryx has no Drawer
605
- component yet — #2575 — so we use the fullscreen Dialog variant.) */}
603
+ {/* Mobile: the side-panel content uses the full viewport so order details
604
+ are not squeezed beside narrow page chrome. Drawer remains available
605
+ for side-overlay flows that intentionally preserve page context. */}
606
606
  <Dialog
607
607
  variant="fullscreen"
608
608
  isOpen={isNarrow && isPanelDialogOpen}
@@ -1652,8 +1652,8 @@ const COMPOSER_TOOLS: {
1652
1652
  /**
1653
1653
  * The compose sheet.
1654
1654
  *
1655
- * **`BottomSheet hasScrim={false}`, which is the only non-modal surface in
1656
- * core.** With the scrim off the sheet opens with `show()` rather than
1655
+ * **`BottomSheet hasScrim={false}` keeps this bottom-edge composer non-modal.**
1656
+ * With the scrim off the sheet opens with `show()` rather than
1657
1657
  * `showModal()`, so it never enters the top layer, never makes the page inert
1658
1658
  * and never locks body scroll: the inbox behind it stays live. That matters
1659
1659
  * more here than anywhere else in this template, because writing a message is
@@ -1661,12 +1661,14 @@ const COMPOSER_TOOLS: {
1661
1661
  * actually said, copy an order number out of a thread. A modal composer
1662
1662
  * answers that with "close me first".
1663
1663
  *
1664
- * The trade is position, and it is worth naming. A sheet rises from the bottom
1665
- * edge, centred: its positioner is `inset-inline: 0` with
1666
- * `justify-content: center`, and there is no anchor, side or width prop. So
1667
- * this cannot be docked to the inline-end corner the way a `Dialog` can be.
1668
- * `Dialog` takes a static `position` and would put the window in that corner —
1669
- * but `Dialog` is always modal, so buying the corner costs the live inbox.
1664
+ * The trade is bottom-edge geometry, and it is worth naming. A non-modal
1665
+ * `Drawer` keeps page context live from an inline edge, but it is a full-height
1666
+ * side panel; this composer intentionally rises from the bottom. A sheet's
1667
+ * positioner is `inset-inline: 0` with `justify-content: center`, and there is
1668
+ * no anchor, side or width prop. So this cannot be docked to the inline-end
1669
+ * corner the way a `Dialog` can be. `Dialog` takes a static `position` and would
1670
+ * put the window in that corner — but `Dialog` is always modal, so buying the
1671
+ * corner costs the live inbox.
1670
1672
  * `ChatComposerDrawer` is a false friend: despite the name it is an in-flow
1671
1673
  * disclosure tray for a ChatComposer's attachments, with no positioning of its
1672
1674
  * own.
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * 1. WRAP THE APP. A theme does nothing until it is provided.
20
20
  *
21
- * import {Theme} from '@astryxdesign/core';
21
+ * import {Theme} from '@astryxdesign/core/theme';
22
22
  * import {myTheme} from './theme';
23
23
  *
24
24
  * <Theme theme={myTheme} mode="system"> // 'system' | 'light' | 'dark'
@@ -383,6 +383,12 @@ export interface ComponentPropDoc {
383
383
  * ```
384
384
  */
385
385
  slotElements?: ComponentSlotElement[];
386
+ /** Lifecycle marker for a prop outside the stable compatibility promise
387
+ * (spec:AST-017 FR21). Set `'experimental'` on a stable component's
388
+ * optional `experimental` prop object, and say in `description` that its
389
+ * contents may change or be removed in a patch release. Omit for stable
390
+ * props. */
391
+ stability?: 'experimental';
386
392
  }
387
393
 
388
394
  /**
@@ -30,7 +30,7 @@ export const doc = {
30
30
  name: 'name',
31
31
  type: 'string',
32
32
  description:
33
- "Stable machine identity and directory name without the Astryx prefix, PascalCase. e.g. 'Button', 'TextInput', 'AppShell'. Change `displayName`, not `name`, to edit the visible label; registry URLs derive from this identity by default.",
33
+ "Stable machine identity and directory name without the Astryx prefix, PascalCase. e.g. 'Button', 'TextInput', 'AppShell'. For an integration component it is the export apps import: the doc sits beside the file that exports it, and when that file exports nothing under its own name, `name` may name the export it does have. Change `displayName`, not `name`, to edit the visible label; registry URLs derive from this identity by default.",
34
34
  required: true,
35
35
  },
36
36
  {
@@ -56,7 +56,7 @@ export const doc = {
56
56
  name: 'replaces',
57
57
  type: 'string',
58
58
  description:
59
- "Integration components only: the exact `name` of the Core ComponentDoc this component takes over for unqualified lookup, so every app that loads the integration gets it from component detail, component lists, search, `swizzle <Name>`, and issue routing; `swizzle --list` keeps listing Core names. The Core original stays reachable with `--package @astryxdesign/core`. It takes effect only when the package's peer range starts at the release that applies it, `\"@astryxdesign/cli\": \">=0.6.7\"` or later; without such a range the component keeps its own name and `doctor integration components` warns. Set it only to intentionally own a Core identity; give an alternative or variant its own name instead.",
59
+ 'Integration components only: the exact `name` of the Core ComponentDoc this component takes over for unqualified lookup, so every app that loads the integration gets it from component detail, component lists, search, `swizzle <Name>`, and issue routing; `swizzle --list` keeps listing Core names. The Core original stays reachable with `--package @astryxdesign/core`. It takes effect only when the package\'s peer range starts at the release that applies it, `"@astryxdesign/cli": ">=0.6.7"` or later; without such a range the component keeps its own name and `doctor integration components` warns. Set it only to intentionally own a Core identity; give an alternative or variant its own name instead.',
60
60
  },
61
61
  {
62
62
  name: 'keywords',
@@ -181,13 +181,13 @@ export const doc = {
181
181
  name: 'props',
182
182
  type: 'ComponentPropDoc[]',
183
183
  description:
184
- 'SingleComponentDoc variant (required there): all public props for the one primary component. Each prop is {name, type, description, default?, required?, slotElements?}. Skip styling props like xstyle/className/style. Also present on SubComponentDoc.',
184
+ 'SingleComponentDoc variant (required there): all public props for the one primary component. Each prop is {name, type, description, default?, required?, slotElements?, stability?}. Skip styling props like xstyle/className/style. Also present on SubComponentDoc.',
185
185
  },
186
186
  {
187
187
  name: 'components',
188
188
  type: '(ComponentEntry | ComponentRef)[]',
189
189
  description:
190
- 'MultiComponentDoc variant (required there): one entry per public component/hook exported from the directory. Each entry is a full ComponentEntry (inline: name, displayName, description, props | params+returns) or a name-only ComponentRef pointing at a sibling {Name}.doc.mjs.',
190
+ 'MultiComponentDoc variant (required there): one entry per public component/hook exported from the directory. Each entry is a full ComponentEntry (inline: name, displayName, description, props | params+returns) or a name-only ComponentRef pointing at a sibling {Name}.doc.mjs. In an integration, each full entry the module named by `import` exports is a component under its own name.',
191
191
  },
192
192
  {
193
193
  name: 'subComponentOf',
@@ -33,9 +33,12 @@ export interface ComponentBaseDoc extends AuthoredDocGraphFields {
33
33
  type?: 'component';
34
34
  /**
35
35
  * Stable machine identity and directory name without the Astryx prefix,
36
- * PascalCase (for example `Button`, `Table`, or `AppShell`). Do not change
37
- * this to edit the visible label; use `displayName` for that. Registry URLs
38
- * derive from this value by default.
36
+ * PascalCase (for example `Button`, `Table`, or `AppShell`). For an
37
+ * integration component it is the export apps import: the doc sits beside
38
+ * the file that exports it, and when that file exports nothing under its own
39
+ * name, `name` may name the export it does have. Do not change this to edit
40
+ * the visible label; use `displayName` for that. Registry URLs derive from
41
+ * this value by default.
39
42
  */
40
43
  name: string;
41
44
  /** Human-readable display name with spaces between words, used by the
@@ -0,0 +1,11 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in authoring/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * @file SchemaDoc for the `IntegrationTemplateDoc` doc-type — the template
6
+ * metadata an integration package ships: TemplateDoc, with the package's own
7
+ * `category`. Colocated with the type it describes (`type.ts`).
8
+ * @position packages/cli/authoring/doctypes/template — doc-type documentation
9
+ */
10
+ /** @type {import('@astryxdesign/cli/authoring').SchemaDoc} */
11
+ export const doc: import("@astryxdesign/cli/authoring").SchemaDoc;
@@ -0,0 +1,49 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file SchemaDoc for the `IntegrationTemplateDoc` doc-type — the template
5
+ * metadata an integration package ships: TemplateDoc, with the package's own
6
+ * `category`. Colocated with the type it describes (`type.ts`).
7
+ * @position packages/cli/authoring/doctypes/template — doc-type documentation
8
+ */
9
+
10
+ /** @type {import('@astryxdesign/cli/authoring').SchemaDoc} */
11
+ export const doc = {
12
+ type: 'schema',
13
+ name: 'integration-template-doc',
14
+ displayName: 'IntegrationTemplateDoc',
15
+ namespace: 'authoring',
16
+ description:
17
+ "The doc-type for an integration package's template metadata. It has every " +
18
+ "TemplateDoc field and rule, except that `category` is the package's own " +
19
+ 'string instead of a TemplateCategory value.',
20
+ appliesTo: '<Name>.doc.mjs',
21
+ fields: [
22
+ {
23
+ name: 'category',
24
+ type: 'string',
25
+ description:
26
+ "The package's own category. Bare `astryx template` groups the package's templates by it.",
27
+ },
28
+ ],
29
+ examples: [
30
+ {
31
+ label: 'Block template in an integration package',
32
+ code: `/** @type {import('@astryxdesign/cli/authoring').IntegrationTemplateDoc} */
33
+ export default {
34
+ type: 'block',
35
+ name: 'acme-pricing-table',
36
+ displayName: 'Pricing Table',
37
+ description: 'Plans side by side, with the chosen plan highlighted.',
38
+ aspectRatio: 16 / 9,
39
+ category: 'Acme - Commerce',
40
+ };`,
41
+ },
42
+ ],
43
+ notes: [
44
+ {
45
+ type: 'prose',
46
+ text: "Every other field, and the page and block variants, are TemplateDoc's. Core's own templates use TemplateDoc, whose `category` is a TemplateCategory value.",
47
+ },
48
+ ],
49
+ };
@@ -18,7 +18,9 @@ export type BlockTemplateDoc = import("./type.js").BlockTemplateDoc;
18
18
  * Templates as they load. Integration templates already published omit
19
19
  * `displayName` and `aspectRatio` and group themselves under their own
20
20
  * `category`, so the loader accepts those (template discovery falls back to an
21
- * aspect ratio of 1). The published types keep asking authors for all three.
21
+ * aspect ratio of 1). The published `TemplateDoc` keeps asking authors for all
22
+ * three; `IntegrationTemplateDoc`, for an integration package's templates,
23
+ * takes the package's own `category`.
22
24
  */
23
25
  export type LoadedPageTemplateDoc = Omit<PageTemplateDoc, "displayName" | "category"> & {
24
26
  displayName?: string;
@@ -28,7 +30,9 @@ export type LoadedPageTemplateDoc = Omit<PageTemplateDoc, "displayName" | "categ
28
30
  * Templates as they load. Integration templates already published omit
29
31
  * `displayName` and `aspectRatio` and group themselves under their own
30
32
  * `category`, so the loader accepts those (template discovery falls back to an
31
- * aspect ratio of 1). The published types keep asking authors for all three.
33
+ * aspect ratio of 1). The published `TemplateDoc` keeps asking authors for all
34
+ * three; `IntegrationTemplateDoc`, for an integration package's templates,
35
+ * takes the package's own `category`.
32
36
  */
33
37
  export type LoadedBlockTemplateDoc = Omit<BlockTemplateDoc, "displayName" | "aspectRatio" | "category"> & {
34
38
  displayName?: string;
@@ -78,7 +78,9 @@ const blockTemplateSchema = z
78
78
  * Templates as they load. Integration templates already published omit
79
79
  * `displayName` and `aspectRatio` and group themselves under their own
80
80
  * `category`, so the loader accepts those (template discovery falls back to an
81
- * aspect ratio of 1). The published types keep asking authors for all three.
81
+ * aspect ratio of 1). The published `TemplateDoc` keeps asking authors for all
82
+ * three; `IntegrationTemplateDoc`, for an integration package's templates,
83
+ * takes the package's own `category`.
82
84
  *
83
85
  * @typedef {Omit<PageTemplateDoc, 'displayName' | 'category'>
84
86
  * & {displayName?: string, category?: string}} LoadedPageTemplateDoc
@@ -201,3 +201,25 @@ export type TemplateCategory =
201
201
  | 'Shell - Blank';
202
202
 
203
203
  export type TemplateDoc = PageTemplateDoc | BlockTemplateDoc;
204
+
205
+ /** A page template an integration package ships, with its own `category`. */
206
+ export interface IntegrationPageTemplateDoc extends Omit<
207
+ PageTemplateDoc,
208
+ 'category'
209
+ > {
210
+ category?: string;
211
+ }
212
+
213
+ /** A block template an integration package ships, with its own `category`. */
214
+ export interface IntegrationBlockTemplateDoc extends Omit<
215
+ BlockTemplateDoc,
216
+ 'category'
217
+ > {
218
+ category?: string;
219
+ }
220
+
221
+ /** The doc-type for an integration package's template metadata: TemplateDoc,
222
+ * with `category` the package's own string (bare `astryx template` groups
223
+ * the package's templates by it) instead of a TemplateCategory value. */
224
+ export type IntegrationTemplateDoc =
225
+ IntegrationPageTemplateDoc | IntegrationBlockTemplateDoc;
@@ -28,6 +28,7 @@ export type {HookDoc} from './doctypes/types.js'; // useToast.doc.mjs
28
28
  export type {FunctionDoc} from './doctypes/types.js'; // search.doc.mjs (hook | api)
29
29
  export type {ReferenceDoc} from './doctypes/types.js'; // theming.doc.mjs
30
30
  export type {TemplateDoc} from './doctypes/types.js'; // Foo.doc.mjs
31
+ export type {IntegrationTemplateDoc} from './doctypes/types.js'; // an integration's Foo.doc.mjs
31
32
  export type {SchemaDoc} from './doctypes/types.js'; // config.doc.mjs (object shape)
32
33
  export type {CommandDoc} from './doctypes/types.js'; // search.doc.mjs (CLI command)
33
34
  export type {EnumDoc} from './doctypes/types.js'; // error-codes.doc.mjs (vocabulary)
@@ -74,6 +74,13 @@ export const doc = {
74
74
  description: 'Where to file issues/feedback for this integration.',
75
75
  example: "'https://github.com/acme/widgets/issues'",
76
76
  },
77
+ {
78
+ name: 'docsiteUrl',
79
+ type: 'string',
80
+ description:
81
+ "Link to this integration's docs site, such as its home page. Catalogs that list the package may show it.",
82
+ example: "'https://widgets.acme.dev'",
83
+ },
77
84
  ],
78
85
  examples: [
79
86
  {
@@ -118,7 +125,7 @@ export const doc = {
118
125
  text:
119
126
  'Validate the manifest with `astryx doctor integration validate`. At the ' +
120
127
  'load boundary, a known field of the wrong type is an error, issuesUrl ' +
121
- 'must be a valid URL, and unknown fields become warnings so an older CLI ' +
128
+ 'and docsiteUrl must be valid URLs, and unknown fields become warnings so an older CLI ' +
122
129
  'can still load the fields it understands. Before publishing, also run ' +
123
130
  '`templates`, `components`, and `docs` under the same `doctor integration` ' +
124
131
  'group. Those leaves compare authored identities with Core and explain ' +
@@ -87,6 +87,16 @@ describe('parseIntegration (load boundary)', () => {
87
87
  expect(reason({issuesUrl: 'nope'})).toContain('issuesUrl');
88
88
  });
89
89
 
90
+ it('accepts a docsiteUrl as a known key', () => {
91
+ const manifest = {docsiteUrl: 'https://widgets.example.com'};
92
+ expect(parseIntegration(manifest)).toEqual(manifest);
93
+ expect(unknownIntegrationKeys(manifest)).toEqual([]);
94
+ });
95
+
96
+ it('rejects a non-URL docsiteUrl', () => {
97
+ expect(reason({docsiteUrl: 'nope'})).toContain('docsiteUrl');
98
+ });
99
+
90
100
  it('rejects a noncanonical providerId', () => {
91
101
  expect(reason({providerId: 'Acme Widgets'})).toContain('providerId');
92
102
  });
@@ -39,6 +39,7 @@ export const integrationBaseSchema: z.ZodObject<{
39
39
  docs: z.ZodOptional<z.ZodString>;
40
40
  themes: z.ZodOptional<z.ZodString>;
41
41
  issuesUrl: z.ZodOptional<z.ZodString>;
42
+ docsiteUrl: z.ZodOptional<z.ZodString>;
42
43
  }, z.core.$strip>;
43
44
  export const integrationSchema: z.ZodObject<{
44
45
  providerId: z.ZodOptional<z.ZodString>;
@@ -48,6 +49,7 @@ export const integrationSchema: z.ZodObject<{
48
49
  docs: z.ZodOptional<z.ZodString>;
49
50
  themes: z.ZodOptional<z.ZodString>;
50
51
  issuesUrl: z.ZodOptional<z.ZodString>;
52
+ docsiteUrl: z.ZodOptional<z.ZodString>;
51
53
  agentDocs: z.ZodOptional<z.ZodObject<{
52
54
  append: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
53
55
  }, z.core.$strip>>;
@@ -78,6 +78,7 @@ export const integrationBaseSchema = z.object({
78
78
  docs: z.string().optional(),
79
79
  themes: z.string().optional(),
80
80
  issuesUrl: z.string().url().optional(),
81
+ docsiteUrl: z.string().url().optional(),
81
82
  });
82
83
 
83
84
  // Unknown top-level keys are stripped rather than rejected. `.strict()` made a key from a
@@ -44,6 +44,10 @@ export interface AstryxIntegration {
44
44
  };
45
45
  /** Where to file issues/feedback for this integration. */
46
46
  issuesUrl?: string;
47
+ /** Link to this integration's docs site, such as its home page. The CLI
48
+ * accepts it as a known field; catalogs that list the package may show
49
+ * it. */
50
+ docsiteUrl?: string;
47
51
  }
48
52
 
49
53
  /**
@@ -168,7 +168,9 @@ describe('theme build report', () => {
168
168
  }
169
169
  expect(count(out, 'Install in your app')).toBe(1);
170
170
  expect(count(out, 'Or with a <link> tag:')).toBe(1);
171
- expect(count(out, "import { Theme } from '@astryxdesign/core';")).toBe(2);
171
+ expect(
172
+ count(out, "import { Theme } from '@astryxdesign/core/theme';"),
173
+ ).toBe(2);
172
174
  expect(count(out, '[note]')).toBe(1);
173
175
  expect(count(out, 'note: Font')).toBe(0);
174
176
  expect(count(out, 'fonts.googleapis.com/css2')).toBe(1);
@@ -397,7 +397,7 @@ function printThemeApp(result) {
397
397
  emit(
398
398
  text('Wire the generated module once in the app root:'),
399
399
  code(
400
- "import {Theme} from '@astryxdesign/core';\n" +
400
+ "import {Theme} from '@astryxdesign/core/theme';\n" +
401
401
  `import {themes, defaultThemeSlug} from './${stem}';\n\n` +
402
402
  '<Theme theme={themes[defaultThemeSlug]}>\n <App />\n</Theme>',
403
403
  ),
@@ -9,6 +9,9 @@
9
9
  * - Multi-file write is atomic: if one file fails, none are left behind.
10
10
  * - An `--out` escape, or an output directory that cannot be created, fails
11
11
  * with a registered error code.
12
+ * - Every output stays inside the working directory: an absolute `--out`
13
+ * path or a theme file outside it fails before anything is written, while
14
+ * absolute paths inside it build, also through a symlinked spelling.
12
15
  *
13
16
  * Runs the CLI in-process (shared runCli harness) against a tiny synthetic theme file.
14
17
  */
@@ -129,6 +132,68 @@ describe('theme build output-path failures carry registered codes', () => {
129
132
  expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
130
133
  });
131
134
 
135
+ it('reports an absolute --out path outside the project as ERR_PATH_TRAVERSAL, with --check too', async () => {
136
+ const project = path.join(tmpDir, 'project');
137
+ const outside = path.join(tmpDir, 'outside');
138
+ fs.mkdirSync(outside, {recursive: true});
139
+ const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
140
+ const out = path.join(outside, 'ocean.css');
141
+
142
+ for (const extra of [[], ['--check']]) {
143
+ const {result, envelope} = await buildJson(
144
+ [path.relative(project, themeFile), '--out', out, ...extra],
145
+ project,
146
+ );
147
+
148
+ expect(result.code).toBe(1);
149
+ expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
150
+ }
151
+ expect(fs.readdirSync(outside)).toEqual([]);
152
+ });
153
+
154
+ it('reports a theme file outside the project as ERR_PATH_TRAVERSAL, because its outputs go beside it', async () => {
155
+ const project = path.join(tmpDir, 'project');
156
+ fs.mkdirSync(project, {recursive: true});
157
+ const shared = path.join(tmpDir, 'shared');
158
+ const themeFile = writeTheme(shared, 'ocean');
159
+
160
+ for (const file of [themeFile, path.relative(project, themeFile)]) {
161
+ const {result, envelope} = await buildJson([file], project);
162
+
163
+ expect(result.code).toBe(1);
164
+ expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
165
+ }
166
+ expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
167
+ });
168
+
169
+ it('builds absolute paths inside the project, also when they reach it through a symlink', async () => {
170
+ const project = path.join(tmpDir, 'project');
171
+ const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
172
+ const alias = path.join(tmpDir, 'alias');
173
+ fs.symlinkSync(project, alias, 'dir');
174
+
175
+ for (const [args, written] of [
176
+ [
177
+ [themeFile, '--out', path.join(project, 'dist', 'direct.css')],
178
+ path.join(project, 'dist', 'direct.css'),
179
+ ],
180
+ [
181
+ [themeFile, '--out', path.join(alias, 'dist', 'aliased.css')],
182
+ path.join(project, 'dist', 'aliased.css'),
183
+ ],
184
+ [
185
+ [path.join(alias, 'themes', 'theme.mjs')],
186
+ path.join(project, 'themes', 'ocean.css'),
187
+ ],
188
+ ]) {
189
+ const {result, envelope} = await buildJson(args, project);
190
+
191
+ expect(result.code).toBe(0);
192
+ expect(envelope.type).toBe('theme.build');
193
+ expect(fs.existsSync(written)).toBe(true);
194
+ }
195
+ });
196
+
132
197
  it('reports an --out path that escapes through a symlink as ERR_PATH_TRAVERSAL', async () => {
133
198
  const project = path.join(tmpDir, 'project');
134
199
  const outside = path.join(tmpDir, 'outside');
@@ -177,4 +242,23 @@ describe('theme build output-path failures carry registered codes', () => {
177
242
  code: ERROR_CODES.ERR_PATH_TRAVERSAL,
178
243
  });
179
244
  });
245
+
246
+ it('themeBuild() rejects an output outside cwd: an absolute out path, or the default beside a theme file outside cwd', async () => {
247
+ const project = path.join(tmpDir, 'project');
248
+ const inside = writeTheme(path.join(project, 'themes'), 'ocean');
249
+ const shared = path.join(tmpDir, 'shared');
250
+ const outsideTheme = writeTheme(shared, 'ocean');
251
+ const outDir = path.join(tmpDir, 'out');
252
+
253
+ for (const [file, options] of [
254
+ [path.relative(project, inside), {out: path.join(outDir, 'ocean.css')}],
255
+ [outsideTheme, {}],
256
+ ]) {
257
+ await expect(
258
+ themeBuild(file, options, {cwd: project}),
259
+ ).rejects.toMatchObject({code: ERROR_CODES.ERR_PATH_TRAVERSAL});
260
+ }
261
+ expect(fs.existsSync(outDir)).toBe(false);
262
+ expect(fs.readdirSync(shared)).toEqual(['theme.mjs']);
263
+ });
180
264
  });
@@ -126,7 +126,10 @@ describe('theme build emits a live TextInput selector (#4109)', () => {
126
126
  `};\n`,
127
127
  );
128
128
 
129
- const result = await runCli(['theme', 'build', themeFile, '-o', outFile]);
129
+ const result = await runCli(
130
+ ['theme', 'build', themeFile, '-o', outFile],
131
+ tmpDir,
132
+ );
130
133
  const css = fs.readFileSync(outFile, 'utf8');
131
134
 
132
135
  expect(result.status).toBe(0);
@@ -148,7 +151,7 @@ describe('theme build emits a live TextInput selector (#4109)', () => {
148
151
  `};\n`,
149
152
  );
150
153
 
151
- await runCli(['theme', 'build', themeFile, '-o', outFile]);
154
+ await runCli(['theme', 'build', themeFile, '-o', outFile], tmpDir);
152
155
  const css = fs.readFileSync(outFile, 'utf8');
153
156
 
154
157
  expect(css).toContain('.astryx-text-input');
@@ -8,7 +8,8 @@ export const doc = {
8
8
  name: 'doctor integration components',
9
9
  displayName: 'astryx doctor integration components',
10
10
  namespace: 'cli/commands',
11
- summary: 'Check integration component names and replacements against Core',
11
+ summary:
12
+ 'Check integration component names, replacements, and documented props',
12
13
  description:
13
14
  'Compares one local or installed integration with Core component names. A ' +
14
15
  'conflict is allowed and exits successfully, but the report recommends ' +
@@ -17,7 +18,12 @@ export const doc = {
17
18
  'range that turns replacement on, a missing Core target, an invalid value, a ' +
18
19
  'component named after a different Core component, or two replacements for one ' +
19
20
  'Core component is an error. In a package without that range they are warnings, ' +
20
- 'with a warning for each such component, naming the range to declare.',
21
+ 'with a warning for each such component, naming the range to declare. ' +
22
+ "Then it compares each component doc's props with the component's TypeScript " +
23
+ "props, using the project's own `typescript`, and warns about a documented prop " +
24
+ 'the code lacks, a prop the package declares that the doc omits, and a wrong ' +
25
+ '`required`. Without TypeScript it prints one ' +
26
+ 'line saying the comparison was skipped.',
21
27
  fn: 'integrationComponentConflicts',
22
28
  args: [
23
29
  {
@@ -38,7 +44,7 @@ export const doc = {
38
44
  exitCodes: [
39
45
  {
40
46
  code: 0,
41
- when: 'the check completed; component name conflicts, and replaces findings for a package without the CLI range, are warnings',
47
+ when: 'the check completed; component name conflicts, replaces findings for a package without the CLI range, and props that differ from the code are warnings',
42
48
  },
43
49
  {
44
50
  code: 1,