@astryxdesign/cli 0.1.2 → 0.1.3
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 +64 -1
- package/README.md +49 -55
- package/docs/layout.doc.dense.mjs +90 -0
- package/docs/layout.doc.mjs +160 -0
- package/docs/principles.doc.dense.mjs +2 -2
- package/docs/principles.doc.mjs +4 -0
- package/docs/principles.doc.zh.mjs +2 -2
- package/docs/theme.doc.mjs +2 -2
- package/package.json +13 -1
- package/src/api/blog.mjs +192 -0
- package/src/api/blog.test.mjs +144 -0
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +14 -7
- package/src/api/doctor.mjs +10 -25
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +11 -10
- package/src/api/layout.test.mjs +4 -1
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +242 -84
- 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 +1 -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 +1 -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__/migrate-xds-css-surfaces.test.mjs +67 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -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 +43 -6
- package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -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/agent-docs.mjs +4 -1
- package/src/commands/blog.mjs +60 -0
- package/src/commands/build-theme.import-path.test.mjs +2 -12
- package/src/commands/build-theme.mjs +117 -0
- package/src/commands/build-theme.prose.test.mjs +5 -15
- package/src/commands/build-theme.watch.test.mjs +149 -0
- 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/ensure-core-built.mjs +120 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/swizzle.mjs +224 -235
- 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 -170
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +7 -20
- package/src/config.test.mjs +89 -13
- package/src/index.mjs +8 -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 +41 -55
- package/src/lib/error-codes.mjs +15 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +92 -115
- package/src/lib/integrations.test.mjs +88 -107
- package/src/lib/manifest.mjs +5 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/site.mjs +20 -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 +56 -85
- 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 -237
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
- 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/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
- package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
- package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
- package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
- package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
- package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
- package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
- package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
- package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
- package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
- package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
- package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
- package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
- package/templates/pages/kanban-board/page.tsx +729 -0
- package/templates/pages/kanban-board/template.doc.mjs +12 -0
- 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 +241 -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/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 -464
- package/src/commands/gap-report.test.mjs +0 -168
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -113
- package/src/lib/config.test.mjs +0 -91
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Integration-routing tests for `astryx swizzle`.
|
|
5
|
+
*
|
|
6
|
+
* `rewriteImports` is unit-tested in swizzle.test.mjs. These tests exercise the
|
|
7
|
+
* end-to-end command behavior by spawning the CLI bin against hermetic
|
|
8
|
+
* fixtures: a fake @astryxdesign/core under node_modules plus, for the
|
|
9
|
+
* integration cases, a configured integration package (astryx.config.mjs +
|
|
10
|
+
* astryx.integration.mjs + a `components` dir) — all under node_modules so the
|
|
11
|
+
* config/manifest loaders resolve normally.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
15
|
+
import {execFileSync} from 'node:child_process';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import * as os from 'node:os';
|
|
19
|
+
import {fileURLToPath} from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a fake @astryxdesign/core under <project>/node_modules with a single
|
|
26
|
+
* swizzleable Button component (bare `Button.tsx`, no doc).
|
|
27
|
+
*/
|
|
28
|
+
function buildFakeCore(project) {
|
|
29
|
+
const core = path.join(project, 'node_modules', '@astryxdesign', 'core');
|
|
30
|
+
const buttonDir = path.join(core, 'src', 'Button');
|
|
31
|
+
fs.mkdirSync(buttonDir, {recursive: true});
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(core, 'package.json'),
|
|
34
|
+
'{"name":"@astryxdesign/core","version":"0.0.13"}',
|
|
35
|
+
);
|
|
36
|
+
fs.writeFileSync(
|
|
37
|
+
path.join(buttonDir, 'Button.tsx'),
|
|
38
|
+
[
|
|
39
|
+
`import {tokens} from '../theme/tokens.stylex';`,
|
|
40
|
+
`import {helper} from './helper';`,
|
|
41
|
+
`export const Button = () => null;`,
|
|
42
|
+
'',
|
|
43
|
+
].join('\n'),
|
|
44
|
+
);
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(buttonDir, 'helper.ts'),
|
|
47
|
+
`export const helper = 1;\n`,
|
|
48
|
+
);
|
|
49
|
+
return core;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a configured integration package `@test/meta` under
|
|
54
|
+
* <project>/node_modules with a same-stem component (source + doc) and an
|
|
55
|
+
* escaping import, plus a colocated test file. Writes astryx.config.mjs at the
|
|
56
|
+
* project root listing the integration.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} project
|
|
59
|
+
* @param {{issuesUrl?: string|null, componentName?: string}} [opts]
|
|
60
|
+
*/
|
|
61
|
+
function buildIntegration(project, {issuesUrl, componentName = 'MetaAppShell'} = {}) {
|
|
62
|
+
const intDir = path.join(project, 'node_modules', '@test', 'meta');
|
|
63
|
+
const compRoot = path.join(intDir, 'components');
|
|
64
|
+
const compDir = path.join(compRoot, componentName);
|
|
65
|
+
fs.mkdirSync(compDir, {recursive: true});
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(intDir, 'package.json'),
|
|
68
|
+
JSON.stringify({name: '@test/meta', version: '1.2.3'}),
|
|
69
|
+
);
|
|
70
|
+
const manifest = {components: './components'};
|
|
71
|
+
if (issuesUrl) manifest.issuesUrl = issuesUrl;
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(intDir, 'astryx.integration.mjs'),
|
|
74
|
+
`export default ${JSON.stringify(manifest)};\n`,
|
|
75
|
+
);
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(compDir, `${componentName}.tsx`),
|
|
78
|
+
[
|
|
79
|
+
`import x from '../utils/foo';`,
|
|
80
|
+
`import {sib} from './sibling';`,
|
|
81
|
+
`export function ${componentName}() { return x; }`,
|
|
82
|
+
'',
|
|
83
|
+
].join('\n'),
|
|
84
|
+
);
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(compDir, 'sibling.ts'),
|
|
87
|
+
`export const sib = 1;\n`,
|
|
88
|
+
);
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(compDir, `${componentName}.doc.mjs`),
|
|
91
|
+
`export const docs = {name: '${componentName}', usage: {description: 'x'}};\n`,
|
|
92
|
+
);
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
path.join(compDir, `${componentName}.test.tsx`),
|
|
95
|
+
`it('noop', () => {});\n`,
|
|
96
|
+
);
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
path.join(project, 'astryx.config.mjs'),
|
|
99
|
+
`export default {integrations: ['@test/meta']};\n`,
|
|
100
|
+
);
|
|
101
|
+
return {intDir, compDir};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeProjectPackageJson(project, extra = {}) {
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(project, 'package.json'),
|
|
107
|
+
JSON.stringify({name: 'consumer', version: '1.0.0', ...extra}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runCli(args, cwd) {
|
|
112
|
+
try {
|
|
113
|
+
const out = execFileSync('node', [CLI_BIN, ...args], {
|
|
114
|
+
cwd,
|
|
115
|
+
encoding: 'utf-8',
|
|
116
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
117
|
+
env: {...process.env, FORCE_COLOR: '0'},
|
|
118
|
+
});
|
|
119
|
+
return {code: 0, stdout: out, stderr: ''};
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return {
|
|
122
|
+
code: e.status ?? 1,
|
|
123
|
+
stdout: e.stdout?.toString() ?? '',
|
|
124
|
+
stderr: e.stderr?.toString() ?? '',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let tmpDir;
|
|
130
|
+
let project;
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-swizzle-routing-'));
|
|
133
|
+
project = path.join(tmpDir, 'project');
|
|
134
|
+
fs.mkdirSync(project, {recursive: true});
|
|
135
|
+
});
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('swizzle — core feedback routing via config', () => {
|
|
141
|
+
it('routes core feedback to config.issuesUrl when set', () => {
|
|
142
|
+
buildFakeCore(project);
|
|
143
|
+
writeProjectPackageJson(project);
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
path.join(project, 'astryx.config.mjs'),
|
|
146
|
+
`export default {issuesUrl: 'https://github.com/acme/ds/issues'};\n`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const result = runCli(['--json', 'swizzle', 'Button', '-f'], project);
|
|
150
|
+
expect(result.code).toBe(0);
|
|
151
|
+
const env = JSON.parse(result.stdout);
|
|
152
|
+
expect(env.type).toBe('swizzle.copy');
|
|
153
|
+
expect(env.data.package).toBe('@astryxdesign/core');
|
|
154
|
+
expect(env.data.feedback.issuesUrl).toBe('https://github.com/acme/ds/issues');
|
|
155
|
+
// Escaping import rewritten to core; sibling import preserved.
|
|
156
|
+
const out = fs.readFileSync(
|
|
157
|
+
path.join(project, 'components', 'astryx', 'Button', 'Button.tsx'),
|
|
158
|
+
'utf-8',
|
|
159
|
+
);
|
|
160
|
+
expect(out).toContain(`from '@astryxdesign/core/theme'`);
|
|
161
|
+
expect(out).toContain(`from './helper'`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('falls back to the default issues URL when config has none', () => {
|
|
165
|
+
buildFakeCore(project);
|
|
166
|
+
writeProjectPackageJson(project);
|
|
167
|
+
|
|
168
|
+
const result = runCli(['--json', 'swizzle', 'Button', '-f'], project);
|
|
169
|
+
expect(result.code).toBe(0);
|
|
170
|
+
const env = JSON.parse(result.stdout);
|
|
171
|
+
expect(env.data.feedback.issuesUrl).toBe(
|
|
172
|
+
'https://github.com/facebook/astryx/issues/new',
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('swizzle — integration-owned components', () => {
|
|
178
|
+
it('copies the component dir (excluding test/doc), rewrites escaping imports to the owner package, routes feedback to the integration issuesUrl', () => {
|
|
179
|
+
buildFakeCore(project);
|
|
180
|
+
writeProjectPackageJson(project);
|
|
181
|
+
buildIntegration(project, {issuesUrl: 'https://example.com/meta/issues'});
|
|
182
|
+
|
|
183
|
+
const result = runCli(['--json', 'swizzle', 'MetaAppShell', '-f'], project);
|
|
184
|
+
expect(result.code).toBe(0);
|
|
185
|
+
const env = JSON.parse(result.stdout);
|
|
186
|
+
expect(env.type).toBe('swizzle.copy');
|
|
187
|
+
expect(env.data.package).toBe('@test/meta');
|
|
188
|
+
// Doc + test excluded from the copy.
|
|
189
|
+
expect(env.data.files).toContain('MetaAppShell.tsx');
|
|
190
|
+
expect(env.data.files).toContain('sibling.ts');
|
|
191
|
+
expect(env.data.files).not.toContain('MetaAppShell.doc.mjs');
|
|
192
|
+
expect(env.data.files.some(f => f.includes('.test.'))).toBe(false);
|
|
193
|
+
// Feedback routed to the integration's issues URL.
|
|
194
|
+
expect(env.data.feedback.issuesUrl).toBe('https://example.com/meta/issues');
|
|
195
|
+
|
|
196
|
+
const outDir = path.join(project, 'components', 'astryx', 'MetaAppShell');
|
|
197
|
+
expect(fs.existsSync(path.join(outDir, 'MetaAppShell.doc.mjs'))).toBe(false);
|
|
198
|
+
const out = fs.readFileSync(path.join(outDir, 'MetaAppShell.tsx'), 'utf-8');
|
|
199
|
+
expect(out).toContain(`from '@test/meta/utils'`);
|
|
200
|
+
expect(out).toContain(`from './sibling'`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('omits the feedback note when the integration ships no issuesUrl', () => {
|
|
204
|
+
buildFakeCore(project);
|
|
205
|
+
writeProjectPackageJson(project);
|
|
206
|
+
buildIntegration(project, {issuesUrl: null});
|
|
207
|
+
|
|
208
|
+
const result = runCli(['--json', 'swizzle', 'MetaAppShell', '-f'], project);
|
|
209
|
+
expect(result.code).toBe(0);
|
|
210
|
+
const env = JSON.parse(result.stdout);
|
|
211
|
+
expect(env.type).toBe('swizzle.copy');
|
|
212
|
+
expect(env.data.package).toBe('@test/meta');
|
|
213
|
+
expect(env.data.feedback).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('swizzle — ambiguous ownership', () => {
|
|
218
|
+
it('errors when a name is owned by core + an integration and no --package is given', () => {
|
|
219
|
+
buildFakeCore(project);
|
|
220
|
+
writeProjectPackageJson(project);
|
|
221
|
+
// Integration also provides "Button" (collides with core).
|
|
222
|
+
buildIntegration(project, {
|
|
223
|
+
issuesUrl: 'https://example.com/meta/issues',
|
|
224
|
+
componentName: 'Button',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const result = runCli(['--json', 'swizzle', 'Button', '-f'], project);
|
|
228
|
+
expect(result.code).not.toBe(0);
|
|
229
|
+
const env = JSON.parse(result.stdout);
|
|
230
|
+
expect(env.code).toBe('ERR_AMBIGUOUS_COMPONENT');
|
|
231
|
+
const pkgs = (env.suggestions ?? []).map(s => s.name);
|
|
232
|
+
expect(pkgs).toContain('@astryxdesign/core');
|
|
233
|
+
expect(pkgs).toContain('@test/meta');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('--package resolves an ambiguous name to the integration', () => {
|
|
237
|
+
buildFakeCore(project);
|
|
238
|
+
writeProjectPackageJson(project);
|
|
239
|
+
buildIntegration(project, {
|
|
240
|
+
issuesUrl: 'https://example.com/meta/issues',
|
|
241
|
+
componentName: 'Button',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const result = runCli(
|
|
245
|
+
['--json', 'swizzle', 'Button', '--package', '@test/meta', '-f'],
|
|
246
|
+
project,
|
|
247
|
+
);
|
|
248
|
+
expect(result.code).toBe(0);
|
|
249
|
+
const env = JSON.parse(result.stdout);
|
|
250
|
+
expect(env.data.package).toBe('@test/meta');
|
|
251
|
+
const out = fs.readFileSync(
|
|
252
|
+
path.join(project, 'components', 'astryx', 'Button', 'Button.tsx'),
|
|
253
|
+
'utf-8',
|
|
254
|
+
);
|
|
255
|
+
expect(out).toContain(`from '@test/meta/utils'`);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('--package resolves an ambiguous name to core', () => {
|
|
259
|
+
buildFakeCore(project);
|
|
260
|
+
writeProjectPackageJson(project);
|
|
261
|
+
buildIntegration(project, {
|
|
262
|
+
issuesUrl: 'https://example.com/meta/issues',
|
|
263
|
+
componentName: 'Button',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const result = runCli(
|
|
267
|
+
['--json', 'swizzle', 'Button', '--package', '@astryxdesign/core', '-f'],
|
|
268
|
+
project,
|
|
269
|
+
);
|
|
270
|
+
expect(result.code).toBe(0);
|
|
271
|
+
const env = JSON.parse(result.stdout);
|
|
272
|
+
expect(env.data.package).toBe('@astryxdesign/core');
|
|
273
|
+
const out = fs.readFileSync(
|
|
274
|
+
path.join(project, 'components', 'astryx', 'Button', 'Button.tsx'),
|
|
275
|
+
'utf-8',
|
|
276
|
+
);
|
|
277
|
+
expect(out).toContain(`from '@astryxdesign/core/theme'`);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -11,9 +11,11 @@ import {isNonInteractive} from '../utils/path-safety.mjs';
|
|
|
11
11
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
12
12
|
import {cliError} from '../lib/cli-error.mjs';
|
|
13
13
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
14
|
-
import {template as templateApi
|
|
14
|
+
import {template as templateApi} from '../api/template.mjs';
|
|
15
|
+
import {Project} from '../lib/project.mjs';
|
|
16
|
+
import {warnOnIntegrationIssues} from '../lib/integration-warnings.mjs';
|
|
15
17
|
|
|
16
|
-
export {discoverTemplates, listTemplates
|
|
18
|
+
export {discoverTemplates, listTemplates} from '../api/template.mjs';
|
|
17
19
|
|
|
18
20
|
function isCancel(value) {
|
|
19
21
|
if (p.isCancel(value)) {
|
|
@@ -24,16 +26,27 @@ function isCancel(value) {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export function registerTemplate(program) {
|
|
27
|
-
|
|
29
|
+
program
|
|
28
30
|
.command('template [name] [path]')
|
|
29
31
|
.description('Inject a page or block template')
|
|
30
32
|
.option('--list', 'List available templates')
|
|
31
33
|
.option('--type <type>', 'Filter by template type: page or block')
|
|
34
|
+
.option('--package <pkg>', 'Narrow to templates from a specific package')
|
|
32
35
|
.option('--skeleton', 'Show layout skeleton with spatial annotations (padding, gap, nesting)')
|
|
33
36
|
.option('-f, --overwrite', 'Overwrite existing files without prompting')
|
|
34
37
|
.action(async (name, targetPath, options) => {
|
|
35
38
|
const json = program.opts().json || false;
|
|
36
39
|
|
|
40
|
+
// Non-blocking nudge: if any configured integration has validation
|
|
41
|
+
// issues, print one compact line to stderr pointing at
|
|
42
|
+
// validate-integration. Best-effort; suppressed in --json mode.
|
|
43
|
+
try {
|
|
44
|
+
const project = await Project.load(process.cwd());
|
|
45
|
+
await warnOnIntegrationIssues(project.loadedIntegrations, {json});
|
|
46
|
+
} catch {
|
|
47
|
+
// Never let the nudge break the command.
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
// Pre-flight overwrite check (only when we'd actually copy a file).
|
|
38
51
|
// The API resolves the destination; we need to mirror its logic
|
|
39
52
|
// narrowly enough to detect collisions before invoking it.
|
|
@@ -72,6 +85,7 @@ export function registerTemplate(program) {
|
|
|
72
85
|
list: options.list,
|
|
73
86
|
skeleton: options.skeleton,
|
|
74
87
|
type: options.type,
|
|
88
|
+
package: options.package,
|
|
75
89
|
targetPath,
|
|
76
90
|
cwd: process.cwd(),
|
|
77
91
|
});
|
|
@@ -88,26 +102,28 @@ export function registerTemplate(program) {
|
|
|
88
102
|
case 'template.list': {
|
|
89
103
|
const pages = result.data.filter(t => t.type === 'page');
|
|
90
104
|
const blocks = result.data.filter(t => t.type === 'block');
|
|
105
|
+
const renderEntry = t => {
|
|
106
|
+
const status = t.isReady ? '' : ' (WIP)';
|
|
107
|
+
const pkg =
|
|
108
|
+
t.package && t.package !== '@astryxdesign/core'
|
|
109
|
+
? ` [${t.package}]`
|
|
110
|
+
: '';
|
|
111
|
+
humanLog(` ${t.name}${status}${pkg}`);
|
|
112
|
+
if (t.description) humanLog(` ${t.description}`);
|
|
113
|
+
};
|
|
91
114
|
if (pages.length > 0) {
|
|
92
115
|
humanLog('\nPage Templates:\n');
|
|
93
|
-
for (const t of pages)
|
|
94
|
-
const status = t.isReady ? '' : ' (WIP)';
|
|
95
|
-
humanLog(` ${t.name}${status}`);
|
|
96
|
-
if (t.description) humanLog(` ${t.description}`);
|
|
97
|
-
}
|
|
116
|
+
for (const t of pages) renderEntry(t);
|
|
98
117
|
}
|
|
99
118
|
if (blocks.length > 0) {
|
|
100
119
|
humanLog('\nBlock Templates:\n');
|
|
101
|
-
for (const t of blocks)
|
|
102
|
-
const status = t.isReady ? '' : ' (WIP)';
|
|
103
|
-
humanLog(` ${t.name}${status}`);
|
|
104
|
-
if (t.description) humanLog(` ${t.description}`);
|
|
105
|
-
}
|
|
120
|
+
for (const t of blocks) renderEntry(t);
|
|
106
121
|
}
|
|
107
122
|
humanLog('\nUsage:');
|
|
108
|
-
humanLog(' astryx template <
|
|
109
|
-
humanLog(' astryx template <
|
|
110
|
-
humanLog(' astryx template --list --type block List only blocks
|
|
123
|
+
humanLog(' astryx template <id> [target-path] Scaffold page or block');
|
|
124
|
+
humanLog(' astryx template <id> --skeleton Layout reference');
|
|
125
|
+
humanLog(' astryx template --list --type block List only blocks');
|
|
126
|
+
humanLog(' astryx template --list --package <pkg> List from one package\n');
|
|
111
127
|
break;
|
|
112
128
|
}
|
|
113
129
|
|
|
@@ -131,26 +147,6 @@ export function registerTemplate(program) {
|
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
});
|
|
134
|
-
|
|
135
|
-
templateCmd
|
|
136
|
-
.command('get')
|
|
137
|
-
.description('Fetch a template by ID via the astryx.config.mjs hook')
|
|
138
|
-
.requiredOption('--id <id>', 'Template identifier to fetch')
|
|
139
|
-
.action(async (options) => {
|
|
140
|
-
const json = program.opts().json || false;
|
|
141
|
-
|
|
142
|
-
let result;
|
|
143
|
-
try {
|
|
144
|
-
result = await getTemplateById(options.id, {cwd: process.cwd()});
|
|
145
|
-
} catch (e) {
|
|
146
|
-
cliError(e.message, {suggestions: e.suggestions, code: e.code});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (json) return jsonOut(result.type, result.data);
|
|
151
|
-
|
|
152
|
-
humanLog(result.data.source);
|
|
153
|
-
});
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
/**
|
|
@@ -168,7 +164,7 @@ async function detectTemplateCollision(name, targetPath) {
|
|
|
168
164
|
const {discoverTemplates} = await import('../api/template.mjs');
|
|
169
165
|
let templates;
|
|
170
166
|
try {
|
|
171
|
-
templates = await discoverTemplates();
|
|
167
|
+
templates = await discoverTemplates(process.cwd());
|
|
172
168
|
} catch {
|
|
173
169
|
return null;
|
|
174
170
|
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Upgrade config-codemod-ordering tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies that CORE codemods run BEFORE the consumer's config is loaded, so a
|
|
7
|
+
* config codemod (v0.1.3 migrate-layout-components, meta.codemodType==='config')
|
|
8
|
+
* can repair an otherwise-invalid config that strict `Project.load` would reject.
|
|
9
|
+
*
|
|
10
|
+
* Like the integration-policy tests, these scaffold a real consumer project
|
|
11
|
+
* (package.json + astryx.config.mjs) under a REPO-LOCAL temp dir (so Vite
|
|
12
|
+
* permits dynamic import of the config module — it blocks /tmp) and chdir in.
|
|
13
|
+
*
|
|
14
|
+
* Cases:
|
|
15
|
+
* 1. dry-run, legacy layout.components, v0.1.3 config codemod in range →
|
|
16
|
+
* does NOT abort; reports "fixable, re-run with --apply" + the suggested
|
|
17
|
+
* `--codemod ... --apply` command; exit 0; integrations skipped.
|
|
18
|
+
* 2. --apply, same config → core config codemod writes the repaired config,
|
|
19
|
+
* then config loads strictly OK and the upgrade completes; on-disk config
|
|
20
|
+
* is migrated to experimental.xle.components.
|
|
21
|
+
* 3. dry-run, config genuinely invalid (integrations: 5) with NO pending
|
|
22
|
+
* config codemod → still ABORTS (exit nonzero).
|
|
23
|
+
* 4. happy path (valid config, no config codemod) → unchanged behavior.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
import {Command} from 'commander';
|
|
30
|
+
import {registerUpgrade} from './upgrade.mjs';
|
|
31
|
+
|
|
32
|
+
let tmpDir;
|
|
33
|
+
let originalCwd;
|
|
34
|
+
let logCalls;
|
|
35
|
+
let stdoutCalls;
|
|
36
|
+
let exitCode;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
originalCwd = process.cwd();
|
|
40
|
+
// Repo-local temp dir: under cwd so Vite allows the astryx.config import.
|
|
41
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-upgrade-order-'));
|
|
42
|
+
process.chdir(tmpDir);
|
|
43
|
+
logCalls = [];
|
|
44
|
+
stdoutCalls = [];
|
|
45
|
+
exitCode = undefined;
|
|
46
|
+
vi.spyOn(console, 'log').mockImplementation((...a) => logCalls.push(a.join(' ')));
|
|
47
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
48
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(chunk => {
|
|
49
|
+
stdoutCalls.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
vi.spyOn(process, 'exit').mockImplementation(code => {
|
|
53
|
+
exitCode = code;
|
|
54
|
+
throw new Error(`__exit ${code}`);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
process.chdir(originalCwd);
|
|
60
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function writePkg() {
|
|
65
|
+
fs.writeFileSync(
|
|
66
|
+
path.join(tmpDir, 'package.json'),
|
|
67
|
+
JSON.stringify({name: 'consumer'}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeInstalledCore(version) {
|
|
72
|
+
const dir = path.join(tmpDir, 'node_modules', '@astryxdesign', 'core');
|
|
73
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
path.join(dir, 'package.json'),
|
|
76
|
+
JSON.stringify({name: '@astryxdesign/core', version}),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeConfig(body) {
|
|
81
|
+
fs.writeFileSync(path.join(tmpDir, 'astryx.config.mjs'), body);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeSource() {
|
|
85
|
+
fs.mkdirSync(path.join(tmpDir, 'src'), {recursive: true});
|
|
86
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'index.ts'), 'const foo = 1;\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const LEGACY_CONFIG = `export default {
|
|
90
|
+
layout: {
|
|
91
|
+
components: {
|
|
92
|
+
KpiCard: '@/components/KpiCard',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
function createProgram() {
|
|
99
|
+
const program = new Command();
|
|
100
|
+
program.exitOverride();
|
|
101
|
+
program.option('--json', 'Output as typed JSON');
|
|
102
|
+
registerUpgrade(program);
|
|
103
|
+
return program;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runJson(args) {
|
|
107
|
+
const program = createProgram();
|
|
108
|
+
try {
|
|
109
|
+
await program.parseAsync(['node', 'astryx', ...args]);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (!String(err?.message || '').startsWith('__exit')) throw err;
|
|
112
|
+
}
|
|
113
|
+
for (let i = logCalls.length - 1; i >= 0; i--) {
|
|
114
|
+
const line = logCalls[i];
|
|
115
|
+
if (line.startsWith('{')) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(line);
|
|
118
|
+
} catch {
|
|
119
|
+
// keep looking
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runHuman(args) {
|
|
127
|
+
const program = createProgram();
|
|
128
|
+
try {
|
|
129
|
+
await program.parseAsync(['node', 'astryx', ...args]);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (!String(err?.message || '').startsWith('__exit')) throw err;
|
|
132
|
+
}
|
|
133
|
+
return stdoutCalls.join('') + '\n' + logCalls.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
describe('upgrade — core codemods run before config load', () => {
|
|
137
|
+
it('dry-run: legacy layout.components is reported as fixable, not an abort', async () => {
|
|
138
|
+
// 0.1.2 -> 0.1.3 brings the migrate-layout-components config codemod.
|
|
139
|
+
writePkg();
|
|
140
|
+
writeInstalledCore('0.1.3');
|
|
141
|
+
writeConfig(LEGACY_CONFIG);
|
|
142
|
+
writeSource();
|
|
143
|
+
|
|
144
|
+
const result = await runJson([
|
|
145
|
+
'--json',
|
|
146
|
+
'upgrade',
|
|
147
|
+
'--from',
|
|
148
|
+
'0.1.2',
|
|
149
|
+
'--path',
|
|
150
|
+
'src',
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
expect(result).not.toBeNull();
|
|
154
|
+
// Did NOT abort with a config-validation error.
|
|
155
|
+
expect(result.error).toBeUndefined();
|
|
156
|
+
expect(exitCode).not.toBe(1);
|
|
157
|
+
// Reported as the fixable case with the suggested apply command.
|
|
158
|
+
expect(result.type).toBe('upgrade.status');
|
|
159
|
+
expect(result.data.status).toBe('config_fixable');
|
|
160
|
+
expect(result.data.configCodemods).toContain(
|
|
161
|
+
'migrate-layout-components-to-experimental',
|
|
162
|
+
);
|
|
163
|
+
expect(result.data.suggestedCommand).toBe(
|
|
164
|
+
'astryx upgrade --from 0.1.2 --codemod migrate-layout-components-to-experimental --apply',
|
|
165
|
+
);
|
|
166
|
+
expect(result.data.message).toMatch(/--apply/);
|
|
167
|
+
// Integrations skipped for the preview.
|
|
168
|
+
expect(result.data.note).toMatch(/--apply run/i);
|
|
169
|
+
// Dry-run did not write the config.
|
|
170
|
+
const onDisk = fs.readFileSync(
|
|
171
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
172
|
+
'utf-8',
|
|
173
|
+
);
|
|
174
|
+
expect(onDisk).toContain('layout');
|
|
175
|
+
expect(onDisk).not.toContain('experimental');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('human dry-run prints the fixable guidance + suggested command', async () => {
|
|
179
|
+
writePkg();
|
|
180
|
+
writeInstalledCore('0.1.3');
|
|
181
|
+
writeConfig(LEGACY_CONFIG);
|
|
182
|
+
writeSource();
|
|
183
|
+
|
|
184
|
+
const out = await runHuman(['upgrade', '--from', '0.1.2', '--path', 'src']);
|
|
185
|
+
expect(out).toMatch(/fails strict validation/i);
|
|
186
|
+
expect(out).toContain(
|
|
187
|
+
'astryx upgrade --from 0.1.2 --codemod migrate-layout-components-to-experimental --apply',
|
|
188
|
+
);
|
|
189
|
+
expect(exitCode).not.toBe(1);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('--apply: core config codemod repairs the config, then the upgrade completes', async () => {
|
|
193
|
+
writePkg();
|
|
194
|
+
writeInstalledCore('0.1.3');
|
|
195
|
+
writeConfig(LEGACY_CONFIG);
|
|
196
|
+
writeSource();
|
|
197
|
+
|
|
198
|
+
const result = await runJson([
|
|
199
|
+
'--json',
|
|
200
|
+
'upgrade',
|
|
201
|
+
'--from',
|
|
202
|
+
'0.1.2',
|
|
203
|
+
'--path',
|
|
204
|
+
'src',
|
|
205
|
+
'--apply',
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
expect(result).not.toBeNull();
|
|
209
|
+
expect(result.error).toBeUndefined();
|
|
210
|
+
expect(exitCode).not.toBe(1);
|
|
211
|
+
expect(result.type).toBe('upgrade.run');
|
|
212
|
+
expect(result.data.filesChanged).toBeGreaterThan(0);
|
|
213
|
+
|
|
214
|
+
// The on-disk config was migrated to experimental.xle.components.
|
|
215
|
+
const onDisk = fs.readFileSync(
|
|
216
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
217
|
+
'utf-8',
|
|
218
|
+
);
|
|
219
|
+
expect(onDisk).toContain('experimental');
|
|
220
|
+
expect(onDisk).toContain('xle');
|
|
221
|
+
expect(onDisk).toMatch(
|
|
222
|
+
/KpiCard:\s*\{\s*from:\s*['"]@\/components\/KpiCard['"]/,
|
|
223
|
+
);
|
|
224
|
+
expect(onDisk).not.toMatch(/layout\s*:/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('dry-run: a genuinely invalid config with NO fixing codemod still aborts', async () => {
|
|
228
|
+
writePkg();
|
|
229
|
+
writeInstalledCore('0.1.3');
|
|
230
|
+
// `integrations: 5` is invalid AND no config codemod in 0.1.2->0.1.3 fixes it.
|
|
231
|
+
writeConfig(`export default { integrations: 5 };\n`);
|
|
232
|
+
writeSource();
|
|
233
|
+
|
|
234
|
+
const result = await runJson([
|
|
235
|
+
'--json',
|
|
236
|
+
'upgrade',
|
|
237
|
+
'--from',
|
|
238
|
+
'0.1.2',
|
|
239
|
+
'--path',
|
|
240
|
+
'src',
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
expect(result).not.toBeNull();
|
|
244
|
+
// Aborts with a config-validation error (not the fixable status).
|
|
245
|
+
expect(result.error).toBeTruthy();
|
|
246
|
+
expect(result.type).not.toBe('upgrade.status');
|
|
247
|
+
expect(exitCode).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('happy path: valid config with no config codemod behaves normally', async () => {
|
|
251
|
+
writePkg();
|
|
252
|
+
writeInstalledCore('0.1.3');
|
|
253
|
+
// A valid, already-migrated config; no relocation needed.
|
|
254
|
+
writeConfig(`export default { integrations: [] };\n`);
|
|
255
|
+
writeSource();
|
|
256
|
+
|
|
257
|
+
const result = await runJson([
|
|
258
|
+
'--json',
|
|
259
|
+
'upgrade',
|
|
260
|
+
'--from',
|
|
261
|
+
'0.1.2',
|
|
262
|
+
'--path',
|
|
263
|
+
'src',
|
|
264
|
+
'--apply',
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
expect(result).not.toBeNull();
|
|
268
|
+
expect(result.error).toBeUndefined();
|
|
269
|
+
expect(exitCode).not.toBe(1);
|
|
270
|
+
// Completed as a run (the config codemod is a no-op on an already-migrated config).
|
|
271
|
+
expect(result.type).toBe('upgrade.run');
|
|
272
|
+
});
|
|
273
|
+
});
|