@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
package/src/lib/integrations.mjs
CHANGED
|
@@ -3,95 +3,89 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Integration manifest loading for Astryx config.
|
|
5
5
|
*
|
|
6
|
-
* Integrations are
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Integrations are PACKAGE NAMES listed in astryx.config.{ts,mjs,js}. Each
|
|
7
|
+
* package declares a single conventional root manifest sibling to its
|
|
8
|
+
* package.json — astryx.integration.{ts,mjs,js} — which contributes
|
|
9
|
+
* components/templates/codemods roots and an optional issuesUrl. Identity
|
|
10
|
+
* (name, version) comes from the package's package.json, not the manifest.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import * as fs from 'node:fs';
|
|
12
14
|
import * as path from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
+
import {AstryxIntegrationSchema} from './config-schema.mjs';
|
|
16
|
+
import {loadModuleWithSchema, findPresentFiles} from './module-loader.mjs';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
/** Conventional manifest basenames, in load-precedence order. */
|
|
19
|
+
export const MANIFEST_BASENAMES = [
|
|
20
|
+
'astryx.integration.ts',
|
|
21
|
+
'astryx.integration.mjs',
|
|
22
|
+
'astryx.integration.js',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return the conventional root manifest paths present in `dir`, in
|
|
27
|
+
* load-precedence order. Unlike {@link resolveManifestPath} this never throws —
|
|
28
|
+
* callers (e.g. validate-integration) decide how to treat zero / multiple.
|
|
29
|
+
* @param {string} dir
|
|
30
|
+
* @returns {string[]} absolute manifest paths
|
|
31
|
+
*/
|
|
32
|
+
export function findManifestPaths(dir) {
|
|
33
|
+
return findPresentFiles(dir, MANIFEST_BASENAMES);
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Load and validate a manifest module's default export against the integration
|
|
38
|
+
* schema. Default export only — `.ts` is loaded via jiti; `.mjs`/`.js` via
|
|
39
|
+
* dynamic import. Throws if the default export is missing or invalid. Exposed
|
|
40
|
+
* for validate-integration.
|
|
41
|
+
* @param {string} file absolute manifest path
|
|
42
|
+
* @param {string} [label] used in error messages
|
|
43
|
+
* @returns {Promise<import('../types/integration').AstryxIntegration>}
|
|
44
|
+
*/
|
|
45
|
+
export async function loadManifestObject(file, label = 'integration manifest') {
|
|
46
|
+
return loadModuleWithSchema(file, AstryxIntegrationSchema, {label});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the directory of an installed integration package from the consumer's
|
|
51
|
+
* node_modules.
|
|
52
|
+
* @param {string} packageName
|
|
53
|
+
* @param {string} [cwd]
|
|
54
|
+
*/
|
|
25
55
|
export function resolvePackageDir(packageName, cwd = process.cwd()) {
|
|
26
56
|
return path.resolve(cwd, 'node_modules', ...packageName.split('/'));
|
|
27
57
|
}
|
|
28
58
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} catch {
|
|
59
|
+
/**
|
|
60
|
+
* Find the single conventional root manifest sibling of the package's
|
|
61
|
+
* package.json. Multiple present is a hard error; missing is a hard error.
|
|
62
|
+
* @param {string} packageDir
|
|
63
|
+
* @param {string} spec
|
|
64
|
+
* @returns {string} absolute manifest path
|
|
65
|
+
*/
|
|
66
|
+
function resolveManifestPath(packageDir, spec) {
|
|
67
|
+
const present = findPresentFiles(packageDir, MANIFEST_BASENAMES);
|
|
68
|
+
if (present.length === 0) {
|
|
40
69
|
throw new Error(
|
|
41
|
-
`
|
|
70
|
+
`Integration package "${spec}" has no conventional root manifest. Add one of: ${MANIFEST_BASENAMES.join(', ')} next to its package.json.`,
|
|
42
71
|
);
|
|
43
72
|
}
|
|
44
|
-
|
|
45
|
-
const manifestPath = pkg.astryx?.integration ?? pkg.xds?.integration;
|
|
46
|
-
if (!manifestPath) {
|
|
73
|
+
if (present.length > 1) {
|
|
47
74
|
throw new Error(
|
|
48
|
-
`
|
|
75
|
+
`Integration package "${spec}" has multiple root manifests (${present
|
|
76
|
+
.map(file => path.basename(file))
|
|
77
|
+
.join(', ')}). Keep exactly one.`,
|
|
49
78
|
);
|
|
50
79
|
}
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function normalizePackageContribution(integration) {
|
|
55
|
-
const docs = integration.docs;
|
|
56
|
-
if (!docs) return null;
|
|
57
|
-
const packageDir = integration.__packageDir ?? integration.__dir;
|
|
58
|
-
const docsDir = path.resolve(packageDir, docs);
|
|
59
|
-
return {
|
|
60
|
-
name: integration.name ?? integration.__spec,
|
|
61
|
-
version: integration.version,
|
|
62
|
-
description: integration.description,
|
|
63
|
-
displayName: integration.displayName,
|
|
64
|
-
dir: packageDir,
|
|
65
|
-
astryx: {
|
|
66
|
-
docs,
|
|
67
|
-
category: integration.category,
|
|
68
|
-
blocks: integration.blocks,
|
|
69
|
-
},
|
|
70
|
-
category: integration.category ?? integration.name ?? integration.__spec,
|
|
71
|
-
docsDir,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function resolveHookFunction(hook, integration) {
|
|
76
|
-
if (typeof hook === 'function') return hook;
|
|
77
|
-
if (typeof hook !== 'string') return hook;
|
|
78
|
-
const [moduleSpec, exportName = 'default'] = hook.split('#');
|
|
79
|
-
return async (...args) => {
|
|
80
|
-
const mod = isPathSpec(moduleSpec)
|
|
81
|
-
? await import(
|
|
82
|
-
pathToFileURL(path.resolve(integration.__dir, moduleSpec)).href
|
|
83
|
-
)
|
|
84
|
-
: await import(moduleSpec);
|
|
85
|
-
const fn = exportName === 'default' ? mod.default : mod[exportName];
|
|
86
|
-
if (typeof fn !== 'function') {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Integration hook ${hook} did not resolve to a function.`,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
return fn(...args);
|
|
92
|
-
};
|
|
80
|
+
return present[0];
|
|
93
81
|
}
|
|
94
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Load configured integrations.
|
|
85
|
+
*
|
|
86
|
+
* @param {string[]} [specs] package names
|
|
87
|
+
* @param {{cwd?: string}} [options]
|
|
88
|
+
*/
|
|
95
89
|
export async function loadIntegrations(specs = [], {cwd = process.cwd()} = {}) {
|
|
96
90
|
const integrations = [];
|
|
97
91
|
const seen = new Set();
|
|
@@ -100,55 +94,38 @@ export async function loadIntegrations(specs = [], {cwd = process.cwd()} = {}) {
|
|
|
100
94
|
if (!spec || seen.has(spec)) continue;
|
|
101
95
|
seen.add(spec);
|
|
102
96
|
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
const packageDir = resolvePackageDir(spec, cwd);
|
|
98
|
+
const pkgPath = path.join(packageDir, 'package.json');
|
|
99
|
+
let pkg;
|
|
100
|
+
try {
|
|
101
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Could not find installed integration package "${spec}" at ${pkgPath}. Install it first.`,
|
|
105
|
+
);
|
|
108
106
|
}
|
|
109
|
-
const integration = validateIntegration(exported, `Integration ${spec}`);
|
|
110
107
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
__file: file,
|
|
118
|
-
__dir: integrationDir,
|
|
119
|
-
__packageDir: packageDir,
|
|
120
|
-
__spec: spec,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
if (Array.isArray(normalized.codemods)) {
|
|
124
|
-
for (const codemod of normalized.codemods) {
|
|
125
|
-
if (typeof codemod.transform === 'string') {
|
|
126
|
-
const transformPath = path.resolve(integrationDir, codemod.transform);
|
|
127
|
-
const transformMod = await import(pathToFileURL(transformPath).href);
|
|
128
|
-
codemod.transform =
|
|
129
|
-
transformMod.default ?? transformMod.transform ?? transformMod;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
108
|
+
const manifestFile = resolveManifestPath(packageDir, spec);
|
|
109
|
+
const manifest = await loadModuleWithSchema(
|
|
110
|
+
manifestFile,
|
|
111
|
+
AstryxIntegrationSchema,
|
|
112
|
+
{label: `Integration ${spec}`},
|
|
113
|
+
);
|
|
133
114
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
normalized.gapReport?.command &&
|
|
137
|
-
isPathSpec(normalized.gapReport.command)
|
|
138
|
-
) {
|
|
139
|
-
normalized.gapReport = {
|
|
140
|
-
...normalized.gapReport,
|
|
141
|
-
command: path.resolve(packageDir, normalized.gapReport.command),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (normalized.template?.get) {
|
|
145
|
-
normalized.template = {
|
|
146
|
-
...normalized.template,
|
|
147
|
-
get: resolveHookFunction(normalized.template.get, normalized),
|
|
148
|
-
};
|
|
149
|
-
}
|
|
115
|
+
const resolveRoot = value =>
|
|
116
|
+
value == null ? undefined : path.resolve(packageDir, value);
|
|
150
117
|
|
|
151
|
-
integrations.push(
|
|
118
|
+
integrations.push({
|
|
119
|
+
name: pkg.name ?? spec,
|
|
120
|
+
version: pkg.version,
|
|
121
|
+
components: resolveRoot(manifest.components),
|
|
122
|
+
templates: resolveRoot(manifest.templates),
|
|
123
|
+
codemods: resolveRoot(manifest.codemods),
|
|
124
|
+
issuesUrl: manifest.issuesUrl,
|
|
125
|
+
__spec: spec,
|
|
126
|
+
__packageDir: packageDir,
|
|
127
|
+
__manifestFile: manifestFile,
|
|
128
|
+
});
|
|
152
129
|
}
|
|
153
130
|
|
|
154
131
|
return integrations;
|
|
@@ -3,91 +3,39 @@
|
|
|
3
3
|
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import {Project} from './project.mjs';
|
|
7
|
+
import {loadIntegrations} from './integrations.mjs';
|
|
7
8
|
import {discover} from '../api/discover.mjs';
|
|
8
|
-
import {loadGapReportConfig} from '../utils/github.mjs';
|
|
9
|
-
import {getTemplateById} from '../api/template.mjs';
|
|
10
9
|
|
|
11
10
|
let tmpDir;
|
|
12
11
|
let originalCwd;
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
path.join(process.cwd(), '.astryx-integration-test-'),
|
|
18
|
-
);
|
|
19
|
-
fs.mkdirSync(path.join(tmpDir, 'node_modules', '@acme', 'widgets'), {
|
|
20
|
-
recursive: true,
|
|
21
|
-
});
|
|
13
|
+
function writeManifestPackage(dir, {basename = 'astryx.integration.mjs', body}) {
|
|
14
|
+
const pkgDir = path.join(dir, 'node_modules', '@acme', 'widgets');
|
|
15
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
22
16
|
fs.writeFileSync(
|
|
23
|
-
path.join(
|
|
24
|
-
`export default { integrations: '@acme/widgets' };\n`,
|
|
25
|
-
);
|
|
26
|
-
fs.writeFileSync(
|
|
27
|
-
path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'package.json'),
|
|
17
|
+
path.join(pkgDir, 'package.json'),
|
|
28
18
|
JSON.stringify({
|
|
29
19
|
name: '@acme/widgets',
|
|
30
20
|
version: '1.2.3',
|
|
31
|
-
displayName: 'Acme Widgets',
|
|
32
|
-
astryx: {integration: './astryx.integration.mjs'},
|
|
33
21
|
}),
|
|
34
22
|
);
|
|
35
|
-
fs.writeFileSync(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
),
|
|
43
|
-
`export default {
|
|
44
|
-
name: '@acme/widgets',
|
|
45
|
-
version: '1.2.3',
|
|
46
|
-
displayName: 'Acme Widgets',
|
|
47
|
-
docs: './docs',
|
|
48
|
-
category: 'Acme',
|
|
49
|
-
gapReport: {command: './scripts/report-gap.sh'},
|
|
50
|
-
template: {get: './template.mjs#getTemplate'},
|
|
51
|
-
codemods: [{name: 'noop', from: '0.0.0', to: '9.0.0', transform: './codemod.mjs'}],
|
|
52
|
-
};\n`,
|
|
53
|
-
);
|
|
54
|
-
fs.mkdirSync(path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'docs'));
|
|
55
|
-
fs.writeFileSync(
|
|
56
|
-
path.join(
|
|
57
|
-
tmpDir,
|
|
58
|
-
'node_modules',
|
|
59
|
-
'@acme',
|
|
60
|
-
'widgets',
|
|
61
|
-
'docs',
|
|
62
|
-
'Widget.doc.mjs',
|
|
63
|
-
),
|
|
64
|
-
`export const doc = {
|
|
65
|
-
name: 'Widget',
|
|
66
|
-
usage: {description: 'Acme widget'},
|
|
67
|
-
props: [],
|
|
68
|
-
};\n`,
|
|
69
|
-
);
|
|
70
|
-
fs.mkdirSync(
|
|
71
|
-
path.join(tmpDir, 'node_modules', '@acme', 'widgets', 'scripts'),
|
|
72
|
-
);
|
|
73
|
-
fs.writeFileSync(
|
|
74
|
-
path.join(
|
|
75
|
-
tmpDir,
|
|
76
|
-
'node_modules',
|
|
77
|
-
'@acme',
|
|
78
|
-
'widgets',
|
|
79
|
-
'scripts',
|
|
80
|
-
'report-gap.sh',
|
|
81
|
-
),
|
|
82
|
-
'#!/bin/sh\ncat >/dev/null\necho ok\n',
|
|
23
|
+
fs.writeFileSync(path.join(pkgDir, basename), body);
|
|
24
|
+
return pkgDir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
originalCwd = process.cwd();
|
|
29
|
+
tmpDir = fs.mkdtempSync(
|
|
30
|
+
path.join(process.cwd(), '.astryx-integration-test-'),
|
|
83
31
|
);
|
|
84
32
|
fs.writeFileSync(
|
|
85
|
-
path.join(tmpDir, '
|
|
86
|
-
|
|
33
|
+
path.join(tmpDir, 'package.json'),
|
|
34
|
+
JSON.stringify({name: 'consumer'}),
|
|
87
35
|
);
|
|
88
36
|
fs.writeFileSync(
|
|
89
|
-
path.join(tmpDir, '
|
|
90
|
-
`export default
|
|
37
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
38
|
+
`export default { integrations: ['@acme/widgets'] };\n`,
|
|
91
39
|
);
|
|
92
40
|
process.chdir(tmpDir);
|
|
93
41
|
});
|
|
@@ -98,57 +46,90 @@ afterEach(() => {
|
|
|
98
46
|
});
|
|
99
47
|
|
|
100
48
|
describe('configured integrations', () => {
|
|
101
|
-
it('
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
49
|
+
it('resolves identity from package.json and contribution roots to absolute paths', async () => {
|
|
50
|
+
const pkgDir = writeManifestPackage(tmpDir, {
|
|
51
|
+
body: `export default {
|
|
52
|
+
components: './docs',
|
|
53
|
+
templates: './blocks',
|
|
54
|
+
codemods: './codemods',
|
|
55
|
+
issuesUrl: 'https://example.com/issues',
|
|
56
|
+
};\n`,
|
|
108
57
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'widgets',
|
|
118
|
-
'scripts',
|
|
119
|
-
'report-gap.sh',
|
|
120
|
-
),
|
|
58
|
+
fs.mkdirSync(path.join(pkgDir, 'docs'));
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
path.join(pkgDir, 'docs', 'Widget.doc.mjs'),
|
|
61
|
+
`export const doc = {
|
|
62
|
+
name: 'Widget',
|
|
63
|
+
usage: {description: 'Acme widget'},
|
|
64
|
+
props: [],
|
|
65
|
+
};\n`,
|
|
121
66
|
);
|
|
122
|
-
|
|
67
|
+
|
|
68
|
+
const project = await Project.load(tmpDir);
|
|
69
|
+
expect(project.integrations).toEqual(['@acme/widgets']);
|
|
70
|
+
const loaded = project.loadedIntegrations[0];
|
|
71
|
+
// Identity comes from package.json, not the manifest.
|
|
72
|
+
expect(loaded.name).toBe('@acme/widgets');
|
|
73
|
+
expect(loaded.version).toBe('1.2.3');
|
|
74
|
+
expect(loaded.components).toBe(path.join(pkgDir, 'docs'));
|
|
75
|
+
expect(loaded.templates).toBe(path.join(pkgDir, 'blocks'));
|
|
76
|
+
expect(loaded.codemods).toBe(path.join(pkgDir, 'codemods'));
|
|
77
|
+
expect(loaded.issuesUrl).toBe('https://example.com/issues');
|
|
123
78
|
});
|
|
124
79
|
|
|
125
|
-
it('makes integration
|
|
80
|
+
it('makes integration components discoverable', async () => {
|
|
81
|
+
const pkgDir = writeManifestPackage(tmpDir, {
|
|
82
|
+
body: `export default { components: './docs' };\n`,
|
|
83
|
+
});
|
|
84
|
+
fs.mkdirSync(path.join(pkgDir, 'docs'));
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
path.join(pkgDir, 'docs', 'Widget.doc.mjs'),
|
|
87
|
+
`export const doc = {
|
|
88
|
+
name: 'Widget',
|
|
89
|
+
usage: {description: 'Acme widget'},
|
|
90
|
+
props: [],
|
|
91
|
+
};\n`,
|
|
92
|
+
);
|
|
93
|
+
|
|
126
94
|
const result = await discover(undefined, {});
|
|
127
95
|
expect(result.type).toBe('discover.list');
|
|
128
96
|
expect(result.data).toEqual([
|
|
129
97
|
expect.objectContaining({
|
|
130
98
|
name: '@acme/widgets',
|
|
131
|
-
category: '
|
|
99
|
+
category: '@acme/widgets',
|
|
132
100
|
components: ['Widget'],
|
|
133
101
|
}),
|
|
134
102
|
]);
|
|
135
103
|
});
|
|
136
104
|
|
|
137
|
-
it('
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
105
|
+
it('errors when the package has no conventional root manifest', async () => {
|
|
106
|
+
const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
|
|
107
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(pkgDir, 'package.json'),
|
|
110
|
+
JSON.stringify({name: '@acme/widgets'}),
|
|
111
|
+
);
|
|
112
|
+
await expect(loadIntegrations(['@acme/widgets'], {cwd: tmpDir})).rejects.toThrow(
|
|
113
|
+
/no conventional root manifest/,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('errors when the package has multiple root manifests', async () => {
|
|
118
|
+
const pkgDir = writeManifestPackage(tmpDir, {
|
|
119
|
+
body: `export default {};\n`,
|
|
152
120
|
});
|
|
121
|
+
fs.writeFileSync(
|
|
122
|
+
path.join(pkgDir, 'astryx.integration.js'),
|
|
123
|
+
`module.exports = {};\n`,
|
|
124
|
+
);
|
|
125
|
+
await expect(loadIntegrations(['@acme/widgets'], {cwd: tmpDir})).rejects.toThrow(
|
|
126
|
+
/multiple root manifests/,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('errors when the package is not installed', async () => {
|
|
131
|
+
await expect(loadIntegrations(['@acme/missing'], {cwd: tmpDir})).rejects.toThrow(
|
|
132
|
+
/Could not find installed integration package/,
|
|
133
|
+
);
|
|
153
134
|
});
|
|
154
135
|
});
|
package/src/lib/manifest.mjs
CHANGED
|
@@ -56,16 +56,15 @@ export const RESPONSE_TYPES = {
|
|
|
56
56
|
'template.show',
|
|
57
57
|
'template.skeleton',
|
|
58
58
|
'template.copy',
|
|
59
|
-
'template.get',
|
|
60
59
|
],
|
|
61
60
|
hook: ['hook.list', 'hook.brief', 'hook.full', 'hook.detail', 'hook.detail.params'],
|
|
62
61
|
'theme build': ['theme.build'],
|
|
63
62
|
'theme list': ['theme.list'],
|
|
64
63
|
'theme add': ['theme.list', 'theme.add'],
|
|
65
64
|
upgrade: ['upgrade.list', 'upgrade.status', 'upgrade.run'],
|
|
66
|
-
'gap-report': ['gap-report.categories', 'gap-report.dryRun', 'gap-report.file'],
|
|
67
65
|
manifest: ['manifest'],
|
|
68
66
|
doctor: ['doctor'],
|
|
67
|
+
'validate-integration': ['integration.validate'],
|
|
69
68
|
'layout expand': ['layout.expand'],
|
|
70
69
|
'layout check': ['layout.check'],
|
|
71
70
|
'layout grammar': ['layout.grammar'],
|
|
@@ -89,6 +88,10 @@ const EXAMPLES = {
|
|
|
89
88
|
upgrade: ['astryx upgrade --json'],
|
|
90
89
|
manifest: ['astryx manifest --json', 'astryx --json'],
|
|
91
90
|
doctor: ['astryx doctor', 'astryx doctor --json'],
|
|
91
|
+
'validate-integration': [
|
|
92
|
+
'astryx validate-integration',
|
|
93
|
+
'astryx validate-integration @acme/widgets --json',
|
|
94
|
+
],
|
|
92
95
|
init: ['astryx init'],
|
|
93
96
|
'layout expand': [`astryx layout expand 'V[g6] > C{card-callout}*4' ./src/Page.tsx`],
|
|
94
97
|
'layout check': [`astryx layout check 'A[cp6] > L > LC > S[p6]' --json`],
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Shared module-loading + conventional-file-discovery primitives.
|
|
5
|
+
*
|
|
6
|
+
* Both config loading and integration loading need to (a) import a
|
|
7
|
+
* user-authored module (`.ts` via jiti, `.mjs`/`.js` via native dynamic
|
|
8
|
+
* import) and (b) find conventional files by basename in a fixed
|
|
9
|
+
* load-precedence order. These helpers centralize that so the two callers stay
|
|
10
|
+
* in lockstep.
|
|
11
|
+
*
|
|
12
|
+
* `loadModuleWithSchema` builds on these primitives to provide the single
|
|
13
|
+
* load/validation boundary shared by config, integration, codemod, and
|
|
14
|
+
* template discovery: import the module, take its default export, and validate
|
|
15
|
+
* it against the expected zod schema.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import {pathToFileURL} from 'node:url';
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import {createJiti} from 'jiti';
|
|
22
|
+
import {formatZodError} from './config-schema.mjs';
|
|
23
|
+
|
|
24
|
+
let jitiInstance;
|
|
25
|
+
function getJiti() {
|
|
26
|
+
if (!jitiInstance) {
|
|
27
|
+
jitiInstance = createJiti(import.meta.url);
|
|
28
|
+
}
|
|
29
|
+
return jitiInstance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Import a user-authored module. `.ts` is loaded via jiti; `.mjs`/`.js` via
|
|
34
|
+
* native dynamic import (file:// URL). Returns the full module namespace.
|
|
35
|
+
* @param {string} file absolute path
|
|
36
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
37
|
+
*/
|
|
38
|
+
export async function importUserModule(file) {
|
|
39
|
+
if (file.endsWith('.ts')) {
|
|
40
|
+
return await getJiti().import(file);
|
|
41
|
+
}
|
|
42
|
+
return await import(pathToFileURL(file).href);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Return the conventional files (by basename, in the given precedence order)
|
|
47
|
+
* that exist directly in `dir`, as absolute paths. Never throws.
|
|
48
|
+
* @param {string} dir
|
|
49
|
+
* @param {string[]} basenames precedence-ordered
|
|
50
|
+
* @returns {string[]} absolute paths of present files, in basenames order
|
|
51
|
+
*/
|
|
52
|
+
export function findPresentFiles(dir, basenames) {
|
|
53
|
+
return basenames
|
|
54
|
+
.filter(name => fs.existsSync(path.join(dir, name)))
|
|
55
|
+
.map(name => path.join(dir, name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Import a user-authored module, take its default export, and validate it
|
|
60
|
+
* against a zod schema. This is the single load/validation boundary for all
|
|
61
|
+
* user-authored modules (config, integration, codemod, template): execute the
|
|
62
|
+
* module, take the default export, validate it against the expected schema.
|
|
63
|
+
* Throws a clear, readable error if the default export is missing or fails
|
|
64
|
+
* validation.
|
|
65
|
+
*
|
|
66
|
+
* @template {import('zod').ZodType} S
|
|
67
|
+
* @param {string} file absolute path
|
|
68
|
+
* @param {S} schema
|
|
69
|
+
* @param {{label?: string}} [opts] label used in error messages
|
|
70
|
+
* @returns {Promise<import('zod').infer<S>>} parsed + typed value
|
|
71
|
+
*/
|
|
72
|
+
export async function loadModuleWithSchema(file, schema, {label} = {}) {
|
|
73
|
+
const mod = await importUserModule(file);
|
|
74
|
+
const exported = mod?.default;
|
|
75
|
+
const result = schema.safeParse(exported);
|
|
76
|
+
if (!result.success) {
|
|
77
|
+
throw new Error(formatZodError(label ?? file, result.error));
|
|
78
|
+
}
|
|
79
|
+
return result.data;
|
|
80
|
+
}
|