@astryxdesign/cli 0.1.1 → 0.1.2-canary.043f3fd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +31 -8
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +87 -28
- package/src/api/doctor.mjs +7 -22
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +302 -0
- package/src/api/layout.test.mjs +241 -0
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +421 -122
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +2 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +2 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +81 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +15 -6
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +241 -224
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -239
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +18 -0
- package/src/config.test.mjs +100 -0
- package/src/index.mjs +13 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +105 -0
- package/src/lib/error-codes.mjs +18 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +132 -0
- package/src/lib/integrations.test.mjs +135 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +15 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +70 -0
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -252
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -456
- package/src/commands/gap-report.test.mjs +0 -163
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -86
- package/src/lib/config.test.mjs +0 -42
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import * as os from 'node:os';
|
|
7
|
+
import {
|
|
8
|
+
discoverIntegrationComponents,
|
|
9
|
+
discoverOwnedComponents,
|
|
10
|
+
findIntegrationComponentDoc,
|
|
11
|
+
findIntegrationComponentSource,
|
|
12
|
+
CORE_PACKAGE,
|
|
13
|
+
} from '../lib/component-discovery.mjs';
|
|
14
|
+
|
|
15
|
+
// The api `component()` reads integrations via Project.load(). In the vitest
|
|
16
|
+
// environment, Vite's `server.fs.allow` only permits loading modules from
|
|
17
|
+
// under `node_modules`, so a real astryx.config.mjs at a tmp root cannot be
|
|
18
|
+
// imported. We therefore mock Project.load to return a project whose
|
|
19
|
+
// `loadedIntegrations` are resolved (exactly the shape lib/integrations.mjs
|
|
20
|
+
// produces) while keeping the integration's `components` dir on disk under
|
|
21
|
+
// node_modules so its `.doc.mjs` files load normally.
|
|
22
|
+
const projectLoadMock = vi.fn();
|
|
23
|
+
vi.mock('../lib/project.mjs', () => ({
|
|
24
|
+
Project: {load: (...args) => projectLoadMock(...args)},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Import the api AFTER the mock is registered.
|
|
28
|
+
const {component} = await import('../api/component.mjs');
|
|
29
|
+
|
|
30
|
+
let tmpDir;
|
|
31
|
+
|
|
32
|
+
const INTEGRATION_NAME = '@test/meta';
|
|
33
|
+
const INTEGRATION_ISSUES = 'https://example.com/meta/issues';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a consumer fixture:
|
|
37
|
+
* - packages/core symlinked to the real @astryxdesign/core (loadDocs source)
|
|
38
|
+
* - node_modules/@test/meta with a `components` dir using the same-stem
|
|
39
|
+
* source/doc convention (MetaAppShell.tsx + MetaAppShell.doc.mjs)
|
|
40
|
+
* Returns the absolute `components` dir so the Project.load mock can hand back a
|
|
41
|
+
* resolved integration entry.
|
|
42
|
+
*/
|
|
43
|
+
function createFixture({withSource = true, extraComponent = null} = {}) {
|
|
44
|
+
const realCoreDir = path.resolve(import.meta.dirname, '..', '..', '..', 'core');
|
|
45
|
+
const coreDir = path.join(tmpDir, 'packages', 'core');
|
|
46
|
+
fs.mkdirSync(path.dirname(coreDir), {recursive: true});
|
|
47
|
+
fs.symlinkSync(realCoreDir, coreDir);
|
|
48
|
+
|
|
49
|
+
const intDir = path.join(tmpDir, 'node_modules', '@test', 'meta');
|
|
50
|
+
const compDir = path.join(intDir, 'components');
|
|
51
|
+
fs.mkdirSync(compDir, {recursive: true});
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(intDir, 'package.json'),
|
|
54
|
+
JSON.stringify({name: INTEGRATION_NAME, version: '1.2.3'}),
|
|
55
|
+
);
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.join(compDir, 'MetaAppShell.doc.mjs'),
|
|
58
|
+
`export const docs = {\n name: 'MetaAppShell',\n usage: { description: 'Meta-flavored app shell.' },\n props: [{ name: 'title', type: 'string', description: 'Header title' }],\n};\n`,
|
|
59
|
+
);
|
|
60
|
+
if (withSource) {
|
|
61
|
+
fs.writeFileSync(
|
|
62
|
+
path.join(compDir, 'MetaAppShell.tsx'),
|
|
63
|
+
"'use client';\nexport function MetaAppShell() { return null; }\n",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (extraComponent) {
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(compDir, `${extraComponent}.doc.mjs`),
|
|
69
|
+
`export const docs = {\n name: '${extraComponent}',\n usage: { description: '${extraComponent} from meta.' },\n};\n`,
|
|
70
|
+
);
|
|
71
|
+
fs.writeFileSync(
|
|
72
|
+
path.join(compDir, `${extraComponent}.tsx`),
|
|
73
|
+
`export function ${extraComponent}() { return null; }\n`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const integration = {
|
|
78
|
+
name: INTEGRATION_NAME,
|
|
79
|
+
version: '1.2.3',
|
|
80
|
+
components: compDir,
|
|
81
|
+
templates: undefined,
|
|
82
|
+
codemods: undefined,
|
|
83
|
+
issuesUrl: INTEGRATION_ISSUES,
|
|
84
|
+
};
|
|
85
|
+
projectLoadMock.mockResolvedValue({
|
|
86
|
+
integrations: [INTEGRATION_NAME],
|
|
87
|
+
loadedIntegrations: [integration],
|
|
88
|
+
});
|
|
89
|
+
return {coreDir, intDir, compDir, integration};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-ownership-'));
|
|
94
|
+
projectLoadMock.mockReset();
|
|
95
|
+
// Default: no integrations (core only).
|
|
96
|
+
projectLoadMock.mockResolvedValue({integrations: [], loadedIntegrations: []});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('discoverIntegrationComponents (ownership records)', () => {
|
|
104
|
+
it('records name, package, sourcePath, and issuesUrl for same-stem components', () => {
|
|
105
|
+
const {integration, compDir} = createFixture();
|
|
106
|
+
const records = discoverIntegrationComponents(integration);
|
|
107
|
+
expect(records).toHaveLength(1);
|
|
108
|
+
const rec = records[0];
|
|
109
|
+
expect(rec.name).toBe('MetaAppShell');
|
|
110
|
+
expect(rec.package).toBe(INTEGRATION_NAME);
|
|
111
|
+
expect(rec.issuesUrl).toBe(INTEGRATION_ISSUES);
|
|
112
|
+
expect(rec.sourcePath).toBe(path.join(compDir, 'MetaAppShell.tsx'));
|
|
113
|
+
expect(fs.existsSync(rec.sourcePath)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('records sourcePath: null when the integration ships docs without source', () => {
|
|
117
|
+
const {integration} = createFixture({withSource: false});
|
|
118
|
+
const [rec] = discoverIntegrationComponents(integration);
|
|
119
|
+
expect(rec.sourcePath).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('discoverOwnedComponents (core + integrations)', () => {
|
|
124
|
+
it('marks core components with the core package and integration components with their owner', () => {
|
|
125
|
+
const {coreDir, integration} = createFixture();
|
|
126
|
+
const records = discoverOwnedComponents(coreDir, [integration]);
|
|
127
|
+
const core = records.find(r => r.package === CORE_PACKAGE);
|
|
128
|
+
expect(core).toBeTruthy();
|
|
129
|
+
expect(core.issuesUrl).toBeUndefined();
|
|
130
|
+
const meta = records.find(r => r.name === 'MetaAppShell');
|
|
131
|
+
expect(meta.package).toBe(INTEGRATION_NAME);
|
|
132
|
+
expect(meta.issuesUrl).toBe(INTEGRATION_ISSUES);
|
|
133
|
+
expect(meta.sourcePath).toContain('MetaAppShell.tsx');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('findIntegrationComponentDoc / Source', () => {
|
|
138
|
+
it('finds doc + source by name', () => {
|
|
139
|
+
const {integration} = createFixture();
|
|
140
|
+
expect(findIntegrationComponentDoc(integration, 'MetaAppShell')).toContain('MetaAppShell.doc.mjs');
|
|
141
|
+
expect(findIntegrationComponentSource(integration, 'MetaAppShell')).toContain('MetaAppShell.tsx');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns null source when none present', () => {
|
|
145
|
+
const {integration} = createFixture({withSource: false});
|
|
146
|
+
expect(findIntegrationComponentSource(integration, 'MetaAppShell')).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('component() — integration ownership via config', () => {
|
|
151
|
+
it('discovers a config integration component by package ownership (detail)', async () => {
|
|
152
|
+
createFixture();
|
|
153
|
+
const result = await component('MetaAppShell', {cwd: tmpDir});
|
|
154
|
+
expect(result.type).toBe('component.detail');
|
|
155
|
+
expect(result.data.name).toBe('MetaAppShell');
|
|
156
|
+
expect(result.data.package).toBe(INTEGRATION_NAME);
|
|
157
|
+
expect(result.data.sourceAvailable).toBe(true);
|
|
158
|
+
expect(result.data.import).toBe(`${INTEGRATION_NAME}/MetaAppShell`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('--package resolves the integration component', async () => {
|
|
162
|
+
createFixture();
|
|
163
|
+
const result = await component('MetaAppShell', {cwd: tmpDir, package: INTEGRATION_NAME});
|
|
164
|
+
expect(result.type).toBe('component.detail');
|
|
165
|
+
expect(result.data.package).toBe(INTEGRATION_NAME);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('--source returns the integration source when available', async () => {
|
|
169
|
+
createFixture();
|
|
170
|
+
const result = await component('MetaAppShell', {cwd: tmpDir, source: true});
|
|
171
|
+
expect(result.type).toBe('component.detail.source');
|
|
172
|
+
expect(result.data.component).toBe('MetaAppShell');
|
|
173
|
+
expect(result.data.source).toContain('MetaAppShell');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('--source throws ERR_NO_SOURCE when the integration ships no source', async () => {
|
|
177
|
+
createFixture({withSource: false});
|
|
178
|
+
await expect(
|
|
179
|
+
component('MetaAppShell', {cwd: tmpDir, source: true}),
|
|
180
|
+
).rejects.toMatchObject({code: 'ERR_NO_SOURCE'});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('errors with candidate packages when a name is ambiguous (core + integration)', async () => {
|
|
184
|
+
// 'AppShell' exists in core; add a same-named integration component.
|
|
185
|
+
createFixture({extraComponent: 'AppShell'});
|
|
186
|
+
let caught;
|
|
187
|
+
try {
|
|
188
|
+
await component('AppShell', {cwd: tmpDir});
|
|
189
|
+
} catch (e) {
|
|
190
|
+
caught = e;
|
|
191
|
+
}
|
|
192
|
+
expect(caught).toBeTruthy();
|
|
193
|
+
expect(caught.code).toBe('ERR_UNKNOWN_COMPONENT');
|
|
194
|
+
const pkgs = (caught.suggestions ?? []).map(s => s.name);
|
|
195
|
+
expect(pkgs).toContain(CORE_PACKAGE);
|
|
196
|
+
expect(pkgs).toContain(INTEGRATION_NAME);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('--package disambiguates an ambiguous name to core', async () => {
|
|
200
|
+
createFixture({extraComponent: 'AppShell'});
|
|
201
|
+
const result = await component('AppShell', {cwd: tmpDir, package: CORE_PACKAGE});
|
|
202
|
+
expect(result.type).toBe('component.detail');
|
|
203
|
+
expect(result.data.package).toBe(CORE_PACKAGE);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('--package disambiguates an ambiguous name to the integration', async () => {
|
|
207
|
+
createFixture({extraComponent: 'AppShell'});
|
|
208
|
+
const result = await component('AppShell', {cwd: tmpDir, package: INTEGRATION_NAME});
|
|
209
|
+
expect(result.type).toBe('component.detail');
|
|
210
|
+
expect(result.data.package).toBe(INTEGRATION_NAME);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('JSON list includes integration components as {name, package} objects', async () => {
|
|
214
|
+
createFixture();
|
|
215
|
+
const result = await component(undefined, {cwd: tmpDir, list: true});
|
|
216
|
+
expect(result.type).toBe('component.list');
|
|
217
|
+
const allEntries = Object.values(result.data).flat();
|
|
218
|
+
for (const entry of allEntries) {
|
|
219
|
+
expect(typeof entry.name).toBe('string');
|
|
220
|
+
expect(typeof entry.package).toBe('string');
|
|
221
|
+
}
|
|
222
|
+
const meta = allEntries.find(e => e.name === 'MetaAppShell');
|
|
223
|
+
expect(meta).toBeTruthy();
|
|
224
|
+
expect(meta.package).toBe(INTEGRATION_NAME);
|
|
225
|
+
expect(allEntries.some(e => e.package === CORE_PACKAGE)).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* astryx discover searchterm Search across all packages
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {loadConfig} from '../lib/config.mjs';
|
|
14
13
|
import {formatFull, formatBrief, formatCompact} from '../lib/component-format.mjs';
|
|
15
14
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
16
15
|
import {cliError} from '../lib/cli-error.mjs';
|
|
@@ -45,27 +44,17 @@ export function registerDiscover(program) {
|
|
|
45
44
|
switch (result.type) {
|
|
46
45
|
case 'discover.list': {
|
|
47
46
|
if (result.data.length === 0) {
|
|
48
|
-
const config = await loadConfig();
|
|
49
47
|
humanLog('');
|
|
50
|
-
if (
|
|
51
|
-
humanLog('No
|
|
48
|
+
if (result.meta && result.meta.configured === false) {
|
|
49
|
+
humanLog('No integrations configured.');
|
|
52
50
|
humanLog('');
|
|
53
|
-
humanLog('Add
|
|
51
|
+
humanLog('Add integration package names to astryx.config.mjs:');
|
|
54
52
|
humanLog('');
|
|
55
53
|
humanLog(' export default {');
|
|
56
|
-
humanLog("
|
|
54
|
+
humanLog(" integrations: ['@scope/your-integration'],");
|
|
57
55
|
humanLog(' };');
|
|
58
56
|
} else {
|
|
59
|
-
humanLog('No external
|
|
60
|
-
humanLog('');
|
|
61
|
-
humanLog('Packages opt in by adding an "astryx" field to package.json:');
|
|
62
|
-
humanLog('');
|
|
63
|
-
humanLog(' {');
|
|
64
|
-
humanLog(' "astryx": {');
|
|
65
|
-
humanLog(' "docs": "./src",');
|
|
66
|
-
humanLog(' "category": "Common"');
|
|
67
|
-
humanLog(' }');
|
|
68
|
-
humanLog(' }');
|
|
57
|
+
humanLog('No external components found in configured integrations.');
|
|
69
58
|
}
|
|
70
59
|
humanLog('');
|
|
71
60
|
} else {
|
|
@@ -132,7 +132,7 @@ describe('doctor — individual checks', () => {
|
|
|
132
132
|
);
|
|
133
133
|
const configPath = path.join(fixtureDir, 'astryx.config.mjs');
|
|
134
134
|
try {
|
|
135
|
-
fs.writeFileSync(configPath, 'export default {
|
|
135
|
+
fs.writeFileSync(configPath, 'export default { integrations: [] };');
|
|
136
136
|
const res = await checkConfig({cwd: fixtureDir, configPath});
|
|
137
137
|
if (res.status !== 'pass') {
|
|
138
138
|
throw new Error(`expected pass, got ${res.status}: ${res.message}`);
|
|
@@ -158,13 +158,13 @@ describe('doctor — individual checks', () => {
|
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
it('config: FAIL when
|
|
161
|
+
it('config: FAIL when default export is not an object', async () => {
|
|
162
162
|
const fixtureDir = fs.mkdtempSync(
|
|
163
163
|
path.join(path.dirname(new URL(import.meta.url).pathname), '__doctor_cfg_'),
|
|
164
164
|
);
|
|
165
165
|
const configPath = path.join(fixtureDir, 'astryx.config.mjs');
|
|
166
166
|
try {
|
|
167
|
-
fs.writeFileSync(configPath, 'export default
|
|
167
|
+
fs.writeFileSync(configPath, 'export default 123;');
|
|
168
168
|
const res = await checkConfig({cwd: fixtureDir, configPath});
|
|
169
169
|
expect(res.status).toBe('fail');
|
|
170
170
|
} finally {
|
package/src/commands/init.mjs
CHANGED
|
@@ -29,6 +29,37 @@ import {requireInteractive} from '../utils/interactive.mjs';
|
|
|
29
29
|
const VALID_FEATURES = ['agents', 'theme', 'template'];
|
|
30
30
|
const run = getRunPrefix();
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Build the "Next steps" lines printed at the end of `astryx init`.
|
|
34
|
+
*
|
|
35
|
+
* Theme guidance must match the runtime recommendation emitted by core's
|
|
36
|
+
* <Theme> component (packages/core/src/theme/Theme.tsx): the pre-built theme
|
|
37
|
+
* path (`/built` import + `theme.css`) plus the base CSS import, so users
|
|
38
|
+
* don't end up with an unstyled app or the slower runtime style-injection
|
|
39
|
+
* path. See https://github.com/facebook/astryx/issues/3080.
|
|
40
|
+
*
|
|
41
|
+
* Exported for testing.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} runPrefix package-manager run prefix (e.g. `npx`)
|
|
44
|
+
* @returns {string[]} ordered list of human-facing lines
|
|
45
|
+
*/
|
|
46
|
+
export function getNextSteps(runPrefix) {
|
|
47
|
+
return [
|
|
48
|
+
'',
|
|
49
|
+
' Next steps:',
|
|
50
|
+
" 1. Import base styles: import '@astryxdesign/core/reset.css'",
|
|
51
|
+
" and import '@astryxdesign/core/astryx.css'",
|
|
52
|
+
" 2. Import components: import { Button } from '@astryxdesign/core'",
|
|
53
|
+
' 3. Optionally add a theme (use the pre-built path for performance):',
|
|
54
|
+
" import { neutralTheme } from '@astryxdesign/theme-neutral/built'",
|
|
55
|
+
" import '@astryxdesign/theme-neutral/theme.css'",
|
|
56
|
+
' <Theme theme={neutralTheme}>...</Theme>',
|
|
57
|
+
` For custom themes, run \`${runPrefix} astryx theme build <file>\` to generate the built artifacts.`,
|
|
58
|
+
` 4. ${runPrefix} astryx --help for all commands`,
|
|
59
|
+
'',
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
function isCancel(value) {
|
|
33
64
|
if (p.isCancel(value)) {
|
|
34
65
|
p.cancel('Setup cancelled.');
|
|
@@ -250,13 +281,8 @@ export function registerInit(program) {
|
|
|
250
281
|
// Outro
|
|
251
282
|
p.outro('Design system initialized!');
|
|
252
283
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
humanLog(' 2. Optionally add a theme:');
|
|
257
|
-
humanLog(" import { neutralTheme } from '@astryxdesign/theme-neutral'");
|
|
258
|
-
humanLog(' <Theme theme={neutralTheme}>...</Theme>');
|
|
259
|
-
humanLog(` 3. ${run} astryx --help for all commands`);
|
|
260
|
-
humanLog('');
|
|
284
|
+
for (const line of getNextSteps(run)) {
|
|
285
|
+
humanLog(line);
|
|
286
|
+
}
|
|
261
287
|
});
|
|
262
288
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Regression test for `astryx init` "Next steps" theme guidance.
|
|
5
|
+
*
|
|
6
|
+
* The init command previously steered users toward the slower runtime
|
|
7
|
+
* style-injection path:
|
|
8
|
+
*
|
|
9
|
+
* import { neutralTheme } from '@astryxdesign/theme-neutral'
|
|
10
|
+
* <Theme theme={neutralTheme}>...</Theme>
|
|
11
|
+
*
|
|
12
|
+
* ...which contradicted the runtime console warning emitted by core's
|
|
13
|
+
* <Theme> component (packages/core/src/theme/Theme.tsx) recommending the
|
|
14
|
+
* pre-built path, and left users with an unstyled app because the base CSS
|
|
15
|
+
* imports were never mentioned (facebook/astryx#3080).
|
|
16
|
+
*
|
|
17
|
+
* These assertions lock in the corrected guidance: base CSS imports, the
|
|
18
|
+
* pre-built (`/built` + `theme.css`) theme path, and the custom-theme build
|
|
19
|
+
* command.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {describe, it, expect} from 'vitest';
|
|
23
|
+
import {getNextSteps} from './init.mjs';
|
|
24
|
+
|
|
25
|
+
describe('init Next steps theme guidance', () => {
|
|
26
|
+
const text = getNextSteps('npx').join('\n');
|
|
27
|
+
|
|
28
|
+
it('mentions the base CSS imports so the app is not left unstyled', () => {
|
|
29
|
+
expect(text).toContain("'@astryxdesign/core/reset.css'");
|
|
30
|
+
expect(text).toContain("'@astryxdesign/core/astryx.css'");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses the pre-built theme path matching the runtime recommendation', () => {
|
|
34
|
+
expect(text).toContain("'@astryxdesign/theme-neutral/built'");
|
|
35
|
+
expect(text).toContain("'@astryxdesign/theme-neutral/theme.css'");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('mentions building custom themes via `astryx theme build`', () => {
|
|
39
|
+
expect(text).toContain('astryx theme build <file>');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not steer users to the runtime style-injection import', () => {
|
|
43
|
+
// The bare source import (no `/built`) is the slow runtime-injection path.
|
|
44
|
+
expect(text).not.toContain("from '@astryxdesign/theme-neutral'");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -91,15 +91,6 @@ describe('--json contract: rejects before side effects', () => {
|
|
|
91
91
|
expect(fs.readdirSync(tmpDir)).toEqual([]);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it('astryx gap-report setup --json rejects before prompting', () => {
|
|
95
|
-
const {status, stdout} = runCli(['gap-report', 'setup', '--json'], {cwd: tmpDir});
|
|
96
|
-
expect(status).toBe(1);
|
|
97
|
-
const parsed = parseJson(stdout);
|
|
98
|
-
expect(parsed.error).toMatch(/gap-report setup/);
|
|
99
|
-
// No astryx.config.mjs written.
|
|
100
|
-
expect(fs.existsSync(path.join(tmpDir, 'astryx.config.mjs'))).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
94
|
it('astryx theme --json (parent, no subcommand) rejects without printing help', () => {
|
|
104
95
|
const {status, stdout, stderr} = runCli(['theme', '--json'], {cwd: tmpDir});
|
|
105
96
|
expect(status).toBe(1);
|
|
@@ -145,29 +136,6 @@ describe('--json contract: supported commands emit valid envelopes', () => {
|
|
|
145
136
|
expect(Array.isArray(parsed.data.jsonSupported)).toBe(true);
|
|
146
137
|
});
|
|
147
138
|
|
|
148
|
-
it('astryx gap-report --json --list-categories returns a typed envelope', () => {
|
|
149
|
-
const {status, stdout} = runCli(['gap-report', '--json', '--list-categories'], {cwd: tmpDir});
|
|
150
|
-
expect(status).toBe(0);
|
|
151
|
-
const parsed = parseJson(stdout);
|
|
152
|
-
expect(parsed.type).toBe('gap-report.categories');
|
|
153
|
-
expect(Array.isArray(parsed.data)).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('astryx gap-report --json without required flags emits a structured error (no clack)', () => {
|
|
157
|
-
// Configure gap-report so it doesn't error on "disabled" — we want to
|
|
158
|
-
// hit the "interactive prompt would start" branch.
|
|
159
|
-
fs.writeFileSync(
|
|
160
|
-
path.join(tmpDir, 'astryx.config.mjs'),
|
|
161
|
-
'export default { gapReport: { command: "true" } };\n',
|
|
162
|
-
);
|
|
163
|
-
const {status, stdout} = runCli(['gap-report', '--json'], {cwd: tmpDir});
|
|
164
|
-
expect(status).toBe(1);
|
|
165
|
-
const parsed = parseJson(stdout);
|
|
166
|
-
expect(parsed.error).toMatch(/component/i);
|
|
167
|
-
// Must not contain clack-rendered prompt characters.
|
|
168
|
-
expect(stdout).not.toMatch(/Which component/);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
139
|
it('astryx upgrade --json --list returns the codemod list', () => {
|
|
172
140
|
const {status, stdout} = runCli(['upgrade', '--json', '--list'], {cwd: tmpDir});
|
|
173
141
|
expect(status).toBe(0);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file layout command — thin CLI wrapper around api/layout.mjs.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* astryx layout expand "<expr>" [path] compressed expression → validated TSX
|
|
8
|
+
* astryx layout check "<expr>" validate + echo both canonical surfaces
|
|
9
|
+
* astryx layout grammar agent cheatsheet (alias table is branch-generated)
|
|
10
|
+
*
|
|
11
|
+
* The expression argument may also come from --file or stdin (`-`),
|
|
12
|
+
* which is how multi-line outline (XLO) input usually arrives.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
17
|
+
import {cliError} from '../lib/cli-error.mjs';
|
|
18
|
+
import {layoutExpand, layoutCheck, layoutGrammar} from '../api/layout.mjs';
|
|
19
|
+
|
|
20
|
+
/** Resolve the expression from arg, --file, or stdin ('-'). */
|
|
21
|
+
async function readExpression(expr, options) {
|
|
22
|
+
if (options.file) return fs.readFileSync(options.file, 'utf-8');
|
|
23
|
+
if (expr === '-' || (!expr && !process.stdin.isTTY)) {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
26
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
27
|
+
}
|
|
28
|
+
return expr;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerLayout(program) {
|
|
32
|
+
const layoutCmd = program
|
|
33
|
+
.command('layout')
|
|
34
|
+
.description('Generate XDS layouts from compressed expressions (XLE/XLO)');
|
|
35
|
+
|
|
36
|
+
layoutCmd
|
|
37
|
+
.command('expand [expression] [path]')
|
|
38
|
+
.description('Expand a layout expression into validated XDS TSX')
|
|
39
|
+
.option('--file <file>', 'Read the expression from a file')
|
|
40
|
+
.option('--form <form>', 'Input surface: compact, outline, or auto', 'auto')
|
|
41
|
+
.option('--name <name>', 'Generated component name (PascalCase)', 'GeneratedLayout')
|
|
42
|
+
.option('--loose', 'Downgrade unknown {block} hints to TODO placeholders')
|
|
43
|
+
.action(async (expression, targetPath, options) => {
|
|
44
|
+
const json = program.opts().json || false;
|
|
45
|
+
const source = await readExpression(expression, options);
|
|
46
|
+
if (!source || source.trim() === '') {
|
|
47
|
+
cliError('No layout expression given — pass it as an argument, via --file, or on stdin');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
result = await layoutExpand(source, {
|
|
53
|
+
targetPath,
|
|
54
|
+
form: options.form,
|
|
55
|
+
loose: options.loose || false,
|
|
56
|
+
name: options.name,
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (json) return jsonOut(result.type, result.data);
|
|
64
|
+
|
|
65
|
+
for (const warning of result.data.warnings) humanLog(`⚠ ${warning}`);
|
|
66
|
+
if (result.data.written) {
|
|
67
|
+
humanLog(`\n✓ Expanded to ${result.data.written}`);
|
|
68
|
+
humanLog(` Components: ${result.data.componentsUsed.join(', ')}`);
|
|
69
|
+
if (result.data.todos.length > 0) {
|
|
70
|
+
humanLog(` TODOs: ${result.data.todos.length} (search for "TODO(xle)")`);
|
|
71
|
+
}
|
|
72
|
+
humanLog('');
|
|
73
|
+
} else {
|
|
74
|
+
humanLog(result.data.code);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
layoutCmd
|
|
79
|
+
.command('check [expression]')
|
|
80
|
+
.description('Validate a layout expression and echo canonical compact/outline forms')
|
|
81
|
+
.option('--file <file>', 'Read the expression from a file')
|
|
82
|
+
.option('--form <form>', 'Input surface: compact, outline, or auto', 'auto')
|
|
83
|
+
.option('--loose', 'Downgrade unknown {block} hints to TODO placeholders')
|
|
84
|
+
.action(async (expression, options) => {
|
|
85
|
+
const json = program.opts().json || false;
|
|
86
|
+
const source = await readExpression(expression, options);
|
|
87
|
+
if (!source || source.trim() === '') {
|
|
88
|
+
cliError('No layout expression given — pass it as an argument, via --file, or on stdin');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let result;
|
|
92
|
+
try {
|
|
93
|
+
result = await layoutCheck(source, {
|
|
94
|
+
form: options.form,
|
|
95
|
+
loose: options.loose || false,
|
|
96
|
+
cwd: process.cwd(),
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (json) return jsonOut(result.type, result.data);
|
|
103
|
+
|
|
104
|
+
const {valid, form, errors, warnings, compact, outline} = result.data;
|
|
105
|
+
if (!valid) {
|
|
106
|
+
humanLog(`\n✗ Invalid (${errors.length} error${errors.length === 1 ? '' : 's'}):`);
|
|
107
|
+
for (const e of errors) {
|
|
108
|
+
humanLog(` - ${e.formatted}`);
|
|
109
|
+
if (e.suggestions?.length > 0) humanLog(` did you mean: ${e.suggestions.join(', ')}?`);
|
|
110
|
+
}
|
|
111
|
+
humanLog('');
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
humanLog(`\n✓ Valid (parsed as ${form})`);
|
|
116
|
+
for (const warning of warnings) humanLog(`⚠ ${warning}`);
|
|
117
|
+
humanLog('\ncompact:');
|
|
118
|
+
humanLog(` ${compact}`);
|
|
119
|
+
humanLog('\noutline:');
|
|
120
|
+
humanLog(outline.split('\n').map(l => ` ${l}`).join('\n'));
|
|
121
|
+
humanLog('');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
layoutCmd
|
|
125
|
+
.command('grammar')
|
|
126
|
+
.description('Print the XLE/XLO cheatsheet (alias table generated from this branch)')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
const json = program.opts().json || false;
|
|
129
|
+
let result;
|
|
130
|
+
try {
|
|
131
|
+
result = await layoutGrammar({cwd: process.cwd()});
|
|
132
|
+
} catch (e) {
|
|
133
|
+
cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (json) return jsonOut(result.type, result.data);
|
|
137
|
+
humanLog(result.data.text);
|
|
138
|
+
});
|
|
139
|
+
}
|