@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,91 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Pluggable config/discovery cache for the Project API.
|
|
5
|
+
*
|
|
6
|
+
* The Project class memoizes resolved discovery (components, templates,
|
|
7
|
+
* codemods, config) behind a small key/value cache so the same work is never
|
|
8
|
+
* repeated within a single Project instance. The cache interface is
|
|
9
|
+
* intentionally tiny — `get(key)` / `set(key, value)` — so a future
|
|
10
|
+
* file-backed, cross-process cache can drop in with NO API change.
|
|
11
|
+
*
|
|
12
|
+
* Cache keys are derived from a CONTENT HASH of the project's config file
|
|
13
|
+
* (see {@link configContentHash}) combined with the cwd and the
|
|
14
|
+
* discovery-kind (+ args). The in-memory implementation never busts on a
|
|
15
|
+
* content change, but the hash is computed regardless so a hash-busting,
|
|
16
|
+
* file-backed cache works against the exact same key path.
|
|
17
|
+
*
|
|
18
|
+
* @typedef {Object} ConfigCache
|
|
19
|
+
* @property {(key: string) => unknown} get
|
|
20
|
+
* @property {(key: string, value: unknown) => void} set
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as crypto from 'node:crypto';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Stable sentinel hash used when there is no config file (or it can't be
|
|
28
|
+
* read). Keeps key derivation total — every cwd still produces a key.
|
|
29
|
+
*/
|
|
30
|
+
export const NO_CONFIG_HASH = 'no-config';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compute a stable content hash for a config file. Reads the file bytes and
|
|
34
|
+
* returns their sha256 hex digest; a missing/unreadable file yields
|
|
35
|
+
* {@link NO_CONFIG_HASH}. The hash is the bust signal a future file-backed
|
|
36
|
+
* cache would key on.
|
|
37
|
+
*
|
|
38
|
+
* @param {string | null | undefined} configPath absolute config path, or null
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function configContentHash(configPath) {
|
|
42
|
+
if (!configPath) return NO_CONFIG_HASH;
|
|
43
|
+
try {
|
|
44
|
+
const bytes = fs.readFileSync(configPath);
|
|
45
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
46
|
+
} catch {
|
|
47
|
+
return NO_CONFIG_HASH;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a cache key from a config content hash, a cwd, and a discovery kind
|
|
53
|
+
* (already including any args, e.g. `codemods:0.1.0..0.2.0`). The shape is
|
|
54
|
+
* stable so the same inputs always map to the same key.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} hash config content hash
|
|
57
|
+
* @param {string} cwd
|
|
58
|
+
* @param {string} kind discovery kind (+args)
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function cacheKey(hash, cwd, kind) {
|
|
62
|
+
return `${hash}\u0000${cwd}\u0000${kind}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Default in-memory cache. Per-instance by default (the Project creates one in
|
|
67
|
+
* {@link Project.load} when none is supplied), so it behaves like simple
|
|
68
|
+
* memoization. A shared instance can be passed to reuse results across
|
|
69
|
+
* Projects. Never busts — a content change produces a different KEY (via the
|
|
70
|
+
* hash), not an eviction, which is exactly the behavior a future file-backed
|
|
71
|
+
* cache also relies on.
|
|
72
|
+
*
|
|
73
|
+
* @implements {ConfigCache}
|
|
74
|
+
*/
|
|
75
|
+
export class InMemoryConfigCache {
|
|
76
|
+
/** @type {Map<string, unknown>} */
|
|
77
|
+
#m = new Map();
|
|
78
|
+
|
|
79
|
+
/** @param {string} key */
|
|
80
|
+
get(key) {
|
|
81
|
+
return this.#m.get(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} key
|
|
86
|
+
* @param {unknown} value
|
|
87
|
+
*/
|
|
88
|
+
set(key, value) {
|
|
89
|
+
this.#m.set(key, value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
InMemoryConfigCache,
|
|
8
|
+
cacheKey,
|
|
9
|
+
configContentHash,
|
|
10
|
+
NO_CONFIG_HASH,
|
|
11
|
+
} from './config-cache.mjs';
|
|
12
|
+
|
|
13
|
+
let tmpDir;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-cache-test-'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('InMemoryConfigCache', () => {
|
|
24
|
+
it('stores and returns values by key', () => {
|
|
25
|
+
const cache = new InMemoryConfigCache();
|
|
26
|
+
expect(cache.get('a')).toBeUndefined();
|
|
27
|
+
cache.set('a', 42);
|
|
28
|
+
expect(cache.get('a')).toBe(42);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('can store falsy values distinctly from a miss', () => {
|
|
32
|
+
const cache = new InMemoryConfigCache();
|
|
33
|
+
cache.set('zero', {value: 0});
|
|
34
|
+
expect(cache.get('zero')).toEqual({value: 0});
|
|
35
|
+
expect(cache.get('missing')).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('configContentHash', () => {
|
|
40
|
+
it('returns the sentinel when there is no config path', () => {
|
|
41
|
+
expect(configContentHash(null)).toBe(NO_CONFIG_HASH);
|
|
42
|
+
expect(configContentHash(undefined)).toBe(NO_CONFIG_HASH);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns the sentinel for a missing file', () => {
|
|
46
|
+
expect(configContentHash(path.join(tmpDir, 'nope.mjs'))).toBe(
|
|
47
|
+
NO_CONFIG_HASH,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('is stable for the same bytes', () => {
|
|
52
|
+
const file = path.join(tmpDir, 'astryx.config.mjs');
|
|
53
|
+
fs.writeFileSync(file, 'export default { integrations: [] };\n');
|
|
54
|
+
const a = configContentHash(file);
|
|
55
|
+
const b = configContentHash(file);
|
|
56
|
+
expect(a).toBe(b);
|
|
57
|
+
expect(a).not.toBe(NO_CONFIG_HASH);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('differs when the config content changes', () => {
|
|
61
|
+
const file = path.join(tmpDir, 'astryx.config.mjs');
|
|
62
|
+
fs.writeFileSync(file, 'export default { integrations: [] };\n');
|
|
63
|
+
const before = configContentHash(file);
|
|
64
|
+
fs.writeFileSync(file, 'export default { integrations: ["@x/y"] };\n');
|
|
65
|
+
const after = configContentHash(file);
|
|
66
|
+
expect(before).not.toBe(after);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('cacheKey', () => {
|
|
71
|
+
it('is stable for identical inputs and varies by kind', () => {
|
|
72
|
+
const k1 = cacheKey('hash', '/cwd', 'components');
|
|
73
|
+
const k2 = cacheKey('hash', '/cwd', 'components');
|
|
74
|
+
const k3 = cacheKey('hash', '/cwd', 'templates');
|
|
75
|
+
expect(k1).toBe(k2);
|
|
76
|
+
expect(k1).not.toBe(k3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('varies by hash and cwd', () => {
|
|
80
|
+
expect(cacheKey('h1', '/cwd', 'k')).not.toBe(cacheKey('h2', '/cwd', 'k'));
|
|
81
|
+
expect(cacheKey('h', '/a', 'k')).not.toBe(cacheKey('h', '/b', 'k'));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -8,77 +8,63 @@ const Fn = z.custom(value => typeof value === 'function', {
|
|
|
8
8
|
message: 'Expected function',
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const GapReportSchema = z.union([
|
|
13
|
-
z.literal(false),
|
|
14
|
-
z.object({command: z.string()}).passthrough(),
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
export const AstryxConfigSchema = z
|
|
11
|
+
export const PostCodemodHookSchema = z
|
|
18
12
|
.object({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
gapReport: GapReportSchema.optional(),
|
|
22
|
-
template: z
|
|
23
|
-
.object({
|
|
24
|
-
get: Fn.optional(),
|
|
25
|
-
})
|
|
26
|
-
.passthrough()
|
|
27
|
-
.optional(),
|
|
13
|
+
name: z.string().optional(),
|
|
14
|
+
buildCommand: Fn,
|
|
28
15
|
})
|
|
29
|
-
.
|
|
16
|
+
.strict();
|
|
30
17
|
|
|
31
|
-
export const
|
|
18
|
+
export const XleComponentSchema = z
|
|
32
19
|
.object({
|
|
33
|
-
|
|
34
|
-
from: z.string().optional(),
|
|
35
|
-
to: z.string().optional(),
|
|
36
|
-
title: z.string().optional(),
|
|
20
|
+
from: z.string(),
|
|
37
21
|
description: z.string().optional(),
|
|
38
|
-
|
|
39
|
-
optional: z.boolean().optional(),
|
|
40
|
-
fileExtensions: z.array(z.string()).optional(),
|
|
41
|
-
transform: z.union([z.string(), Fn]),
|
|
22
|
+
default: z.boolean().optional(),
|
|
42
23
|
})
|
|
43
|
-
.
|
|
24
|
+
.strict();
|
|
44
25
|
|
|
45
|
-
export const
|
|
26
|
+
export const AstryxConfigSchema = z
|
|
46
27
|
.object({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
integrations: z.array(z.string()).optional(),
|
|
29
|
+
issuesUrl: z.string().url().optional(),
|
|
30
|
+
hooks: z
|
|
31
|
+
.object({
|
|
32
|
+
postCodemod: z.array(PostCodemodHookSchema).optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict()
|
|
35
|
+
.optional(),
|
|
36
|
+
experimental: z
|
|
37
|
+
.object({
|
|
38
|
+
xle: z
|
|
39
|
+
.object({
|
|
40
|
+
components: z.record(z.string(), XleComponentSchema).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict()
|
|
43
|
+
.optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict()
|
|
46
|
+
.optional(),
|
|
50
47
|
})
|
|
51
|
-
.
|
|
52
|
-
.refine(value => value.run || value.command, {
|
|
53
|
-
message: 'postCodemod hook must define run() or command()',
|
|
54
|
-
});
|
|
48
|
+
.strict();
|
|
55
49
|
|
|
56
50
|
export const AstryxIntegrationSchema = z
|
|
57
51
|
.object({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
docs: z.string().optional(),
|
|
63
|
-
category: z.string().optional(),
|
|
64
|
-
blocks: z.string().optional(),
|
|
65
|
-
gapReport: GapReportSchema.optional(),
|
|
66
|
-
template: z
|
|
67
|
-
.object({
|
|
68
|
-
get: z.union([z.string(), Fn]).optional(),
|
|
69
|
-
})
|
|
70
|
-
.passthrough()
|
|
71
|
-
.optional(),
|
|
72
|
-
codemods: z.array(AstryxIntegrationCodemodSchema).optional(),
|
|
73
|
-
postCodemod: z.array(AstryxPostCodemodHookSchema).optional(),
|
|
52
|
+
components: z.string().optional(),
|
|
53
|
+
templates: z.string().optional(),
|
|
54
|
+
codemods: z.string().optional(),
|
|
55
|
+
issuesUrl: z.string().url().optional(),
|
|
74
56
|
})
|
|
75
|
-
.
|
|
57
|
+
.strict();
|
|
76
58
|
|
|
77
59
|
/**
|
|
60
|
+
* Format a zod error into a single readable line, mirroring the issue-joining
|
|
61
|
+
* convention used across the CLI (path: message; path: message). Exported so
|
|
62
|
+
* the shared module loader and other validators stay in lockstep.
|
|
78
63
|
* @param {string} label
|
|
79
64
|
* @param {import('zod').ZodError} error
|
|
65
|
+
* @returns {string}
|
|
80
66
|
*/
|
|
81
|
-
function formatZodError(label, error) {
|
|
67
|
+
export function formatZodError(label, error) {
|
|
82
68
|
const issues = error.issues
|
|
83
69
|
.map(issue => {
|
|
84
70
|
const path = issue.path.length ? issue.path.join('.') : '(root)';
|
|
@@ -96,7 +82,7 @@ export function validateConfig(config) {
|
|
|
96
82
|
const result = AstryxConfigSchema.safeParse(config);
|
|
97
83
|
if (!result.success) {
|
|
98
84
|
throw new Error(
|
|
99
|
-
formatZodError('astryx.config
|
|
85
|
+
formatZodError('astryx.config default export', result.error),
|
|
100
86
|
);
|
|
101
87
|
}
|
|
102
88
|
return result.data;
|
|
@@ -105,7 +91,7 @@ export function validateConfig(config) {
|
|
|
105
91
|
/**
|
|
106
92
|
* @param {unknown} integration
|
|
107
93
|
* @param {string} [label]
|
|
108
|
-
* @returns {import('../types/
|
|
94
|
+
* @returns {import('../types/integration').AstryxIntegration}
|
|
109
95
|
*/
|
|
110
96
|
export function validateIntegration(
|
|
111
97
|
integration,
|
package/src/lib/error-codes.mjs
CHANGED
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
* | 'ERR_UNKNOWN_SECTION'
|
|
53
53
|
* | 'ERR_UNKNOWN_CATEGORY'
|
|
54
54
|
* | 'ERR_UNKNOWN_TEMPLATE'
|
|
55
|
+
* | 'ERR_AMBIGUOUS_TEMPLATE'
|
|
56
|
+
* | 'ERR_AMBIGUOUS_COMPONENT'
|
|
55
57
|
* | 'ERR_UNKNOWN_THEME'
|
|
56
58
|
* | 'ERR_UNKNOWN_PACKAGE'
|
|
57
59
|
* | 'ERR_UNKNOWN_AGENT'
|
|
@@ -69,13 +71,12 @@
|
|
|
69
71
|
* | 'ERR_WRITE_FAILED'
|
|
70
72
|
* | 'ERR_THEME_INVALID'
|
|
71
73
|
* | 'ERR_THEME_LOAD'
|
|
72
|
-
* | 'ERR_TEMPLATE_CONFIG'
|
|
73
|
-
* | 'ERR_TEMPLATE_GET'
|
|
74
74
|
* | 'ERR_VERSION_DETECT'
|
|
75
75
|
* | 'ERR_INVALID_VERSION'
|
|
76
76
|
* | 'ERR_DEP_MISSING'
|
|
77
77
|
* | 'ERR_GH_CLI'
|
|
78
|
-
* | '
|
|
78
|
+
* | 'ERR_UNKNOWN_POST'
|
|
79
|
+
* | 'ERR_FETCH_FAILED'
|
|
79
80
|
* | 'ERR_LAYOUT_PARSE'
|
|
80
81
|
* | 'ERR_LAYOUT_INVALID'
|
|
81
82
|
* )} ErrorCode
|
|
@@ -125,6 +126,10 @@ export const ERROR_CODES = Object.freeze({
|
|
|
125
126
|
ERR_UNKNOWN_CATEGORY: 'ERR_UNKNOWN_CATEGORY',
|
|
126
127
|
/** No template matched the requested name. */
|
|
127
128
|
ERR_UNKNOWN_TEMPLATE: 'ERR_UNKNOWN_TEMPLATE',
|
|
129
|
+
/** A template id matched more than one template (narrow with --type/--package). */
|
|
130
|
+
ERR_AMBIGUOUS_TEMPLATE: 'ERR_AMBIGUOUS_TEMPLATE',
|
|
131
|
+
/** A component name is owned by more than one package (narrow with --package). */
|
|
132
|
+
ERR_AMBIGUOUS_COMPONENT: 'ERR_AMBIGUOUS_COMPONENT',
|
|
128
133
|
/** No theme matched the requested slug (theme add). */
|
|
129
134
|
ERR_UNKNOWN_THEME: 'ERR_UNKNOWN_THEME',
|
|
130
135
|
/** No package matched the requested name (discover). */
|
|
@@ -166,12 +171,6 @@ export const ERROR_CODES = Object.freeze({
|
|
|
166
171
|
/** A theme file could not be loaded / parsed into a defineTheme result. */
|
|
167
172
|
ERR_THEME_LOAD: 'ERR_THEME_LOAD',
|
|
168
173
|
|
|
169
|
-
// ── Template config ──────────────────────────────────────────────
|
|
170
|
-
/** `template.get` is not configured in astryx.config.mjs (fetch-by-id). */
|
|
171
|
-
ERR_TEMPLATE_CONFIG: 'ERR_TEMPLATE_CONFIG',
|
|
172
|
-
/** A configured `template.get` threw or returned an invalid value. */
|
|
173
|
-
ERR_TEMPLATE_GET: 'ERR_TEMPLATE_GET',
|
|
174
|
-
|
|
175
174
|
// ── Upgrade ──────────────────────────────────────────────────────
|
|
176
175
|
/** The current `@astryxdesign/core` version could not be detected. */
|
|
177
176
|
ERR_VERSION_DETECT: 'ERR_VERSION_DETECT',
|
|
@@ -180,11 +179,15 @@ export const ERROR_CODES = Object.freeze({
|
|
|
180
179
|
/** A required external dependency (e.g. jscodeshift) is missing. */
|
|
181
180
|
ERR_DEP_MISSING: 'ERR_DEP_MISSING',
|
|
182
181
|
|
|
183
|
-
// ──
|
|
182
|
+
// ── GitHub CLI ───────────────────────────────────────────────────
|
|
184
183
|
/** GitHub CLI (`gh`) is not installed or not authenticated. */
|
|
185
184
|
ERR_GH_CLI: 'ERR_GH_CLI',
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
|
|
186
|
+
// ── Blog (read via the published RSS feed) ───────────────────────
|
|
187
|
+
/** No blog post matched the requested slug in the feed. */
|
|
188
|
+
ERR_UNKNOWN_POST: 'ERR_UNKNOWN_POST',
|
|
189
|
+
/** A network fetch (RSS feed or post text) failed. */
|
|
190
|
+
ERR_FETCH_FAILED: 'ERR_FETCH_FAILED',
|
|
188
191
|
|
|
189
192
|
// ── Layout expressions (XLE/XLO) ─────────────────────────────────
|
|
190
193
|
/** A layout expression failed to parse (syntax error, with line/col). */
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Compact, non-blocking integration-issue nudge for everyday commands.
|
|
5
|
+
*
|
|
6
|
+
* When a CONFIGURED integration (from the Project's `loadedIntegrations`) has
|
|
7
|
+
* validation issues, the everyday commands (component / template / upgrade)
|
|
8
|
+
* should print ONE compact, non-blocking line per integration telling the user
|
|
9
|
+
* to run `validate-integration` — instead of silently skipping broken
|
|
10
|
+
* contributions or spamming per-contribution diagnostics.
|
|
11
|
+
*
|
|
12
|
+
* Design constraints (all enforced here):
|
|
13
|
+
* - Reuses the validate-integration validators (validateLoadedIntegration);
|
|
14
|
+
* no validation logic is duplicated.
|
|
15
|
+
* - Writes to STDERR only, so it never corrupts a --json stdout envelope.
|
|
16
|
+
* - Suppressed entirely in --json mode.
|
|
17
|
+
* - Best-effort: never throws, never changes the exit code. Broken
|
|
18
|
+
* contributions are still skipped downstream exactly as before; this only
|
|
19
|
+
* ADDS a one-line nudge.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {validateLoadedIntegration} from '../api/validate-integration.mjs';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* For each configured (already-loaded) integration, compute its issues using
|
|
26
|
+
* the shared validate-integration validators and, if any exist, print exactly
|
|
27
|
+
* ONE line per integration to stderr:
|
|
28
|
+
*
|
|
29
|
+
* Warning: <pkg> has N integration issue(s). Run: astryx validate-integration <pkg>
|
|
30
|
+
*
|
|
31
|
+
* @param {Array<object>} loadedIntegrations the Project's loaded integrations
|
|
32
|
+
* @param {{json?: boolean}} [options]
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
*/
|
|
35
|
+
export async function warnOnIntegrationIssues(loadedIntegrations, {json = false} = {}) {
|
|
36
|
+
try {
|
|
37
|
+
// Cheap guard: only do work when integrations are actually configured.
|
|
38
|
+
if (json) return;
|
|
39
|
+
if (!Array.isArray(loadedIntegrations) || loadedIntegrations.length === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const integration of loadedIntegrations) {
|
|
43
|
+
if (!integration || typeof integration !== 'object') continue;
|
|
44
|
+
let issues;
|
|
45
|
+
try {
|
|
46
|
+
issues = await validateLoadedIntegration(integration);
|
|
47
|
+
} catch {
|
|
48
|
+
// Best-effort: a validator throwing must not break the host command.
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(issues) || issues.length === 0) continue;
|
|
52
|
+
const pkg = integration.name ?? integration.__spec ?? '(integration)';
|
|
53
|
+
// Stderr only — keeps stdout (and any --json envelope) clean.
|
|
54
|
+
console.error(
|
|
55
|
+
`Warning: ${pkg} has ${issues.length} integration issue(s). ` +
|
|
56
|
+
`Run: astryx validate-integration ${pkg}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Never throw, never change the exit code.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Hermetic tests for the compact integration-issue nudge.
|
|
5
|
+
*
|
|
6
|
+
* Builds loaded-integration-shaped objects (as produced by
|
|
7
|
+
* lib/integrations.mjs) with absolute contribution roots under the repo root
|
|
8
|
+
* (process.cwd()) so any node_modules-style path stays within Vite's allowed
|
|
9
|
+
* fs roots. We never load configs from /tmp. The helper is exercised directly;
|
|
10
|
+
* stderr is captured to assert exactly one warning line per broken integration
|
|
11
|
+
* and zero output in --json mode / for clean integrations.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import {warnOnIntegrationIssues} from './integration-warnings.mjs';
|
|
18
|
+
|
|
19
|
+
let tmpDir;
|
|
20
|
+
let errSpy;
|
|
21
|
+
let errLines;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-warn-it-'));
|
|
25
|
+
errLines = [];
|
|
26
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
27
|
+
errLines.push(args.join(' '));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
errSpy.mockRestore();
|
|
33
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a loaded-integration-shaped object. Roots are absolute (mirrors
|
|
38
|
+
* loadIntegrations' resolveRoot).
|
|
39
|
+
*/
|
|
40
|
+
function loaded({name = '@acme/widgets', components, templates, codemods} = {}) {
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
components: components == null ? undefined : path.join(tmpDir, components),
|
|
45
|
+
templates: templates == null ? undefined : path.join(tmpDir, templates),
|
|
46
|
+
codemods: codemods == null ? undefined : path.join(tmpDir, codemods),
|
|
47
|
+
__spec: name,
|
|
48
|
+
__packageDir: tmpDir,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('warnOnIntegrationIssues', () => {
|
|
53
|
+
it('emits exactly one stderr line for an integration with issues', async () => {
|
|
54
|
+
// Declared components root that does not exist on disk → missing_root.
|
|
55
|
+
const integration = loaded({components: 'does-not-exist'});
|
|
56
|
+
await warnOnIntegrationIssues([integration], {json: false});
|
|
57
|
+
|
|
58
|
+
expect(errLines).toHaveLength(1);
|
|
59
|
+
expect(errLines[0]).toBe(
|
|
60
|
+
'Warning: @acme/widgets has 1 integration issue(s). ' +
|
|
61
|
+
'Run: astryx validate-integration @acme/widgets',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('emits nothing in --json mode (keeps stdout/JSON clean)', async () => {
|
|
66
|
+
const integration = loaded({components: 'does-not-exist'});
|
|
67
|
+
await warnOnIntegrationIssues([integration], {json: true});
|
|
68
|
+
expect(errLines).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('emits nothing for an integration with no issues', async () => {
|
|
72
|
+
// Existing components root with no broken contributions → no issues.
|
|
73
|
+
const componentsRoot = path.join(tmpDir, 'components');
|
|
74
|
+
fs.mkdirSync(componentsRoot, {recursive: true});
|
|
75
|
+
const integration = loaded({components: 'components'});
|
|
76
|
+
await warnOnIntegrationIssues([integration], {json: false});
|
|
77
|
+
expect(errLines).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('emits nothing when there are no configured integrations', async () => {
|
|
81
|
+
await warnOnIntegrationIssues([], {json: false});
|
|
82
|
+
await warnOnIntegrationIssues(undefined, {json: false});
|
|
83
|
+
expect(errLines).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('emits one line per broken integration', async () => {
|
|
87
|
+
const a = loaded({name: '@acme/a', components: 'missing-a'});
|
|
88
|
+
const b = loaded({name: '@acme/b', templates: 'missing-b'});
|
|
89
|
+
await warnOnIntegrationIssues([a, b], {json: false});
|
|
90
|
+
expect(errLines).toHaveLength(2);
|
|
91
|
+
expect(errLines[0]).toContain('@acme/a');
|
|
92
|
+
expect(errLines[1]).toContain('@acme/b');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('never throws and never emits when a validator misbehaves', async () => {
|
|
96
|
+
// A non-object entry must be skipped silently.
|
|
97
|
+
await expect(
|
|
98
|
+
warnOnIntegrationIssues([null, 'nope', 42], {json: false}),
|
|
99
|
+
).resolves.toBeUndefined();
|
|
100
|
+
expect(errLines).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
});
|