CLI Integrations
Build an npm package that adds components, templates, themes, docs, and codemods to Astryx apps.Overview
An integration is a way to share things with Astryx users: a package you own and maintain, built on a framework Astryx gives you. Publish one, many, or any mix, and people install it in one step. It works with the Astryx CLI alongside Core. See each kind under astryx docs cli/integrations/building-blocks.
For example, ship a carousel as an integration. Any app that installs it can find it with npx astryx search carousel, and the CLI uses it like any Astryx Core component.
An integration can also replace a built-in template or doc. To start, install @astryxdesign/cli and open astryx docs cli/integrations/quick-start.
Quick start
Make the package
An integration starts as a normal npm package. Make a folder for it, create a package.json inside that folder, give the package a name, then install the Astryx CLI and Core for development.
bashmkdir acme-widgets && cd acme-widgetsnpm init -ynpm pkg set name=@acme/astryx-widgetsnpm pkg set 'exports={}' --jsonnpm install -D @astryxdesign/cli @astryxdesign/core
- Start with
"exports": {}: each component and template you add then writes the public import thatintegration verifyresolves. - Run the CLI as
npx astryx, which runs the@astryxdesign/cliyou installed as a devDependency. - Component commands read Core, so they need
@astryxdesign/coreinstalled.
Add your first integration item
An integration can ship several kinds of items. Start with a component named AcmeCarousel.
bashnpx astryx integration add component AcmeCarousel
textcomponent contribution added[ok] AcmeCarouselDeclare component root ./components in astryx.integration.mjs.- components/AcmeCarousel.doc.mjs- components/AcmeCarousel.tsx- package.json- astryx.integration.mjs
integration add creates the component source, its .doc.mjs, the package export, and astryx.integration.mjs. Update the generated doc so the CLI understands the component, then write the component itself.
Read it back the way an app will.
bashnpx astryx component AcmeCarousel
You now have an integration package with one exported component.
Verify and pack the package
Run integration verify to check that an app would see the component, then make the .tgz file you install next. --pack-destination .. writes it beside the package folder, so the next pack does not ship it.
bashnpx astryx integration verifynpm pack --pack-destination ..
textIntegration package ready[ok] @acme/astryx-widgets@1.0.04 packed files; 3/3 required files present.
integration verify packs the package, unpacks it into a temporary app, and checks that the component resolves through its public import there. It publishes nothing and leaves no .tgz file, so npm pack writes ../acme-astryx-widgets-1.0.0.tgz for the next step.
Use it in an app
Install Core, the CLI, and your .tgz file in a new app. The app loads your package because it is a dependency, with no config.
bashcd ..mkdir my-app && cd my-appnpm init -ynpm install @astryxdesign/core @astryxdesign/cli ../acme-astryx-widgets-1.0.0.tgznpx astryx component AcmeCarousel
text**Import:** `import {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel';`
The next sections explain each package field and the integration file. To build the real component, continue with astryx docs cli/integrations/building-blocks/components.
Fill in package.json
An integration is an ordinary npm package. These six fields control how it is named, developed, checked, and published.
peerDependencies are packages the app supplies. devDependencies are packages you use while building the integration.
| Field | Set it to | Why |
|---|---|---|
name | Your package name, such as @acme/astryx-widgets | Each add writes it into the import of the doc it generates. Rename before you add, or update each import after. |
version | The release you publish, such as 1.0.0 | Apps see it; astryx.integration.mjs never repeats it. |
exports | Start with {} | Each component and template add writes its public import here, and integration verify resolves it. |
files | Optional: the paths to publish | Keeps private files out. When the list exists, each add appends its root and astryx.integration.mjs. |
peerDependencies | @astryxdesign/cli; add @astryxdesign/core when your code imports it | The app supplies these packages for your integration. |
devDependencies | @astryxdesign/cli and @astryxdesign/core | Lets you run the CLI and build components while working on the integration. |
Peer ranges and releases are covered in astryx docs cli/integrations/ship/versioning; files and publishing in astryx docs cli/integrations/ship/publishing.
The integration file
The first integration add creates astryx.integration.mjs, and each later add updates it. You do not need to write or edit this file during the quick start.
Files in the package
package.jsondefines the npm package, including its name, version, dependencies, published files, and exports.astryx.integration.mjstells the CLI where the package keeps each kind of integration item.- The integration item files live under those roots. For example, a component can have
AcmeWidget.tsxfor its source andAcmeWidget.doc.mjsfor its documentation.
Fields in the integration file
javascript/** @type {import('@astryxdesign/cli/authoring').AstryxIntegration} */export default {components: './components',};
The file tells the CLI where this package keeps its integration items. Edit it only when you want a custom root or another optional setting. Paths are relative to package.json, and later adds keep a custom path you already set.
| Field | Type | Required | Description |
|---|---|---|---|
| providerId | string | no | The name that marks this package as the source of everything it contributes. Leave it out to use the package name from package.json. Set it to the old name only during a rename, so the IDs of what the package already contributed stay the same. If two packages use the same name here, the one you are working on wins; otherwise the one the CLI reads first wins, and the CLI warns about the other. Example: '@acme/widgets'. |
| components | string | no | The folder that holds your components and their docs, relative to package.json. Example: './src/components'. |
| templates | string | no | The folder that holds your templates, relative to package.json. Example: './src/templates'. |
| codemods | string | no | The folder that holds your codemods, relative to package.json. Example: './codemods'. |
| docs | string | no | The folder that holds your doc topics, relative to package.json. Every {topic}.doc.{ts,mjs,js} in it shows up in astryx docs next to the built-in topics; a topic can also set replaces or extends to take over a built-in topic or add to it. Example: './docs'. |
| themes | string | no | The folder that holds your themes, relative to package.json, with one folder per theme. Each theme folder has the theme source and a matching .doc.mjs file with the same name. Installed themes show up in astryx theme list; theme add --import imports their built package exports, and theme eject creates an editable local fork. Example: './themes'. |
| agentDocs | { append?: readonly string[] } | no | Lines of guidance your package adds to the end of the agent instructions the CLI manages. The CLI owns the heading, labels, bullets, and which files it writes. Example: { append: ['Run acme verify.'] }. |
| issuesUrl | string | no | Where to file issues/feedback for this integration. Example: 'https://github.com/acme/widgets/issues'. |
From Astryx Integration: astryx docs authoring integration
Use the CLI
You can edit the package files by hand, but the CLI handles the normal workflow. These links always open the canonical command docs.
astryx integration add: Add one working contribution to an integration package. Read it with astryx docs cli/commands/integration-add.
astryx integration verify: Check the package the way npm will publish it, before you publish. Read it with astryx docs cli/commands/integration-verify.
astryx doctor integration: Check an integration package while authoring it. Read it with astryx docs cli/commands/doctor-integration.
Building Blocks
These are everything you can add to an integration. Mix them however you like. You are not limited to any one thing.
- One component, or a whole library of them.
- Components together with themes and templates.
- Only docs, if that is all you need.
- Agent guidance, so the AI agents in an app follow your library.
- Codemods that update app code across your breaking changes.
Each kind below is its own guide. Open one to learn, in depth, how to add it and how to keep it healthy as your package grows.
Pick a component name
Choose a unique PascalCase name, such as AcmeCarousel. The name is the component's public export and CLI identity, so keep it stable. Use displayName when you only want to change the label people read.
- Prefer a name that describes the component's purpose in your product.
- Avoid a Core component name unless you are deliberately replacing that component.
- If replacement is truly required, read
astryx docs cli/integrations/building-blocks/components/replace-a-core-component.
Run the add command
Run integration add component in your package with the name. It writes the component, its doc file, and the public import apps use.
bashnpx astryx integration add component AcmeCarousel
textcomponent contribution added[ok] AcmeCarouselDeclare component root ./components in astryx.integration.mjs.- components/AcmeCarousel.doc.mjs- components/AcmeCarousel.tsx- package.json- astryx.integration.mjs
integration add starts with one source export and one single-component .doc.mjs. Keep that shape for one public component. If the source exposes a family, choose the correct shape in astryx docs cli/integrations/building-blocks/components/describe-the-component.
Add never overwrites a file, and --dry-run shows what it would write. Component commands read Core, so install @astryxdesign/core in the package first (astryx docs cli/integrations/quick-start).
Describe the component
A component's .doc.mjs is part of the integration's public contract, not optional commentary. Astryx uses it for CLI output and search, and people and agents read it to decide whether the component fits and how to use it.
- Change the source and its
.doc.mjstogether. - Update the doc whenever the public name, import, behavior, props, defaults, examples, or accessibility requirements change.
integration verifychecks the doc shape and packed import, but it cannot prove the prose still matches the component.
Every component doc has one stable identity and enough usage guidance for a reader to choose it correctly. Pick the shape below that matches your module.
After every source or doc change, read the component back. This output is what people and agents receive.
bashnpx astryx component AcmeCarousel
Start from the generated doc
integration add component creates the normal doc for one public component. Keep the generated identity and import, then replace its sample text and props with the component's real public contract.
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeCarousel',displayName: 'Acme Carousel',import: '@acme/astryx-widgets/components/AcmeCarousel',usage: {description:'Cycles through slides one at a time. Use it for a small set of related cards.',},props: [{name: 'slides',type: 'ReactNode[]',description: 'The slides to show, in order.',required: true,},],};
- Keep
propson the top-level doc. Private implementation helpers do not need entries. - Keep the doc beside the source and change both in the same pull request.
- If the module later exposes several related public exports, adapt this doc with
astryx docs cli/integrations/building-blocks/components/describe-the-component/component-family.
ComponentDoc: The doc-type for a component directory's {Name}.doc.mjs. A discriminated union of SingleComponentDoc (props on the doc), MultiComponentDoc (a components array), and SubComponentDoc (a subComponentOf pointer). All three share the ComponentBaseDoc fields below; the variant is chosen by which of props / components / subComponentOf you set. Read it with astryx docs authoring component-doc.
Explain when to use it
Write usage.description so a person or agent can decide whether this is the right component without opening its source. Say what it does, when to use it, and the most important boundary with a nearby alternative.
javascriptusage: {description:'Cycles through slides one at a time. Use it for a small set of related cards. Use a static list when every item should stay visible.',bestPractices: [{guidance: true, description: 'Keep the slide order stable while someone interacts with the carousel.'},{guidance: false, description: 'Hide information that must remain visible for comparison.'},],},
Document every public prop
Copy the public prop names and types from the source. Explain the behavior a caller controls, not only the TypeScript type.
javascriptprops: [{name: 'slides',type: 'ReactNode[]',description: 'The slides to show, in order.',required: true,},{name: 'interval',type: 'number',description: 'Milliseconds between automatic slide changes.',default: '5000',},],
- Set
required: trueonly when every caller must pass the prop. - Write
defaultexactly as the value should appear in documentation. - Skip styling escape hatches such as
xstyle,className, andstyle.
Add focused examples
Add short examples for important usage that the prop table does not make obvious. Each example should teach one complete pattern and use only public imports.
javascriptexamples: [{label: 'Automatic rotation',code: '<AcmeCarousel slides={slides} interval={5000} />',},],
Read the result
Read the component after every source or doc change. Confirm that its purpose, import, props, defaults, and examples match the source, then verify the packed package.
bashnpx astryx component AcmeCarouselnpx astryx integration verify
Choose the family shape
Start with a complete single-component doc. Convert its top-level props into a components array only when one source module or component directory exposes several public components or hooks as one family. The family doc keeps the shared usage guidance; each array entry owns one public export.
- Put the primary or most-used export first.
- Use a full entry when this file owns that export's description and signature.
- Use
propsfor a component entry. Useparamsandreturnsfor a hook entry. - Do not add private implementation helpers or exports that people should not use directly.
| Field | Type | Required | Description |
|---|---|---|---|
| components | (ComponentEntry | ComponentRef)[] | no | 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. |
From ComponentDoc: astryx docs authoring component-doc
Document the family inline
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeTabs',displayName: 'Acme Tabs',import: '@acme/astryx-widgets/components/AcmeTabs',usage: {description:'Switches between related views without leaving the page.',},components: [{name: 'AcmeTabs',displayName: 'Acme Tabs',description: 'Owns selection and lays out the tab list and panels.',props: [{name: 'value',type: 'string',description: 'The selected tab value.',required: true,},],},{name: 'AcmeTab',displayName: 'Acme Tab',description: 'Selects one view in Acme Tabs.',props: [{name: 'value',type: 'string',description: 'The value this tab selects.',required: true,},],},],};
The public module named by import must export every component or hook named by a full entry.
Give a member its own file
When one family member needs its own doc, replace its full entry with {name: 'MemberName'} and move the details into a sibling doc. The parent keeps the family relationship without copying the child's content. Continue with astryx docs cli/integrations/building-blocks/components/describe-the-component/subcomponent.
Choose a separate doc
Start with a component family doc. Move one public member into a sibling doc when it has its own source and enough behavior, props, or usage guidance to maintain separately. The parent still lists the member, but only by name.
- Keep a small member inline when its whole contract stays clear in the family doc.
- Use a sibling doc when the member needs focused search results, examples, usage guidance, or independent maintenance.
- Give each member one documentation owner. Do not keep a full parent entry and a sibling doc for the same name.
Reference it from the parent
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeDialog',displayName: 'Acme Dialog',usage: {description: 'Presents a focused task above the current page.'},components: [{name: 'AcmeDialog',displayName: 'Acme Dialog',description: 'Owns the modal surface and open state.',props: [],},{name: 'AcmeDialogHeader'},],};
The name-only entry keeps AcmeDialogHeader in the family. Its description and props come only from the sibling file.
Write the subcomponent doc
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeDialogHeader',displayName: 'Acme Dialog Header',subComponentOf: 'AcmeDialog',description: 'Labels an Acme Dialog and holds its close action.',props: [{name: 'title',type: 'string',description: 'The dialog title.',required: true,},],};
| Field | Type | Required | Description |
|---|---|---|---|
| subComponentOf | string | no | SubComponentDoc variant (required there): the parent component's name (e.g. 'Chat'). Marks this file as a sub-component doc that inherits family fields (group, category, keywords, theming, playground) from the parent. |
| description | string | no | SubComponentDoc variant (required there): one-sentence description of the sub-component's role within the parent composition. Single/Multi docs have no top-level description; they derive their summary from usage. |
| props | ComponentPropDoc[] | no | 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. |
From ComponentDoc: astryx docs authoring component-doc
subComponentOfmust exactly match the parent doc'sname.descriptionexplains this member's role in the family.usageis optional; add it when the member needs guidance beyond that sentence.- The child inherits family fields such as
group,category,keywords,theming, andplaygroundunless it overrides them.
Export the component
Apps copy the component doc's import field into their code, so that exact specifier must resolve from your packed package. integration add writes it together with an exports entry in package.json.
json"exports": {"./components/AcmeCarousel": "./components/AcmeCarousel.tsx"}
Add writes the entry only when package.json already has an exports map, so start every package with "exports": {}.
Verify the packed import
integration verify installs the packed package in a temporary app, resolves each documented import, and checks that the module exports the documented component name.
bashnpx astryx integration verify
component_import_unresolvablemeans the exports map has no entry for the documented import.component_export_missingmeans the module does not export the documented name, or package.json has no exports map.typescript_extension_in_specifiermeans the public import ends in.tsor.tsx.
To import from the package root, set import: '@acme/astryx-widgets' and re-export the component from the file that exports["."] points to. See astryx docs cli/commands/integration-verify.
Read the installed component
An app that installs your package sees the component beside Core components, with its package name and public import. The app needs no config.
bashnpx astryx component AcmeCarouselnpx astryx component --listnpx astryx search carousel
text# AcmeCarouselCycles through slides one at a time. Use it for a small set of related cards.**Import:** `import {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel';`
component --list shows it as import: @acme/astryx-widgets/components/AcmeCarousel [@acme/astryx-widgets]. The same commands work in your package while you build it.
Test the packed package
Install the packed package in a test app before publishing. Then run the same detail, list, and search commands there. See astryx docs cli/integrations/ship/test-in-an-app.
Decide whether to replace
Most components should not use replaces. Give a new component its own name and let apps choose it explicitly. Replace Core only when your component must become the default for one Core identity everywhere the integration is active.
Use it when
- Your component intentionally serves the same role as one specific Core component.
- Every app that loads the integration should get your component from unqualified component detail,
component --list, search,swizzle <Name>, and issue routing. - You have tested both the replacement and explicit access to the original Core component.
Do not use it when
- Your component is an alternative, variant, wrapper, or product-specific extension. Give it a unique name instead.
- You only need to resolve an accidental name collision.
- You want the replacement in only one screen or workflow. Replacement applies across the app wherever the integration is active.
Set the replacement
javascript/** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */export default {type: 'component',name: 'AcmeSideNav',displayName: 'Acme Side Nav',replaces: 'SideNav',import: '@acme/astryx-widgets/components/AcmeSideNav',usage: {description: 'Product navigation for Acme apps.'},props: [],};
| Field | Type | Required | Description |
|---|---|---|---|
| replaces | string | no | 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. |
From ComponentDoc: astryx docs authoring component-doc
The replacement turns on only when your package declares the first CLI release that applies it. Add this to its package.json:
json{"peerDependencies": {"@astryxdesign/cli": ">=0.6.7"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}}
- Any
@astryxdesign/clirange that starts at that release or later turns it on, including the>=0.7.0that earlierintegration add themeandintegration add doc --parentwrote. - Without such a range, the component keeps its own name, the Core component stays selected (a component named like its target stays ambiguous by that bare name, as before), and
doctor integration componentswarns with the range to add. Apps that load such a package see no change. replacesnames the CoreComponentDocidentity, not its display label, import path, or a standalone hook.- Your component may keep a distinct name or use the same name as the target. A distinct name remains directly addressable on older CLIs that ignore
replaces. --package @astryxdesign/corealways selects the original Core component, and--packagewith your package selects your component by either name.- Replacing
SideNavreplaces only that name.SideNavItemand the otherSideNavparts stay Core components; document any parts your package provides under their own names. swizzle --listlists Core components, including the one you replace.swizzle SideNavcopies your component, andswizzle SideNav --package @astryxdesign/corecopies the original.
Check replacement resolution
bashnpx astryx doctor integration componentsnpx astryx component SideNavnpx astryx component AcmeSideNavnpx astryx component SideNav --package @astryxdesign/core
- Once the package declares the range, a missing target, invalid value, second replacement for one target in the same package, or a replacement named after a different Core component is an error. Without the range,
doctor integration componentsreports each of these as a warning. - When several integrations replace one target, explicit configuration beats the automatic pick. Among explicitly configured integrations, the later package wins and Doctor warns. Among autolinked integrations alone, the dependency listed later in
package.jsonwins and Doctor warns.
What a template is
Unlike a component, a template becomes app code: an app copies it into its own code and adapts it to its product.
A component stays a package dependency and updates with the package. A copied template does not, so updating the package never rewrites the app's copy.
bash# How an app finds a template and copies itnpx astryx template --listnpx astryx template acme-dashboard src/app/dashboard
Templates help other people build apps faster while keeping a consistent visual language across products. Share one when people need more than one component to get started: a template brings the right components, layout, content structure, and interaction wiring already assembled.
Do not turn a product-specific page into a rigid component only to share its structure. Share it as a template and let each app adapt its copy.
Choose a page or block
| Kind | Use it for |
|---|---|
| Page | A complete screen, such as a dashboard, settings page, or checkout flow. |
| Block | A smaller section that fits inside a page, such as a hero, form, or data panel. A block can also be the example or showcase for a component. |
Pick a template id
Choose a stable lowercase kebab-case id, such as acme-dashboard. The id becomes the source and doc file name, the package export, and the value apps pass to astryx template. To change a label, edit the doc (astryx docs cli/integrations/building-blocks/templates/document-the-template/template-doc-overview); never rename the id.
- Start with your product or package name, then name the reusable pattern, so the id stays distinct from Core and other integrations.
- Do not end the id with
-page,-app,-view, or-screen. - Do not reuse a Core id by accident: the bare id becomes ambiguous in every app that installs your package. To take over a Core template on purpose, declare
replaces(astryx docs cli/integrations/building-blocks/templates/document-the-template/replace-a-core-template).
bash# See the Core idsnpx astryx --json template --list --package @astryxdesign/core
Run the add command
Pages are the default. Pass --type block to add a block.
bashnpx astryx integration add template acme-dashboardnpx astryx integration add template acme-stat-card --type block
texttemplate contribution added[ok] acme-dashboardDeclare template root ./templates in astryx.integration.mjs.- templates/acme-dashboard.doc.mjs- templates/acme-dashboard.tsx- package.json- astryx.integration.mjs
The command writes templates/<id>.tsx for the UI and templates/<id>.doc.mjs for its metadata, and declares the templates directory in astryx.integration.mjs. When package.json has an exports map, it also adds the ./templates/<id> export (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
It never overwrites an existing source or doc file. Add --dry-run to see every planned write first.
Understand the two files
Every template has a source file and a doc file. For acme-dashboard, the source is templates/acme-dashboard.tsx and the doc is templates/acme-dashboard.doc.mjs. Both begin with acme-dashboard, which is how Astryx knows they belong together.
| File | What it controls |
|---|---|
templates/acme-dashboard.tsx | The UI source that an app copies and then owns. |
templates/acme-dashboard.doc.mjs | How Astryx names, describes, categorizes, previews, and resolves the template before it is copied. |
The integration manifest points Astryx to the templates directory; it does not list each template. Change the source and doc together whenever the purpose, preview, readiness, or replacement behavior changes. No automated check can tell whether the doc still describes the rendered UI.
These fields apply to every page and block. The page, block, and replacement guides add the fields unique to each.
| Field | Type | Required | Description |
|---|---|---|---|
| type | 'page' | 'block' | yes | Discriminant selecting the variant: 'page' for a full page template, 'block' for an editable composition that may be standalone or component-owned. |
| name | string | yes | Stable identifier for block templates; change displayName, not name, to edit their visible label. For page templates it is a human-readable label, while the existing template-directory/CLI slug owns the default registry path. |
| displayName | string | yes | Human-readable label for the gallery/CLI. Spaces out block names that mirror a PascalCase component ('ChatMessageMetadata' → 'Chat Message Metadata'). |
| description | string | no | One-sentence description of what the template provides. |
| keywords | string[] | no | Search keywords for CLI discovery: the ideas, domains, and other names a builder might use for what the template serves (e.g. ['monitoring', 'uptime', 'on-call'] for a service-health dashboard). Lowercase. astryx search matches them as it matches the description and astryx build ranks page templates on them, so keep them out of description. Integration templates need @astryxdesign/cli 0.6.6 or later: earlier CLIs reject the field and drop that template, and CLIs before 0.6.4 also hide the package's doc topics. |
| isReady | boolean | no | Whether the template is ready for use. false shows as '(WIP)' in the gallery and CLI. |
From TemplateDoc: astryx docs authoring template-doc
- Set
typeto the kind you chose inastryx docs cli/integrations/building-blocks/templates/start-a-template. It decides which other fields the doc accepts. - The file name sets the template id (
astryx docs cli/integrations/building-blocks/templates/start-a-template). Labels live in the doc: the terminal list printsname, and JSON listings printdisplayName. Changing either never changes the id. - Write the description for someone choosing between templates. The Doc metadata category in
astryx docs cli/integrations/building-blocks/templates/write-good-templates/template-grading-rubricdefines what a complete description covers. - Name the ideas the template serves in
keywords: the domains, tasks, and other names a builder might use for it, in lowercase, such as['monitoring', 'uptime', 'on-call']for a service-health dashboard.astryx searchmatches them as it matches the description, andastryx buildranks page templates on them, so the description can stay about the layout. - Add
isReady: falseas soon as you generate the doc, because a doc withoutisReadyis listed as ready. Keep it until the template passesastryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-appand the quality review.
Note: keywords needs @astryxdesign/cli 0.6.6 or later. A stable CLI before 0.6.6 rejects the field and drops that template, and one before 0.6.4 also hides your doc topics: template --list and search print one warning, and docs and build say nothing. Declare the CLI floor as an optional peer (astryx docs cli/integrations/ship/versioning); integration verify fails until you do.
Check how Astryx lists it
Read the package-scoped template list after every doc change. Confirm the id, visible name, description, type, readiness, and package.
bashnpx astryx --json template --list --package @acme/astryx-templates
json{"id": "acme-dashboard","name": "acme-dashboard","displayName": "Acme Dashboard","description": "An analytics dashboard for reviewing account health and recent trends.","type": "page","package": "@acme/astryx-templates","isReady": false}
Start from the generated page doc
integration add template writes a page doc by default. Replace its sample description, then add the page-only fields below.
javascript/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */export default {type: 'page',name: 'acme-dashboard',displayName: 'Acme Analytics Dashboard',description:'A filterable account-health dashboard with headline metrics, trends, and a recent-activity table. Dashboard, reporting, analytics, or status overview.',category: 'Dashboard - Analytics',isReady: false,};
Help people find the page
| Field | Type | Required | Description |
|---|---|---|---|
| category | TemplateCategory | no | Functional gallery category following a 'Group - Variant' convention (e.g. 'Dashboard - Analytics', 'Table - Basic', 'Form - Wizard'). The overview groups by the text before ' - '. |
| scaffold | boolean | no | Scaffolding-only template (e.g. blank page): available via the CLI but hidden from browsable galleries. |
From TemplateDoc: astryx docs authoring template-doc
- Set
categoryto the most specific supported{Group} - {Variant}value. Its words become search terms forastryx search, and it appears in the JSON template list. - Set
scaffold: trueonly for a deliberately sparse starting shell. The JSON template list reports it, so tools and galleries can tell a scaffold from a finished page. - Fields that only Astryx's own gallery reads, such as
isHiddenFromOverview, have no effect on integration templates.
Start with a standalone block
Most blocks stand alone. Start with this shape unless the block is specifically the example or showcase for one component.
javascript/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */export default {type: 'block',name: 'acme-stat-card',displayName: 'Acme Stat Card',description:'A compact metric card with a current value, period delta, and supporting trend. KPI, scorecard, summary, or dashboard statistic.',isReady: false,aspectRatio: 4 / 3,componentsUsed: ['Card', 'HStack', 'Text', 'VStack'],};
Do not add exampleFor only because the block uses a component. componentsUsed records composition; exampleFor declares that one component owns the example.
Choose the component relationship
| Field | Type | Required | Description |
|---|---|---|---|
| exampleFor | string | no | Block templates only: optional component ownership. Set this when the block is specifically an example of one component. Omit it for a standalone composition. |
| alsoExampleFor | string[] | no | Block templates only: additional component/hook doc pages whose Examples section should include this block. |
| alsoShowcaseFor | string[] | no | Block templates only: additional doc pages whose hero showcase should reuse this block (secondary placements; does not change the primary showcase). |
| componentsUsed | string[] | no | Block templates only: component names this block uses, for 'See also'/'Used in' cross-references (not primary attribution). |
| isShowcase | boolean | no | Block templates only: when true this block is the canonical hero showcase for its exampleFor component. Requires exampleFor. |
From TemplateDoc: astryx docs authoring template-doc
Keep one clear primary owner. Use the also* fields only for intentional secondary placements.
javascript/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */export default {type: 'block',name: 'acme-status-card-showcase',displayName: 'Acme Status Card Showcase',description:'An account-health summary that demonstrates status, trend, and action states for AcmeStatusCard.',isReady: true,exampleFor: 'AcmeStatusCard',isShowcase: true,alsoExampleFor: ['AcmeDashboard'],aspectRatio: 4 / 3,componentsUsed: ['AcmeStatusCard', 'Button', 'HStack', 'VStack'],};
Then check the package-scoped template list (astryx docs cli/integrations/building-blocks/templates/document-the-template/template-doc-overview) and confirm the entry carries the relationship you set. astryx component does not show integration blocks, so the list is where to check.
Choose a useful preview
| Field | Type | Required | Description |
|---|---|---|---|
| aspectRatio | number | no | Block templates only (required): width-to-height ratio for preview containers (e.g. 16/9, 1, 3/4). |
From TemplateDoc: astryx docs authoring template-doc
Start from the value for the closest shape below, render the block at that ratio, and adjust it until it neither clips nor leaves large empty space. These are starting points, not contract defaults.
| Block shape | Starting value |
|---|---|
| Wide navigation, banner, toolbar, or tabs | 16 / 4 |
| Square button, badge, avatar, icon, spinner, or status | 1 |
| Tall navigation, calendar, list, or tree | 3 / 4 |
| Content card, dialog, table, or form group | 4 / 3 |
scale affects only Astryx's own block previews. Integration blocks can leave it out.
Replace only on purpose
Use with care: replace a Core template only when every app using this integration should receive your source for an existing Core id by default. An alternative, a product-specific variation, or a different kind of template gets its own id instead (astryx docs cli/integrations/building-blocks/templates/start-a-template).
- Sharing a Core id without
replacesdoes not replace Core. It makes the bare id ambiguous, soastryx template <id>fails until the app adds--package, anddoctor integration templatesreports an accidental collision. - A replacement can keep the Core id or use its own.
replacesis what makes it the default.
Declare the replacement
Find the exact Core id and kind first.
bashnpx astryx --json template --list --package @astryxdesign/core
javascript/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */export default {type: 'page',name: 'acme-app-shell',displayName: 'Acme App Shell',description:'An Acme application shell with product navigation, account controls, and a responsive content region.',replaces: 'shell-side-nav',isReady: true,category: 'Shell - Left Sidebar',};
| Field | Type | Required | Description |
|---|---|---|---|
| replaces | string | no | Integration templates only: the exact id of the Core template this one replaces for unqualified lookup. Find it with astryx --json template --list --package @astryxdesign/core; the Core original stays selectable with --package @astryxdesign/core. A page replaces only a Core page and a block only a Core block. Needs @astryxdesign/cli 0.6.4 or later: earlier CLIs reject the field, drop that template, and hide the package's doc topics. |
From TemplateDoc: astryx docs authoring template-doc
Declare replaces on the template doc, never in astryx.integration.mjs.
Require a compatible CLI
Declare the CLI floor from the field above as an optional peer (astryx docs cli/integrations/ship/versioning).
json{"peerDependencies": {"@astryxdesign/cli": ">=0.6.4"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}}
integration verify reports replaces_needs_cli when that peer is missing or too old. Without it, an older CLI drops that template and hides your doc topics.
What apps receive
bash# Receives the active replacementnpx astryx template shell-side-nav src/app# Receives the original Core templatenpx astryx template shell-side-nav --package @astryxdesign/core src/app
When several integrations replace the same Core template, one wins:
- An integration explicitly listed in
astryx.config.mjswins over one that is only installed and picked automatically. - When several explicitly configured integrations replace the same target, the one listed later wins and Astryx reports the ambiguity.
- When no integration is configured explicitly, the package listed later in the app's package.json dependencies wins. Configure the intended integration explicitly instead of relying on that order.
Check the replacement
bashnpx astryx doctor integration templates
| Issue | Meaning |
|---|---|
missing_template_replacement_target | replaces does not name a Core template id. |
invalid_template_replacement | The replacement is unusable or its page/block kind differs from Core. |
ambiguous_template_replacement | One package declares two replacements for a target, or several active packages contend for it. |
Replacement resolution is safe by default. A missing source, invalid doc, missing target, wrong kind, or conflicting declaration never hands the Core id to a questionable replacement: Astryx reports the issue and keeps the Core template. Fix every issue before publishing.
Know what gets copied
astryx template copies exactly one source file. It does not copy sibling helpers, stylesheets, fonts, icons, images, or media. Plan for that before you add supporting files.
| Template | Directory target | Explicit file target |
|---|---|---|
| Page | Writes <target>/page.tsx | Writes the exact file path |
| Block | Writes <target>/<source-basename>.tsx | Writes the exact file path |
bash# Page: src/app/account/page.tsxnpx astryx template acme-account src/app/account# Block: src/features/account/acme-stat-card.tsxnpx astryx template acme-stat-card src/features/account# Exact destination for either kindnpx astryx template acme-stat-card src/features/account/StatusCard.tsx
Astryx refuses to replace an existing file unless the caller passes --overwrite or -f.
Keep edited code in one file
Put every helper that the app is expected to edit in the template source. Small local components, example data, constants, and event handlers can live above or below the default component in the same file.
- Inline a helper when it is part of the editable starting point.
- Move a helper into the integration package only when it should stay package-owned and update with the package.
- Do not import
./helper,./styles, or any other sibling file. - Do not create routes, configuration, or extra files at runtime. Give the app one explicit source file to own.
Use imports that work in the app
After the copy, imports resolve from the app, not from the template directory. Every bare import must name a package the app installs, and every package path must be public.
tsximport {Button} from '@astryxdesign/core/Button';import {Card} from '@astryxdesign/core/Card';import {HStack, VStack} from '@astryxdesign/core/Stack';import {Text} from '@astryxdesign/core/Text';import {AcmeStatusCard} from '@acme/astryx-widgets/components/AcmeStatusCard';const metrics = [{label: 'Healthy accounts', value: '1,248'},{label: 'Needs review', value: '37'},];function MetricRow({label, value}: {label: string; value: string}) {return (<HStack justify="between"><Text>{label}</Text><Text>{value}</Text></HStack>);}export default function AcmeAccountSummary() {return (<Card><VStack gap={4}><AcmeStatusCard />{metrics.map(metric => <MetricRow key={metric.label} {...metric} />)}<Button label="Review accounts" /></VStack></Card>);}
- Import Astryx components from their public Core paths.
- Import integration-owned components, helpers, icons, or styles only through paths the package exports (
astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets). - Avoid adding another library when Astryx or platform APIs already provide the behavior.
Export one component
The source must default-export one React component. Named helpers inside the file are fine.
- Add
use clientas the first statement only when hooks, event handlers, or browser APIs need a client boundary. - Keep a static template server-compatible when it needs no client behavior.
Assets
The copied file does not bring sibling assets with it (astryx docs cli/integrations/building-blocks/templates/build-the-template/write-the-template-file). Give every asset exactly one owner:
| Owner | Use when | How the copied source reaches it |
|---|---|---|
| Copied source | The value is small, editable, and belongs to the starting point | Keep it in the .tsx file |
| Integration package | The asset should stay centrally maintained with the package | Import a stable public package path |
| App | The app must provide product-specific content or branding | Use an explicit placeholder or app public URL and say what to replace |
Do not make a template look self-contained while it relies on an undocumented package file or app convention.
Start with Astryx
Use Astryx component props, layout primitives, and design tokens before writing custom CSS. This keeps the copied result aligned with the host theme and reduces the styling contract an app inherits.
- Use
StackandGridspacing props instead of margins between children. - Use component variants, sizes, padding, and alignment props before restyling internals.
- Use public theme token exports for a value that Astryx does not expose as a prop.
- Do not copy private generated class names or target component internals with selectors.
Keep editable styles with the source
When a style belongs to the editable starting point, define it in the template file.
tsximport * as stylex from '@stylexjs/stylex';import {colorVars} from '@astryxdesign/core/theme/tokens.stylex';const styles = stylex.create({trend: {color: colorVars['--color-success'],minWidth: 0,},});// Later: <Text xstyle={styles.trend}>Up 12%</Text>
Use custom declarations only when no Astryx prop or token fits. Each one counts in the Custom CSS category of astryx docs cli/integrations/building-blocks/templates/write-good-templates/template-grading-rubric.
When a stylesheet must stay package-owned, import it from the template by its public package path.
tsximport '@acme/astryx-widgets/styles/acme-dashboard.css';
Then export that path and include the file in the package (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets). In a TypeScript app, this import type-checks only when the app declares *.css modules.
Prefer host typography
Use Astryx typography components and theme values unless the template must demonstrate a specific licensed typeface. A copied template should normally inherit the app typography instead of installing a new global font.
- Do not add a font only to make sample content look more polished.
- Do not override the app body font from a template.
- Use a custom face only for a real product or content requirement that the integration package owns.
Package a required font
Keep the font files beside a package-owned stylesheet. The stylesheet can use relative url() paths because it stays in the package, and the template imports the stylesheet by its public path (astryx docs cli/integrations/building-blocks/templates/build-the-template/template-assets/template-styles).
css@font-face {font-family: 'Acme Sans';src: url('../assets/fonts/acme-sans-regular.woff2') format('woff2');font-style: normal;font-weight: 400;font-display: swap;}.acme-dashboard-title {font-family: 'Acme Sans', sans-serif;}
- Ship WOFF2 when it satisfies the supported browsers. Add another format only when those browsers require it.
- Declare every weight and style the template uses. Do not make the browser synthesize bold or italic because a file is missing.
- Use
font-display: swapunless the product requirement and performance test justify another value. - Confirm that the font license permits redistribution in the integration package.
Exporting the stylesheet does not publish the font files it points to. Include the font directory in the package as well (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
Check fonts in the app
When you test the template in an app (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-app), also confirm the following.
- Every font request succeeds from the built asset path.
- Computed styles show the requested family, weight, and style.
Textstays readable with the fallback stack when a font loads slowly or fails.
Use Astryx Icon
Use Icon for a standalone icon. For a component icon prop, pass what that prop accepts: most, such as Button and IconButton, take an element like <Icon icon={BellIcon} />, and some menu and navigation items also accept the icon definition itself. This keeps sizing, color, and accessibility aligned with the system.
tsximport {Button} from '@astryxdesign/core/Button';import {Icon} from '@astryxdesign/core/Icon';import {BellIcon} from '@heroicons/react/24/outline';<Icon icon={BellIcon} size="sm" /><Button label="Alerts" icon={<Icon icon={BellIcon} />} />
- Use one supported icon set consistently inside a template.
- Give an icon-only control its required visible or assistive label through the Astryx control API.
- Do not paste raw
svgandpathmarkup when an existing icon definition fits. - Do not render a library icon directly when
Iconor the receiving Astryx component can render it. - Declare an icon library other than Astryx as a dependency (
astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
Ship a custom icon deliberately
A product mark or missing domain symbol can stay package-owned. Export a compatible icon definition from a public package path, then import that path from the template.
tsximport {Icon} from '@astryxdesign/core/Icon';import {AcmePulseIcon} from '@acme/astryx-widgets/icons/AcmePulseIcon';<Icon icon={AcmePulseIcon} size="sm" label="Account health" />
Export the icon path and include its file in the package (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/export-template-assets).
Check icons in the app
When you test the template in an app (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-app), also confirm the following.
- Every icon state renders, including selected, disabled, loading, and narrow layouts.
- Meaningful icons have an accessible name, and decorative icons add no duplicate spoken text.
- No missing icon leaves an empty control.
Do not rely on sibling media
A reference such as ./hero.png or ../assets/demo.mp4 stays unchanged in the copied file and normally breaks.
- Use a stable public URL when the integration owner will continue to host the media.
- Use an explicit app-owned placeholder when every app must supply product-specific media.
- Use a package-owned JS or CSS entrypoint only when every supported app toolchain can resolve and emit the underlying asset.
- Do not assume that a file inside
node_modulesis automatically served at a browser URL.
Understand `/template-assets`
/template-assets/<path> is a preview-only fixture path. During copy, Astryx replaces a complete static image reference with an inline neutral SVG placeholder. It replaces a complete static video reference with an empty string because it cannot create a valid inline video.
| Source reference | Copied result |
|---|---|
/template-assets/hero.png | Inline SVG data URL placeholder |
/template-assets/demo.mp4 | Empty string |
./hero.png or /product/hero.png | Unchanged |
https://example.com/template-assets/hero.png | Unchanged |
- Supported preview image suffixes are SVG, PNG, JPG, JPEG, GIF, WebP, AVIF, and ICO.
- Supported preview video suffixes are MP4, WebM, MOV, OGV, and M4V.
- Keep the preview path as one complete static string. Concatenation, interpolation, method calls, module imports, and unknown file formats are rejected when Astryx cannot replace them safely.
- Use this path only when a placeholder is the intended copied result. It is not a way to ship required product media.
- Astryx serves
/template-assetsonly in its own docs. A preview you host must serve that path itself.
Design a useful placeholder
When the app must provide the final asset, keep the replacement point obvious in the copied source. Preserve enough surrounding layout and example data that the template still teaches the pattern without pretending the final content is included.
tsxconst HERO_IMAGE = '/acme-dashboard-hero.jpg';// Replace HERO_IMAGE with an app-owned public URL before shipping the page.<imgsrc={HERO_IMAGE}alt="Account health trends for the current quarter"width={1200}height={675}/>
- Use accurate alternative text for informative images and
alt=""for decoration. - Provide dimensions or an aspect-ratio container so loading does not shift the page.
- For video, provide controls, a useful poster, and captions when the content requires them.
- Do not leave an inaccessible, expiring, authenticated, or environment-specific URL in copied source.
Check media in the app
When you test the template in an app (astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-app), also confirm the following.
- The copied source shows the placeholder substitutions you expect.
- The network log has no missing, redirected, blocked, or unauthorized media requests.
- Each placeholder keeps the layout and says what the app must replace.
Keep the generated template export
integration verify resolves each template through a public package path without a file extension, such as ./templates/acme-dashboard, and fails when that path is missing.
json{"exports": {"./templates/acme-dashboard": "./templates/acme-dashboard.tsx"}}
integration addwrites this entry whenpackage.jsonhas anexportsmap (astryx docs cli/integrations/building-blocks/templates/start-a-template). Without a map it writes nothing, because creating one would make existing deep imports private. Add the map yourself, then the entry.- Keep the public path without
.tsx. Only the target file ends in.tsx. - Do not point the entry at a barrel file that loses the template default export.
Export package-owned support files
Add a public path for every package-owned component, helper, icon, or stylesheet that the copied source imports. Keep implementation-only files private.
json{"exports": {"./templates/acme-dashboard": "./templates/acme-dashboard.tsx","./components/AcmeStatusCard": "./components/AcmeStatusCard.tsx","./icons/AcmePulseIcon": "./icons/AcmePulseIcon.tsx","./styles/acme-dashboard.css": "./styles/acme-dashboard.css"}}
If a block is also loaded directly as a live .tsx preview, add a pattern that keeps the extension, such as "./templates/*.tsx": "./templates/*.tsx". Keep the generated entry too; integration verify resolves the template through it.
Include every file
An export can point to a file that npm leaves out. When package.json#files exists, list the integration manifest, the templates directory, and every directory the copied file depends on.
json{"files": ["astryx.integration.mjs","templates","components","icons","styles","assets/fonts"],"sideEffects": ["**/*.css"]}
integration addappends the manifest and templates directory when afileslist already exists. Add the other directories yourself.- If
package.jsonsetssideEffects, include CSS in it, as above, so bundlers keep side-effect stylesheet imports. - Run
npm pack --dry-runand read the real file list instead of reasoning from the workspace.
Declare what the copied file imports
A copied file imports from the app, so the app must install every package it imports. Declare the Astryx peers as described in astryx docs cli/integrations/ship/versioning. For other packages:
| Field | Use it for |
|---|---|
peerDependencies | Libraries the copied source imports, such as an icon library. List them in devDependencies too, so the package itself builds. |
dependencies | Runtime packages used only by package-owned modules, which should travel with the integration |
devDependencies | Everything needed to build and test the integration itself |
Use only ranges you test. A package that resolves only because a workspace hoisted it is not declared.
Run the package check
bashnpx astryx integration verify
integration verify runs npm pack, including lifecycle scripts, unpacks the .tgz file into a temporary app without installing its dependencies, and checks the unpacked integration. It publishes nothing and removes the .tgz file and the temporary app.
- It validates the manifest, every template doc, its source file, and the templates directory.
- It checks that the manifest and every template file are in the
.tgzfile. - It checks that each packed template keeps the same id, type,
name, andreplacesas your working copy. It does not compare template source or other doc fields. - It resolves each template through its public package path in the unpacked package.
- It checks that the resolved template module has a default export.
- It checks the CLI peer that features such as template replacement require.
Each failure prints a [fail] line with its message; add --json to see each issue code. astryx docs cli/integrations/help/troubleshooting lists the common messages and their fixes. Fix every error, and review every warning before publishing.
Know what it does not prove
integration verify checks the packed templates and their public entrypoints. It does not copy a template into an app or run a browser build, so it cannot show the following.
- Whether a relative helper or stylesheet survives the copy.
- Whether the copied file type-checks in every supported app toolchain.
- Whether package-owned CSS, fonts, icons, images, or media load in a browser.
- Whether layout, color modes, keyboard and pointer input, and real content work.
- Whether a template id collides with Core, or whether a
replacestarget exists. Runnpx astryx doctor integration templatesfor those. - How the template scores on quality.
Install the packed package
Install the packed package in a clean app as described in astryx docs cli/integrations/ship/test-in-an-app. Also install every library the copied source imports, such as its icon library. Pack and reinstall after every change.
Copy the template
bash# Confirm discovery and metadatanpx astryx --json template --list --package @acme/astryx-widgets# Inspect source without writingnpx astryx template acme-dashboard --package @acme/astryx-widgets# Copy a page and a block to their real app locationsnpx astryx template acme-dashboard --package @acme/astryx-widgets src/app/acme-dashboardnpx astryx template acme-stat-card --package @acme/astryx-widgets src/components
- Confirm the copied files:
src/app/acme-dashboard/page.tsxandsrc/components/acme-stat-card.tsx. - Inspect the copied source for placeholder substitutions and relative references.
- Import the copied block into a real page. A file that is never imported has not been tested.
Build every supported app
Run the type check (npx tsc --noEmit, or the app's own script) and the production build in every framework or bundler the integration supports. One toolchain passing does not prove another.
bashnpx tsc --noEmitnpm run build
- Every import resolves from the copied location.
- Production builds keep required side-effect CSS.
- No import bypasses
exports, and no dependency resolves only through workspace hoisting.
Render the behavior matrix
| Dimension | Required checks |
|---|---|
| Width | Wide and narrow; include the smallest supported viewport |
| Color | Every supported color mode and theme |
| Input | Keyboard and pointer for the primary task |
| Content | Realistic, empty, long, loading, and error states that the pattern supports |
| Assets | Successful CSS, font, icon, image, poster, and video requests |
| Runtime | No console error, hydration mismatch, or missing-provider failure |
Check the rendered hierarchy, reading order, focus order, clipping, overflow, and fallback states. A screenshot of one default state is not a complete test.
Grade the result
Grade the exact packed revision using the evidence from this test (astryx docs cli/integrations/building-blocks/templates/write-good-templates).
How to write good templates
A good template does more than render. It gives an app a clear product starting point that is easy to understand, safe to change, and complete after Astryx copies it out of the package. A rubric scores that quality so you can grade as you build.
- Its purpose and primary task are clear before someone reads every detail.
- It composes Astryx components instead of rebuilding their behavior with raw HTML or custom styles.
- Its hierarchy, spacing, interactions, and reading order still work at narrow widths and in every supported color mode.
- Its source, imports, styles, fonts, icons, images, and media still work from the copied location.
- Its metadata makes the template easy to find and accurately explains when to use it.
- Its example content is realistic enough to expose overflow, empty-space, and hierarchy problems.
Grade the first runnable version, again after each source, doc, dependency, or asset change, and in full before every release. Grade the source, doc, copied file, and rendered app together; none of them is enough alone.
- Read every scoring rule in
astryx docs cli/integrations/building-blocks/templates/write-good-templates/template-grading-rubric. - Fix publication blockers first, then every reasonable deduction.
- Repeat the full grade on the packed package in a clean app (
astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-app). - To have an agent grade and improve the template, use
astryx docs cli/integrations/building-blocks/templates/write-good-templates/grade-template-with-agent.
Keep each scorecard with its package revision and rubric version.
Understand the score
Template rubric 1.4 scores seven categories for 100 points. Aim for 100; B (75) is the publication floor, not the target. Each category below lists its weight. Never award points for anything you did not inspect.
| Grade | Score | Meaning |
|---|---|---|
| A | 90-100 | Exemplary. Copy-ready with no known quality problems. |
| B | 75-89 | Good. Minor issues may remain, but the template is usable. |
| C | 60-74 | Needs work before publication. |
| D | 40-59 | Poor. Significant rewrites are needed. |
| F | 0-39 | Failing. The template teaches or produces bad patterns. |
Keep improving while a deduction has a reasonable fix. A score below 100 is fine only when the remaining tradeoff is intentional and recorded. A template is not ready if the copied file fails to build or a required asset is missing, whatever its score.
Version 1.4 counts public integration components as Astryx components and grades assets and imports after the copy. Record the version with every score so results stay comparable.
Astryx component purity: 30 points
Count every JSX opening tag in the copied .tsx source. An Astryx element is a component imported from @astryxdesign/core or from a public component export of the integration package. A raw HTML element is any lowercase intrinsic JSX tag. Count occurrences, not only unique tag names.
Do not count fragments or a PascalCase helper defined in the same file. Inspect that helper and count the raw HTML inside it. For each raw element, decide whether it is necessary because Astryx has no equivalent, or unnecessary because an Astryx component can replace it.
| Raw HTML elements | Points |
|---|---|
| 0 | 30 |
| 1-2, all necessary | 25 |
| 1-2, any unnecessary | 20 |
| 3-5 | 15 |
| 6-10 | 8 |
| 11-20 | 4 |
| 21 or more | 0 |
| Raw HTML use | Astryx replacement |
|---|---|
div for layout | VStack, HStack, Card, Section, or Center |
div for a grid | Grid |
span or p for text | Text |
h1 through h6 | Heading level={N} |
button | Button or IconButton |
a | Link |
In-page nav or aside | LayoutPanel in the start slot |
header or main | LayoutHeader or LayoutContent in Layout |
ul, ol, or li | List and ListItem |
input, textarea, or select | The matching Astryx form control |
table, tr, or td | Table |
hr | Divider |
dialog | Dialog |
details or summary | Collapsible |
- Do not deduct for an
imgwhen no general Astryx image component fits and its source passes the Image handling category. - Do not deduct for a
formthat wrapsFormLayoutto provide native submission semantics. - Do not deduct for
input type="hidden"when it carries native form state.
Icon purity: 15 points
Count raw icon markup in the copied file. astryx docs cli/integrations/building-blocks/templates/build-the-template/template-assets/template-icons shows how to render icons through Astryx instead.
- Count every raw
svg,path,circle,rect,line,polyline,polygon,ellipse, orgused as an icon. - Count an inline SVG component defined in the template.
- Count an icon component rendered directly instead of through
Iconor an Astryx icon prop.
| Raw SVG icon instances | Points |
|---|---|
| 0 | 15 |
| 1-2 | 10 |
| 3-5 | 5 |
| 6 or more | 0 |
Custom CSS: 15 points
Prefer Astryx component props and design tokens. Count individual CSS properties inside stylex.create and inline style objects. Count each className and stylex.props use once. Do not count Astryx props such as gap, padding, variant, size, color, level, columns, contentPadding, or height.
| Custom style declarations | Points |
|---|---|
| 0 | 15 |
| 1-3, all justified because no Astryx alternative exists | 12 |
| 1-3, any unjustified because an Astryx prop exists | 8 |
| 4-10 | 5 |
| 11-20 | 2 |
| 21 or more | 0 |
This category scores styles authored in the copied source. A package stylesheet is graded through its effect on portability and the rendered app, not as a way to hide custom declarations from this count.
Layout and structure: 15 points
Page templates
- Use
LayoutorCenteras the page root. A template whose category starts withShell -usesAppShellbecause global chrome is its purpose. - Outside a
Shell -template, leave global navigation to the host app. Put in-page navigation in aLayoutPaneland page headings inLayoutHeader. - Use
Gridwithcolumns={{minWidth: 280}}for responsive collections. Do not fix the column count or rebuild the grid in raw CSS. - Use
Centerfor centered content instead of custom flexbox workarounds. - Render one page from one source file. Links may be inert examples, but the template does not create nested routes or router integration.
| Page condition | Points |
|---|---|
| Correct root, responsive grids, proper centering, and one page | 15 |
| Valid root with a smaller issue such as fixed columns or multi-page behavior | 8 |
| Wrong root for the category, a raw layout root, or no Astryx root | 0 |
Block templates
- Do not wrap a block in
AppShell. A block renders inside a preview or page container. - Keep the block focused on one pattern or component usage.
- Keep the composition substantial enough to teach the pattern and small enough to adapt. About 20-100 lines is the normal range.
Doc metadata: 10 points
Score field accuracy for 6 points, the description for 3 points, and naming for 1 point. Read the source and doc together. A field that exists but disagrees with the source is not complete.
Fields: 6 points
- Page:
type,name,displayName,description, explicitisReady, and a specificcategorywhen the page is meant for browsing. - Block:
type,name,displayName,description, explicitisReady, positiveaspectRatio, and completecomponentsUsed. - Relationship and preview fields follow
astryx docs cli/integrations/building-blocks/templates/document-the-template/block-template. A value that contradicts that guide is an inaccuracy.
| Field condition | Points |
|---|---|
| All applicable fields are present and accurate | 6 |
| All fields are present with one inaccuracy | 4 |
| One required field is missing | 2 |
| Two or more required fields are missing, or no doc exists | 0 |
Description: 3 points
A strong description covers four slots: the archetype, the job someone does, the structural or behavioral differentiator, and the alternate words people may search. Use at least six distinct content words after removing generic words such as page, screen, app, view, and component names. Describe the reusable shape, not only the sample data.
| Description condition | Points |
|---|---|
| All four slots, 6 or more distinct content words, and clear separation from sibling templates | 3 |
| Names and differentiates the pattern but misses one slot or leaves a synonym implicit | 2 |
| Generic, repeats component names, or describes only the sample data | 1 |
| Missing or restates the name | 0 |
Naming: 1 point
The id follows astryx docs cli/integrations/building-blocks/templates/start-a-template, displayName is readable, and a browsable page has a specific category (astryx docs cli/integrations/building-blocks/templates/document-the-template/page-template). Slug length is guidance, not a scored condition.
| Naming condition | Points |
|---|---|
| Id, display name, and applicable category follow the convention | 1 |
| Any naming or category requirement is missed | 0 |
Image handling: 5 points
Inspect every image reference in the copied file against astryx docs cli/integrations/building-blocks/templates/build-the-template/template-assets/template-images-media.
| Image condition | Points |
|---|---|
| No image is needed, or every image still works after copy and the demo-placeholder behavior is intentional | 5 |
| One optional demo image is missing in preview, or a placeholder service remains | 2 |
| An essential image breaks after copy, uses a package-relative path, or depends on an inaccessible URL | 0 |
Code quality: 10 points
Award 2 points for each condition. Grade the copied file, not only the package source.
| Condition | Points | How to verify |
|---|---|---|
| Correct client boundary | 2 | use client is the first executable statement when hooks, event handlers, or browser APIs require it. A static template does not add it without need. |
| Default export | 2 | The copied file has one default-exported React component. |
| Self-contained imports | 2 | Every import resolves from the copied location through React, a public Astryx path, a public integration-package export, or an app dependency the template explicitly requires. |
| Realistic example data | 2 | Content has realistic names, amounts, dates, lengths, and states instead of lorem ipsum or numbered placeholders. |
| No dead code | 2 | There are no unused imports or variables, commented-out blocks, or helpers that are never called. |
Give the agent the rubric
Grade one exact package revision against template rubric 1.4. Give the agent the template id, the package name, and a clean app that can install the packed package, then use this prompt. Replace every angle-bracket value. The first pass is read-only so the original score and findings stay visible.
textGrade integration template <id> from <package> at <revision>.Before scoring:1. Read `npx astryx docs cli/integrations/building-blocks/templates/write-good-templates --full`.2. Read `npx astryx docs cli/integrations/building-blocks/templates/write-good-templates/template-grading-rubric --full` and every guide it links.3. Inspect the template source, its matching .doc.mjs file, package.json exports and files, and astryx.integration.mjs.4. Run `npx astryx integration verify` in the package.5. Follow `npx astryx docs cli/integrations/building-blocks/templates/build-the-template/package-and-test/test-template-in-app --full`: install the packed package in the clean app, copy the template, build the app, and render the behavior matrix.Score all seven rubric categories. Cite file and line evidence for every deduction. Do not award points for a state you did not inspect. If browser or build evidence is unavailable, say so and mark the template not publishable.Return the rubric version, package revision, numeric score, letter grade, category scorecard, publishable yes/no verdict, detailed findings, and the top three fixes. Treat 100 as the target. Name every remaining deduction and say whether it is an intentional tradeoff or still needs work. Do not edit files during this first pass.
Require a scorecard
markdown# Template grade: <id>**Rubric:** 1.4**Package revision:** <revision>**Type:** page | block**Grade:** <letter> (<score>/100)**Publishable:** yes | no| Category | Points | Max | Evidence || --- | ---: | ---: | --- || Astryx component purity | X | 30 | || Icon purity | X | 15 | || Custom CSS | X | 15 | || Layout & structure | X | 15 | || Doc metadata | X | 10 | || Image handling | X | 5 | || Code quality | X | 10 | || **Total** | **X** | **100** | |## Detailed findings- <file:line, rubric rule, observed problem, and deduction>## Publication blockers- <failed pack, copy, build, asset, browser, theme, responsive, or input check>## Top three fixes1. <highest-value fix>2. <second fix>3. <third fix>
The category scores must add exactly to the total, and the grade comes from the published bands. The verdict is yes only when the score is 75 or higher and every package, copy, build, asset, and rendered-app check passes.
Improve and regrade
- Fix publication blockers first.
- Fix every reasonable deduction without hiding it behind helper components or package CSS.
- Pack again, copy into a fresh app, and have the agent repeat the full grade. Keep the earlier scorecard so the improvement is visible.
- Stop only when no reasonable fix remains, and record every intentional tradeoff that still costs points.
Themes
A theme contribution has editable defineTheme source plus a built module and production CSS. integration add theme scaffolds the source and declares the package exports; theme build writes the built outputs. An app installs the package, runs theme add --import, applies the theme from its generated app module, and customizes it with extends instead of copying source.
These guides cover the whole path: scaffold the source, generate its palette, define and build the theme, ship fonts and assets, run integration verify, and document how apps add and extend it. Applying a theme, including mode and SSR, works the same for every theme; see astryx docs use-a-theme.
Add a theme
Run integration add theme with a lowercase kebab-case slug. It writes a blank theme — a defineTheme skeleton and its descriptor — into a folder named after the slug.
bashnpx astryx integration add theme ocean
texttheme contribution added[ok] oceanDeclare theme root ./themes in astryx.integration.mjs.- themes/ocean/oceanTheme.ts- themes/ocean/oceanTheme.doc.mjs- package.json- astryx.integration.mjs
oceanTheme.ts exports oceanTheme, a defineTheme source. oceanTheme.doc.mjs describes it with type: 'theme', name, displayName, description, and maintained; theme list shows its name, description, and maintained. The command also declares ./themes/ocean and ./themes/ocean.css package exports. Every descriptor field is in astryx docs authoring.
The source and built outputs ship in your package. Build the source with theme build, then run integration verify before publishing. An app runs theme add --import to record and import the built module and stylesheets; plain theme add still copies source while that default is deprecated. See astryx docs cli/integrations/building-blocks/themes/use-a-theme-in-an-app.
Start from an existing theme
To begin from an existing theme instead of a blank one, pass --from. It copies that theme as your starting point to diverge from — a fork, with no link back.
bashnpx astryx integration add theme ocean --from neutral
The copy keeps the packages the base theme uses, such as lucide-react for its icons: --from adds them to your dependencies, so an app that installs your package gets them too. Install your dependencies again before you build the theme.
Use --from when you want to change a lot. For a small change that should stay linked to a base theme, use extends instead (astryx docs cli/integrations/building-blocks/themes/define-the-theme).
Ship several themes
A package can ship more than one theme. Run integration add theme once per slug; each theme gets its own folder and descriptor, and theme list shows them all.
Generate a palette
A theme needs dozens of related colors — shades of each brand color for backgrounds, borders, text, and states, in both light and dark mode. A palette is that full set of shades. Rather than hand-pick every one, you name a few seed colors and generate the rest, so the shades stay consistent and keep enough contrast to read.
You write a short config naming each color family and its seed, then run theme palette generate. It expands each seed into a full light and dark ramp — the scale of shades — and writes the result into the theme folder, where your theme tokens point at it (see astryx docs cli/integrations/building-blocks/themes/define-the-theme). Keep the config and the generated palette together in that folder.
The smallest config is one color family: an id to name it and a seed color to grow it from. Save it as themes/ocean/palette.config.json:
json{"families": [{"id": "ocean", "seed": "#0074e2"}]}
bash# Print the palette without writing filesnpx astryx theme palette generate themes/ocean/palette.config.json# Write the palette and its receiptnpx astryx theme palette generate themes/ocean/palette.config.json \--out themes/ocean/tokens/ocean.palette.ts
text[ok] Wrote themes/ocean/tokens/ocean.palette.ts[ok] Wrote themes/ocean/tokens/ocean.palette.receipt.json
The command writes two files. ocean.palette.ts is the palette your theme tokens will read: it exports black, white, and palette — a ramp of 21 shades (stops 0 to 100) for both light and dark. The .receipt.json beside it records the exact seeds and settings, so you can regenerate the same palette later. A second run leaves both files alone unless you pass --overwrite.
Add --preview <file>.html to also get a web page showing every shade, so you can eyeball the palette in a browser. Write it outside themes/ so it does not ship in your package. The full list of config fields and options is in astryx docs cli/commands/theme-palette-generate.
Map the palette to tokens
A theme token is a named slot that Astryx components read for a value — --color-accent for the accent color, and so on. Components never read your palette directly; they read tokens. Defining a theme means pointing each token at a palette shade, so the components wear your colors.
Import the palette in oceanTheme.ts and point your tokens at its stops. Each token takes a [light, dark] pair — the shade to use in light mode and the one in dark mode.
ts// themes/ocean/oceanTheme.tsimport {defineTheme} from '@astryxdesign/core/theme';import {palette} from './tokens/ocean.palette';const {light, dark} = palette.ocean;export const oceanTheme = defineTheme({name: 'ocean',tokens: {'--color-accent': [light['45'], dark['70']],},});
Local imports must stay inside the theme folder. One that leaves it, such as ../../shared/colors, fails with invalid_theme, and the theme disappears from theme list. npx astryx theme template writes a file that explains every defineTheme field. For the full token set, scope selectors, and component theming, read astryx docs author-a-theme.
Build on another theme
To base a theme on an existing one, extends it: import the base theme and override only the tokens you change. The derived theme keeps a live link to the base and inherits its later changes — unlike --from, which forks a copy (astryx docs cli/integrations/building-blocks/themes/add-a-theme).
tsimport {defineTheme} from '@astryxdesign/core/theme';import {oceanTheme} from './oceanTheme';export const oceanContrastTheme = defineTheme({name: 'ocean-contrast',extends: oceanTheme,tokens: {'--color-accent': ['#0051a3', '#4aa3ff'],},});
Keep light-dark() to colors
Each [light, dark] pair compiles to a light-dark() value, which switches only colors. Give it colors. For a value that is not a plain color — a gradient — put light-dark() on each color stop, not around the whole value: a browser without light-dark() drops the declaration, and the stop form is the one that degrades safely.
Do not set Core private variables
Core private variables start with --_, such as --_field-radius. They are internals and can change in any Core release, so theme build reports each one it finds as an [error]. Set the standard CSS property instead — borderRadius, padding — and let the build emit the internal variable where Core needs it.
Declare the peer dependencies
The theme imports @astryxdesign/core/theme, so declare Core as a peer dependency, with the range of Core versions you test the theme against — a range, not an exact version, so a Core patch release does not force a republish. integration add theme already declared the optional @astryxdesign/cli peer that reads themes:
json"peerDependencies": {"@astryxdesign/core": "^0.7.0","@astryxdesign/cli": ">=0.6.4"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}
Name the font
A theme names its typefaces in typography: body, heading, and code, each with a family and fallbacks. These set the --font-family-* tokens every component reads. Always give real fallbacks so text stays readable before the font loads, or if it never does. heading inherits family and fallbacks from body when you omit them.
ts// themes/ocean/oceanTheme.tsexport const oceanTheme = defineTheme({name: 'ocean',typography: {body: {family: 'Acme Sans', fallbacks: 'system-ui, sans-serif'},code: {family: 'Acme Mono', fallbacks: 'ui-monospace, monospace'},},// ...your tokens});
The full type scale and font roles are in astryx docs author-a-theme.
Ship the font loader
Naming a family does not load it. When the theme uses non-system fonts, add <slug>.fonts.css beside the built module and production CSS, then export it as ./themes/<slug>.fonts.css. theme add --import imports that stylesheet with the built theme.
json"exports": {"./themes/ocean": "./themes/ocean/ocean.js","./themes/ocean.css": "./themes/ocean/ocean.css","./themes/ocean.fonts.css": "./themes/ocean/ocean.fonts.css"}
The stylesheet can contain self-hosted @font-face rules or import a hosted stylesheet. A hosted @import is simple, but it delays loading compared with a preconnected <link>; choose that trade-off deliberately. If the app loads the same family outside the generated module, Doctor warns because it cannot prove the loader, but the warning does not fail the app.
css/* themes/ocean/ocean.fonts.css */@font-face {font-family: 'Acme Sans';src: url('./fonts/acme-sans.woff2') format('woff2');font-weight: 100 900;font-style: normal;font-display: swap;unicode-range: U+0000-00FF, U+0131, U+0152-0153;}
- Load every weight and style the theme uses. Do not let the browser synthesize bold or italic.
- Include self-hosted font files and their licenses in the packed package.
- Keep CSS and font assets side-effectful so a bundler does not remove the loader.
- Run
integration verify; it fails when an exported font stylesheet or one of its packed files cannot resolve.
Verify in an app
Before publishing, run integration verify. Then install the package in a clean app, run theme add --import, apply the generated theme, and open it in a browser. The source, packed exports, and applied result form one chain; check the last step too.
Textrenders in the named families at every weight and style — the italic face resolves, not a synthesized slant.- Every font request succeeds; nothing silently falls back to a system font.
- Color pairs still meet contrast in both light and dark mode.
Apply the theme
Install the integration, then run theme add --import with its slug and package. The command records the package owner and regenerates one app theme module that imports the built theme, production CSS, and optional font CSS. Plain theme add still copies source while that default is deprecated.
bashnpm install @acme/astryx-widgetsnpx astryx theme add ocean --import --package @acme/astryx-widgets
tsximport {Theme} from '@astryxdesign/core/theme';import {themes, defaultThemeSlug} from './astryx-themes';<Theme theme={themes[defaultThemeSlug]}>{/* app */}</Theme>
theme list shows the available, added, and default themes. Use theme use <slug> to change the default, theme remove <slug> to stop importing one, and theme eject <slug> only when the app needs an independent source fork. Applying a theme — mode, SSR, and the production build — works the same for every theme; see astryx docs use-a-theme.
Extend to customize
For ordinary customization, do not copy the package source. Import the built theme and derive a new one with defineTheme({extends: importedTheme, ...}), overriding only the tokens the app changes. Use theme eject only when the app must own an independent source fork. See astryx docs cli/integrations/building-blocks/themes/define-the-theme.
tsimport {defineTheme} from '@astryxdesign/core/theme';import {oceanTheme} from '@acme/astryx-widgets/themes/ocean';export const productTheme = defineTheme({name: 'product',extends: oceanTheme,tokens: {'--color-accent': ['#0051a3', '#4aa3ff']},});
Extend the core theme topic
Give your theme a doc topic that extends the core theme topic, so an app that lists your package sees your theme at the end of astryx docs theme. Add it with extends: 'theme' (astryx docs cli/integrations/building-blocks/docs/extend-or-replace).
javascript// docs/ocean-theme.doc.mjs/** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */export default {type: 'generic',name: 'ocean-theme',extends: 'theme',title: 'Ocean theme',description: 'Use the Ocean theme from @acme/astryx-widgets.',sections: [{id: 'use-the-ocean-theme',title: 'Use the Ocean theme',content: [{type: 'prose',text: 'Install @acme/astryx-widgets, then run astryx theme add ocean --import --package @acme/astryx-widgets and apply it from the generated app theme module.',},],},],};
Keep the section short: how to install the package, run theme add --import, apply the generated module (astryx docs cli/integrations/building-blocks/themes/use-a-theme-in-an-app), and customize with extends. When the theme uses a custom font, name the families and the optional exported font stylesheet that theme add --import imports (astryx docs cli/integrations/building-blocks/themes/fonts-and-assets). Extend theme rather than a topic another package replaces, or an app that lists that package first drops your section.
Not agent guidance
Do not put theme usage in agentDocs. Agent lines land in every app's agent file and are for guidance needed every session; a theme's install-and-use steps belong in a doc topic that people and agents read on demand. See astryx docs cli/integrations/building-blocks/configuration/agent-guidance.
Add a codemod
integration add codemod writes a codemod into a folder named after a version. Apps run it with astryx upgrade to migrate their code.
bashnpx astryx integration add codemod rename-delay --to 0.7.0
textcodemods/0.7.0/rename-delay.mjs # the codemodrename-delay.test.mjs # skipped: a test file__tests__/ # skipped: a test folder
The first add declares codemods: './codemods' in astryx.integration.mjs. The codemod's id is its path inside the version folder, without the extension: rename-delay. An id must be unique across all version folders in the package.
The loader skips *.test.*, *.spec.*, and *.fixture.* files and everything under __tests__/ or __fixtures__/, so tests can sit beside the codemod. The folder name decides when an app runs the codemod; see "Choose when a codemod runs".
Write the transform
A codemod default-exports a plain object with a type, a title, and a transform function. transform returns the new source, or null to leave the file as it is.
js// codemods/0.7.0/rename-delay.mjs/** @type {import('@astryxdesign/cli/authoring').AstryxCodemod} */export default {type: 'code',title: 'Rename AcmeCarousel delay to interval',description: 'Renames the delay prop on AcmeCarousel.',fileExtensions: ['.tsx', '.jsx'],transform(file, api) {const j = api.jscodeshift;const root = j(file.source);const props = root.find(j.JSXOpeningElement, {name: {name: 'AcmeCarousel'}}).find(j.JSXAttribute, {name: {name: 'delay'}});if (props.size() === 0) return null;props.forEach(path => {path.node.name.name = 'interval';});return root.toSource();},};
type: 'code' rewrites the app's source files that match fileExtensions. type: 'config' rewrites the app's astryx.config file instead, and runs before code codemods. title shows in the upgrade output, and api.jscodeshift is a jscodeshift instance for the file. Every field is in astryx docs authoring.
Choose when a codemod runs
Two rules decide whether an app's astryx upgrade runs your codemods: the app's Core versions, and whether the app names your package.
- Version folders are matched against the app's
@astryxdesign/coreversions, not your package's version.upgrade --from <version>runs each folder above--from, up to and including the Core version installed in the app. With Core 0.7.0 installed,--from 0.6.3runs0.7.0/, and--from 0.7.0runs nothing. A folder named after your own release, such as1.0.0/, waits until the app has Core 1.0.0. upgraderuns your codemods only when the app lists your package inintegrationsin itsastryx.config, or passes--integration @acme/astryx-widgets. Having your package installed is not enough: the run then skips your codemods with no warning.
Run codemods in an app
An app previews codemods with astryx upgrade and writes the changes with --apply. Without --apply, nothing on disk changes.
bash# Preview each codemod and the files it would changenpx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets# Write the changesnpx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets --apply
textIntegrations: @acme/astryx-widgets1 codemod to run (dry run)Applying integration codemods...Rename AcmeCarousel delay to interval (v0.7.0, @acme/astryx-widgets)! ~ src/Hero.tsx (would change)
The count includes Core's codemods for the same versions, which run first and print above Integrations:. To run only yours, as when you test it, add --codemod rename-delay.
--from is the Core version the app had before it upgraded. The run scans ./src unless the app passes --path, and it never writes a file the app marks as generated, vendored, or ignored; see astryx docs cli/commands/upgrade. upgrade --list shows only Core codemods.
An integration manifest has no hooks field. Commands that run after codemods, such as a formatter, are the app's to set, in hooks.postCodemod in its astryx.config.
Configuration
Beyond the things you ship, an integration can set how it behaves in an app: the guidance it gives the AI agents working there, and the record it keeps of each CLI run. Open one below to set it up.
Add a line of guidance
integration add agent-doc adds one line for AI agents that work in apps using your package. Apps show it in their agent file, such as AGENTS.md.
bashnpx astryx integration add agent-doc 'Use AcmeCarousel for rotating content.'
The line lands in agentDocs.append in your manifest:
js// astryx.integration.mjsexport default {components: './components',agentDocs: {append: ['Use AcmeCarousel for rotating content.'],},};
Adding the same line again changes nothing. To reword or remove a line, edit agentDocs.append by hand.
Keep within the limits
Every line costs space in each app's agent file, so the CLI keeps guidance short and few. Put detail in your docs instead.
| Limit | What happens past it |
|---|---|
| 8 lines per package | integration add fails: append may contain at most 8 lines. |
| 240 characters per line | integration add fails: must contain at most 240 Unicode code points. |
| 32 lines per app, from all its packages | init writes no agent block and prints Could not install agent docs. |
A line is one line of plain text. Line breaks, control characters, spaces at either end, and managed-block markers such as <!-- ASTRYX:START --> are rejected.
See what agents read
An app's astryx init writes your lines at the end of its managed agent block, each labeled with your package name. The CLI owns the heading, the labels, and the markers.
bashnpx astryx init --features agents
textINTEGRATIONS:- `@acme/astryx-widgets`: Use AcmeCarousel for rotating content.<!-- ASTRYX:END -->
The block collects lines from every integration the app has installed, with or without an astryx.config entry.
Refresh the block in an app
The block changes only when the app runs init, or upgrade --from <version> --apply. After an app installs a version of your package with new lines, it runs one of them.
bashnpx astryx init --features agents# Or: --from takes the Core version the app had beforenpx astryx upgrade --from 0.7.0 --apply
text[ok] Agent docs refreshed -> AGENTS.md
upgrade --apply prints that line; init prints [ok] AI agent docs installed -> AGENTS.md. Without --apply, upgrade only reports Agent docs differ from the installed Astryx and integration configuration. It refreshes the block even when Core did not change. See astryx docs cli/commands/upgrade.
Record runs with debug
Export a debug function from astryx.integration.mjs, and the CLI calls it once for each command run in an app that loads your package.
js// astryx.integration.mjsimport {appendFileSync} from 'node:fs';/** @param {import('@astryxdesign/cli/authoring').DebugEvent} event */export function debug(event) {if (event.outcome !== 'ok') {appendFileSync('acme-failed-runs.ndjson', JSON.stringify(event) + '\n');}}export default {components: './components',};
The event is a DebugEvent with command, outcome, exitCode, durationMs, error, and more, its values scrubbed (redacted: true). Every field is in astryx docs authoring.
Keep the function synchronous: the CLI calls it as the process exits and never waits for a promise. The app's own debug handler runs first, then yours. A handler that throws is skipped, and the command's output and exit code stay the same.
An app records every command only when its astryx.config names integrations or debug, as listing your package does. Otherwise your handler runs only for commands that load the app's project, such as component and docs, and not for --version or a mistyped command. In an app whose config names neither word, each of those commands also prints a warning on stderr.
debug is a named export, not a manifest field, so a CLI that does not know it ignores it and loads the rest of your manifest.
Turn off debug in an app
An app can refuse every integration's debug handler and keep its own. It sets inheritDebug in its package.json:
json{"astryx": {"inheritDebug": false}}
From then on, your handler no longer runs in that app.
Handle gap reports
Export a gapReport handler, and astryx gap-report in an app sends it each gap report, such as a missing component or variant. The handler files the report and returns a receipt.
js/** @type {import('@astryxdesign/cli/authoring').GapReportHandler} */export const gapReport = {audience: 'public',async handle(report, {signal}) {if (report.target.package !== '@acme/astryx-widgets') return {status: 'skipped'};const body = JSON.stringify(report);const response = await fetch('https://tracker.example.com/issues', {method: 'POST', body, signal});const {url} = await response.json();return {status: 'filed', url};},};
Every handler in the app gets every report, so check report.target.package and skip reports about other packages. The receipt status is one of:
| `status` | Meaning |
|---|---|
filed | You created or queued the report. Return url or message. |
routed_only | You point the caller to where to file it. url is required. |
skipped | You chose not to act, for example on a duplicate. |
The CLI waits 30 seconds, then aborts signal. A throw, a timeout, or an invalid receipt fails your delivery, and the command exits 1; the other handlers still run. Like debug, gapReport is a named export that older CLIs ignore.
Ask before filing in public
Set audience: 'public' when your handler writes somewhere the public can read. The CLI runs it only when the caller passes --confirm-public; an 'internal' handler always runs.
bashnpx astryx gap-report AcmeCarousel --category missing_variant --reason 'Need a vertical layout'
texthandlerType: integrationhandler: @acme/astryx-widgetsaudience: publicstatus: consent_requiredmessage: Rerun with --confirm-public to file this report.
With --confirm-public, the same delivery reads status: filed and shows your url. The report goes to the package named by --package, else the package that owns the component, else Core.
Fall back to issuesUrl
When the app has no gapReport handler at all, the CLI routes the report to the target package's issuesUrl from its manifest instead.
- A GitHub issues URL, such as
https://github.com/acme/widgets/issues, gets an issue filed with the GitHub CLI,gh, once the caller passes--confirm-public. - Any other URL comes back as a
routed_onlyreceipt for the caller to open. - With no
issuesUrl, the command fails:Package "@acme/astryx-widgets" provides neither a report handler nor an issues URL.
One handler anywhere in the app, from the app or from any package, turns the fallback off for every report. So does a listed package that fails to load: the CLI cannot tell whether it has a handler, so the report fails for that package instead of going to another tracker. See astryx docs cli/commands/gap-report.
Install the packed package in an app
Pack the package beside its folder, so the next pack does not ship it, then install it in an app together with Core and the CLI. The app loads your package because it is a dependency.
bash# In the packagenpm pack --pack-destination ..# In an app folder beside itnpm install @astryxdesign/core @astryxdesign/cli ../acme-astryx-widgets-1.0.0.tgz
- Install the CLI in the app too:
npx astryxruns the CLI from the app'snode_modules. - The app holds a copy, not a link. After each change, pack again and run the same
npm install. - Reading your work inside the package shows your source. The app shows what npm would publish.
Check what the app sees
From the app folder, run the commands your users run. Each should show your contribution under your package name.
bashnpx astryx component AcmeCarouselnpx astryx template --list --package @acme/astryx-widgetsnpx astryx theme list --package @acme/astryx-widgetsnpx astryx docs acmenpx astryx search deploying --type doc
| Command | Look for |
|---|---|
component AcmeCarousel | import {AcmeCarousel} from '@acme/astryx-widgets/components/AcmeCarousel'; |
template --list --package | An acme-dashboard entry with package: @acme/astryx-widgets |
theme list --package | - ocean (maintained, @acme/astryx-widgets) |
docs acme | Your section, with guides such as deploying |
search deploying --type doc | acme/deploying as the first result |
--package narrows template --list and theme list to one package. component --list ignores it and lists Core too, so look a component up by name. Last, import AcmeCarousel in the app's code and build the app, as your users will.
Run doctor in the app
npx astryx doctor checks the whole app, including every package it loaded. doctor integration validate checks your package as the app installed it.
bashnpx astryx doctornpx astryx doctor integration validate @acme/astryx-widgets
textid: implicit-integrationsstatus: [info]label: Implicitly linked integrationsmessage: 1 integration loaded from installed dependencies with no astryx.config entry: @acme/astryx-widgets@1.0.0 from dependencies, contributing components, templates, themes, docs, codemods.
- The
implicit-integrationsrow names each package the app loads with no config.[info]is expected. - The
docs-progressive-disclosurerow warns when one of your doc sections is over 32 KB. Thedoctor integrationchecks do not measure size. doctor integration validate @acme/astryx-widgetsprints[ok] No integration issues found.when the installed copy is sound.
Pick a check
Run these in the package folder. The four doctor integration checks read your source; integration verify checks the package npm would publish.
| Command | Proves | Exits 1 when | Only warns when |
|---|---|---|---|
npx astryx doctor integration validate | The manifest loads, and each root holds contributions the CLI can read | A declared root is missing (missing_root), a contribution does not load (invalid_doc, invalid_component, invalid_theme), or two templates in the package replace one Core id (ambiguous_template_replacement) | The manifest has a key this CLI does not know (unknown_manifest_key). With no astryx.integration.mjs it prints a hint and exits 0 |
npx astryx doctor integration components | No component name clashes with a Core component, and each replaces names one Core component | Core is not installed (core_not_found). In a package that declares the CLI range that turns replacement on, a replaces target is missing (missing_component_replacement_target) or invalid (invalid_component_replacement), or two components replace one Core component (ambiguous_component_replacement) | A name clashes with Core, or a component sets replaces in a package without that range (inactive_component_replacement, plus any missing, invalid, or duplicate replaces reported as a warning) |
npx astryx doctor integration templates | Each replaces names a Core template of the same type | A replaces target is missing (missing_template_replacement_target) or of the other type (invalid_template_replacement), or two templates replace one id (ambiguous_template_replacement) | A template id matches a Core id without replaces |
npx astryx doctor integration docs | Your docs tree, every link, and topic names against Core | A topic takes a Core topic name without replaces or extends, a doc is invalid (invalid_doc), or a namespace or placement fails, which hides the doc (invalid_doc_graph) | A link names no doc (invalid_doc_graph) |
npx astryx integration verify | The packed package holds every file, shows the same contributions, resolves every public import, and declares the CLI it needs | Anything validate fails on, no manifest, a file left out of the .tgz file, an import that does not resolve, or a missing CLI peer | Anything validate warns about, or a component sets replaces and the package does not declare the CLI range that turns it on (component_replaces_needs_cli) |
Pass a package name, such as npx astryx doctor integration validate @acme/astryx-widgets, to check an installed copy from an app instead.
Run every check in CI
Chain the five checks so the first failure stops the run. Install devDependencies first, because the components check needs Core.
bashnpx astryx doctor integration validate && npx astryx doctor integration components && npx astryx doctor integration templates && npx astryx doctor integration docs && npx astryx integration verify
integration verifyrunsvalidatebut none of the other three: a Core name clash, areplacesthat names no Core template, a topic that takes a Core name, or a hidden guide still passes it.- Warnings keep exit code 0, so read them before you publish.
- Bare
npx astryx doctorin the package also warns about a doc section over 32 KB, which no check above measures.
Verify the packed package
integration verify packs your package with npm, unpacks it into a temporary app, and checks that the app sees everything your source has. It publishes nothing and leaves no .tgz file or temporary folder behind.
- It runs
npm packthe waynpm publishwould, including yourprepackscript. - It checks that every contribution file is in the
.tgzfile. A root missing fromfilesfails withAdd "templates" to "files" in package.json. - It lists the components, templates, themes, docs, and codemods the temporary app sees, and compares them with your source.
- It resolves each component's
import, and each template's public import, the way Node does, and checks that the module exports the component, or a default export for a template. - For each theme, it rebuilds the source, compares the built module and production CSS with the authored outputs, and proves the module, CSS, and optional font CSS exports resolve from the packed package.
- It fails a package that needs an
@astryxdesign/clipeer and lacks it:>=0.6.6for a template that setskeywords, and>=0.6.4for a template that setsreplaces, a docs section, a placed guide, a doc section with anid, or a theme.
integration pack --check, the name this check had in 0.6, still runs it and prints a note; it will be removed in a later release. The options and exit codes are in astryx docs cli/commands/integration-verify.
Pick a version
Version your package with semver: a major version for a breaking change, a minor version for a new feature, and a patch for a fix.
bash# 1.0.0 -> 2.0.0: a breaking changenpm version major# 1.0.0 -> 1.1.0: a new featurenpm version minor# 1.0.0 -> 1.0.1: a fixnpm version patch
- Before 1.0.0, npm treats each minor version as breaking: an app that asks for
^0.6.0never gets0.7.0. Bump the minor version for a breaking change, and the patch for anything else. - Ship a codemod with each breaking change so apps can migrate; see
astryx docs cli/integrations/building-blocks/codemods.
Know what breaks apps
A breaking change stops code, commands, or links that worked in an app from working after the upgrade. For an integration, these names are part of its API.
| You change or remove | What stops working in the app |
|---|---|
A component name, such as AcmeCarousel | Imports, and npx astryx component AcmeCarousel |
A template id, such as acme-dashboard | npx astryx template acme-dashboard |
A theme slug, such as ocean | npx astryx theme add ocean --import |
A topic name or route, such as acme/deploying | Reads of the old name, and links to it from other docs |
A component's import path | Imports written from the old path |
An exports entry | Imports of that path, which fail with ERR_PACKAGE_PATH_NOT_EXPORTED |
| A prop | Code that passes it |
List each breaking change in your release notes, with the codemod that migrates it. Any changelog tool works. If package.json has a files allowlist, add CHANGELOG.md to it so npm packs it.
Declare peer ranges
Declare the Astryx packages that your code imports as peer dependencies, in peerDependencies, so the app installs one copy of each. Keep each range as wide as your tests prove.
| Peer | Declare it when | Range |
|---|---|---|
@astryxdesign/core | Your code imports Core, as a theme does with @astryxdesign/core/theme | The Core versions you test, such as ^0.6.0 |
@astryxdesign/theme-* | Your code imports that theme package | The versions you test |
@astryxdesign/cli | You ship a docs section, a placed guide, a template that sets replaces or keywords, a doc section with an id, or a theme | >=0.6.6 when you ship a template that sets keywords, otherwise >=0.6.4. Optional in peerDependenciesMeta |
json{"peerDependencies": {"@astryxdesign/core": "^0.6.0","@astryxdesign/cli": ">=0.6.4"},"peerDependenciesMeta": {"@astryxdesign/cli": {"optional": true}}}
integration verify fails a package that needs the CLI peer and lacks it, or whose range admits a stable CLI too old for what it ships. integration add doc --parent and integration add theme write the peer for you.
Support older CLIs
An app may run an older CLI than the one you build with. A CLI reads what it knows and skips the rest, but some newer files make an older CLI hide your docs.
- An unknown field in
astryx.integration.mjsis ignored with anunknown_manifest_keywarning, and the rest of the manifest still loads. - A named export that the CLI does not know is ignored with no warning, so
debugandgapReportare safe to add. - A stable CLI before 0.7.0 prints each
{@link ...}as written. - Published 0.6.3 and earlier reject a template that sets
replacesorkeywords, drop that template, and hide every doc topic your package ships. Published 0.6.4 and 0.6.5 still drop a template that setskeywords. - A stable CLI before 0.6.4 cannot read a docs section, a section
id, or a theme folder thatintegration add themewrites. It can then hide every doc topic your package ships. - Stable 0.6.3 still loads your components, but 0.6.0 cannot read the component docs that
integration add componentwrites:component AcmeCarouselfails there.
integration verify requires the CLI peer for a docs section, a placed guide, a template replaces or keywords, a doc section with an id, and a theme. A CLI too old for one of them cannot read it, and when it hides your topics, docs gives no warning.
Name codemod folders after Core versions
Name each codemod folder after the Core version whose upgrade should run it, not after your package's version. Which folders an app runs, and when, is in "Choose when a codemod runs" in astryx docs cli/integrations/building-blocks/codemods.
Check before you publish
Publish only a package that passes every check, then preview the upload with npm publish --dry-run. The dry run lists the files and the tag without uploading.
bashnpx astryx doctor integration validate && npx astryx doctor integration components && npx astryx doctor integration templates && npx astryx doctor integration docs && npx astryx integration verifynpm publish --dry-run --access public
textnpm notice Publishing to https://registry.npmjs.org/ with tag latest and public access (dry-run)+ @acme/astryx-widgets@1.0.0
Set a new version for each release; astryx docs cli/integrations/ship/versioning covers what counts as breaking. What each check proves is in astryx docs cli/integrations/ship/checks.
Keep private files out
With no files list, npm packs every file your ignore files do not exclude, including an old .tgz file or an .env file. List what ships instead.
bashnpm pkg set 'files[]=components' 'files[]=templates' 'files[]=themes' 'files[]=docs' 'files[]=codemods' 'files[]=astryx.integration.mjs'npm pack --dry-run
Listonly the roots you ship, and alwaysastryx.integration.mjs. npm adds package.json, and a README and LICENSE when you have them.- Each later
integration addappends its root to the list. integration verifyfails when a root is missing from the list:Declared templates root "templates" has 0 of 2 expected files in the pack list. Add "templates" to "files" in package.json.
Publish to npm
A scoped package is private by default, so publish it with --access public. Without --tag, npm tags the release latest, which is what npm install @acme/astryx-widgets installs.
bash# Publish a releasenpm publish --access public# Or set the access once in package.jsonnpm pkg set publishConfig.access=public
To let apps try a release first, publish it under next, then point latest at it when it is ready.
bash# Only apps that ask for next get it: npm install @acme/astryx-widgets@nextnpm publish --tag next# Make that version the defaultnpm dist-tag add @acme/astryx-widgets@1.1.0 latestnpm dist-tag ls @acme/astryx-widgets
Check the published package
Install the version you published in a clean app, and run the same commands you ran against the .tgz file.
bashmkdir check-release && cd check-releasenpm init -ynpm install @astryxdesign/core @astryxdesign/cli @acme/astryx-widgets@1.0.0npx astryx component AcmeCarouselnpx astryx doctor integration validate @acme/astryx-widgets
The full list of commands to run in an app is in astryx docs cli/integrations/ship/test-in-an-app. To help apps move to the new version, see astryx docs cli/integrations/ship/upgrading.
Update for a new Astryx release
When Astryx ships a release, bump your devDependencies, rerun the checks, widen your peer ranges, and ship a codemod for each change that breaks apps.
bash# 1. Build and test against the new releasenpm install -D @astryxdesign/cli@latest @astryxdesign/core@latest# 2. Rerun the checksnpx astryx doctor integration validatenpx astryx doctor integration docsnpx astryx integration verify# 3. Admit the new Core once the checks passnpm pkg set 'peerDependencies.@astryxdesign/core=^0.6.0 || ^0.7.0'# 4. Migrate apps across a change that breaks themnpx astryx integration add codemod rename-delay --to 0.7.0
- Run every check in
astryx docs cli/integrations/ship/checks, not only the ones shown. - Name each codemod folder after the Core version whose upgrade should run it; see
astryx docs cli/integrations/building-blocks/codemods. - Release the result as a new version of your package; see
astryx docs cli/integrations/ship/publishing.
Upgrade an app
After an app installs a new Core, astryx upgrade runs the codemods for the versions it crossed. It is a dry run by default; --apply writes the changes.
bash# Preview what would change; nothing is writtennpx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets# Write the changesnpx astryx upgrade --from 0.6.3 --integration @acme/astryx-widgets --apply
textIntegrations: @acme/astryx-widgets1 codemod to runApplying integration codemods...Rename AcmeCarousel delay to interval (v0.7.0, @acme/astryx-widgets)[ok] [ok] src/Hero.tsx
--fromis the@astryxdesign/coreversion the app had before, and the target is the installed Core.--integrationnames your package. Without it, or anintegrationsentry inastryx.config, the upgrade skips your codemods, even when the app has your package installed.- The count includes Core's codemods for the same versions, which run first.
- Put this command in your release notes.
Know which files upgrades skip
Upgrades never write files that the app marks as generated, vendored, or ignored. When such a file still needs a codemod change, the upgrade stops with ERR_CODEMOD_PROTECTED and exit code 1.
- Generated: an
@generatedcomment at the top of the file, orlinguist-generatedin.gitattributes. - Vendored:
linguist-vendoredin.gitattributes. - Ignored: a match in
.gitignoreor.hgignore.
text! ! src/gen/Gen.tsx - protected by @generated in the leading comment blockERR_CODEMOD_PROTECTED: 1 protected file still requires a codemod change:src/gen/Gen.tsx — @generated in the leading comment blockUpgrade incomplete: protected changes remain
The upgrade still writes every other file. Its options and exit codes are in astryx docs cli/commands/upgrade.
Fix setup errors
These appear while you start the package or run the CLI in it. Each row quotes what the CLI prints.
| Message | Fix |
|---|---|
No package.json found. Run this command inside an integration package. | Run npm init -y and npm pkg set name=@acme/astryx-widgets first, in the package folder. |
Error: Could not find @astryxdesign/core package | Component commands read Core. Run npm install -D @astryxdesign/core in the package. |
core_not_found: Could not resolve @astryxdesign/core, so component names could not be checked. | Same fix. The check exits 1. |
Error: unknown subcommand 'integration verify', or Error: Pass --check to verify the integration tarball. | Your CLI is older than integration verify. Run npm install -D @astryxdesign/cli@latest. On the older CLI, the same check is integration pack --check. |
Note: integration pack --check is deprecated. | The old name still runs the same check. Switch to npx astryx integration verify; the old name will be removed in a later release. |
Fix integration verify failures
npx astryx integration verify prints each failure as a [fail] line. Add --json to see its issue code, such as component_export_missing.
| Message | Fix |
|---|---|
component_export_missing: Component "AcmeCarousel" advertises import "@acme/astryx-widgets/components/AcmeCarousel", but that packed module does not export "AcmeCarousel". | package.json has no exports map, or the module does not export the name. Keep export function AcmeCarousel, and add the entry: npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx'. |
template_export_missing: Template "acme-dashboard" public import "@acme/astryx-widgets/templates/acme-dashboard" does not have a default export. | Run npm pkg set 'exports[./templates/acme-dashboard]=./templates/acme-dashboard.tsx', and keep export default in the source. Start new packages with "exports": {} so each add writes these. |
component_import_unresolvable or template_import_unresolvable: …Package subpath './components/AcmeCarousel' is not defined by "exports"… | The exports map has no entry for it. Run npm pkg set 'exports[./components/AcmeCarousel]=./components/AcmeCarousel.tsx', or 'exports[./templates/acme-dashboard]=./templates/acme-dashboard.tsx' for a template. |
component_import_unresolvable: …but a consumer cannot resolve it: Cannot find package '@acme/old-name' | You renamed the package after the add wrote each doc's import. Change import in every component doc to the new name. |
Declared templates root "templates" has 0 of 2 expected files in the pack list. Add "templates" to "files" in package.json. | Run npm pkg set 'files[]=templates'. |
docs_tree_needs_cli: The package ships a namespace doc or a placed guide but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.4' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. |
replaces_needs_cli: The package has a template that sets replaces but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.4' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. A stable CLI before 0.6.4 rejects replaces, drops that template, and hides your doc topics. |
component_replaces_needs_cli (a warning): The package has a component that sets replaces but declares no @astryxdesign/cli peer, or a peer range that admits an earlier CLI. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.7' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json to turn the replacement on. Until then the component keeps its own name and the Core component stays selected; a component named like its target stays ambiguous by that bare name. |
keywords_needs_cli: The package has a template that sets keywords but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.6' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. A stable CLI before 0.6.6 rejects keywords and drops that template, and one before 0.6.4 also hides your doc topics. |
themes_need_cli: The package ships a theme but declares no @astryxdesign/cli peer. | Run npm pkg set 'peerDependencies.@astryxdesign/cli=>=0.6.4' and npm pkg set 'peerDependenciesMeta.@astryxdesign/cli.optional=true' --json. A stable CLI before 0.6.4 cannot read typed theme descriptors, so it drops your themes and can hide your doc topics. |
section_ids_need_cli: The package has a doc section that sets id but declares no @astryxdesign/cli peer. | The same fix as themes_need_cli, or drop the section ids: a stable CLI before 0.6.4 rejects them and hides your doc topics. |
Fix problems in an app
These appear in an app that installs your package, or in the doctor checks that predict them.
| Symptom | Fix |
|---|---|
Error: Template "dashboard" is ambiguous — narrow it with --type and/or --package. | Your template id matches a Core id. Rename it, such as acme-dashboard, or set replaces: 'dashboard' to take its place. npx astryx doctor integration templates warns about this before you publish. |
invalid_doc_graph: placement.parent "namespace:nope" names no namespace; @acme/astryx-widgets declares "acme". | The guide stays hidden until its placement names a namespace and slot your package declares. For "…" names no doc, fix the link target. |
Error: Unknown topic "acme". The docs of @acme/astryx-widgets did not load; run astryx doctor integration docs in that package to see why. | One invalid doc hides all of your docs. Run npx astryx doctor integration docs in the package and fix the invalid_doc it names. |
npx astryx upgrade runs none of your codemods | The app must list your package in integrations in astryx.config, or pass --integration @acme/astryx-widgets. It runs a codemod folder only when its version is after --from and at most the installed @astryxdesign/core. |
| Your docs section, templates, or themes are missing only in some apps | Those apps run a CLI too old to read them: a stable CLI before 0.6.6 for a template that sets keywords, or before 0.6.4 for a template that sets replaces, a docs section, or a theme. Update @astryxdesign/cli there, and keep your optional @astryxdesign/cli peer so npm warns about an old CLI. |
What each check catches is in astryx docs cli/integrations/ship/checks.