@astryxdesign/cli 0.1.1 → 0.1.2-canary.043f3fd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/docs/theme.doc.mjs +2 -2
- package/docs/working-with-ai.doc.mjs +6 -6
- package/package.json +31 -8
- package/src/api/component.mjs +253 -12
- package/src/api/discover.mjs +87 -28
- package/src/api/doctor.mjs +7 -22
- package/src/api/index.mjs +1 -0
- package/src/api/layout.mjs +302 -0
- package/src/api/layout.test.mjs +241 -0
- package/src/api/template-integration.test.mjs +225 -0
- package/src/api/template.mjs +421 -122
- package/src/api/template.test.mjs +2 -0
- package/src/api/theme-add.mjs +182 -0
- package/src/api/validate-integration.mjs +370 -0
- package/src/api/validate-integration.test.mjs +222 -0
- package/src/codemod.mjs +93 -0
- package/src/codemod.test.mjs +134 -0
- package/src/codemods/__tests__/registry.test.mjs +2 -0
- package/src/codemods/__tests__/runner.test.mjs +103 -0
- package/src/codemods/integration-discovery.mjs +168 -0
- package/src/codemods/integration-discovery.test.mjs +234 -0
- package/src/codemods/integration-runner.mjs +109 -0
- package/src/codemods/registry.mjs +2 -0
- package/src/codemods/run-codemod.mjs +207 -0
- package/src/codemods/runner.mjs +73 -96
- package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +81 -0
- package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
- package/src/codemods/transforms/v0.1.0/index.mjs +15 -6
- package/src/codemods/transforms/v0.1.2/__tests__/rename-text-color-active-to-accent.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.2/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.2/rename-text-color-active-to-accent.mjs +136 -0
- package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
- package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
- package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
- package/src/commands/build-theme.import-path.test.mjs +22 -2
- package/src/commands/build-theme.mjs +185 -16
- package/src/commands/component/index.mjs +44 -10
- package/src/commands/component-ownership.test.mjs +227 -0
- package/src/commands/discover.mjs +5 -16
- package/src/commands/doctor.test.mjs +3 -3
- package/src/commands/init.mjs +34 -8
- package/src/commands/init.next-steps.test.mjs +46 -0
- package/src/commands/json-contract.test.mjs +0 -32
- package/src/commands/layout.mjs +139 -0
- package/src/commands/swizzle.mjs +241 -224
- package/src/commands/swizzle.path-safety.test.mjs +3 -3
- package/src/commands/swizzle.routing.test.mjs +279 -0
- package/src/commands/template.mjs +33 -37
- package/src/commands/upgrade.config-ordering.test.mjs +273 -0
- package/src/commands/upgrade.integration-policy.test.mjs +204 -0
- package/src/commands/upgrade.mjs +349 -239
- package/src/commands/validate-integration.mjs +110 -0
- package/src/commands/validate-integration.test.mjs +124 -0
- package/src/config.mjs +18 -0
- package/src/config.test.mjs +100 -0
- package/src/index.mjs +13 -3
- package/src/integration.mjs +19 -0
- package/src/lib/component-discovery.mjs +177 -0
- package/src/lib/config-cache.mjs +91 -0
- package/src/lib/config-cache.test.mjs +83 -0
- package/src/lib/config-schema.mjs +105 -0
- package/src/lib/error-codes.mjs +18 -12
- package/src/lib/integration-warnings.mjs +62 -0
- package/src/lib/integration-warnings.test.mjs +102 -0
- package/src/lib/integrations.mjs +132 -0
- package/src/lib/integrations.test.mjs +135 -0
- package/src/lib/levenshtein.mjs +29 -0
- package/src/lib/manifest.mjs +15 -2
- package/src/lib/module-loader.mjs +80 -0
- package/src/lib/module-loader.test.mjs +106 -0
- package/src/lib/package-scanner.mjs +31 -7
- package/src/lib/project.mjs +502 -0
- package/src/lib/project.test.mjs +308 -0
- package/src/lib/string-utils.mjs +5 -14
- package/src/lib/xle/browser.d.ts +100 -0
- package/src/lib/xle/browser.mjs +120 -0
- package/src/lib/xle/expand.mjs +622 -0
- package/src/lib/xle/parse.mjs +581 -0
- package/src/lib/xle/print.mjs +174 -0
- package/src/lib/xle/registry-core.mjs +170 -0
- package/src/lib/xle/registry.mjs +237 -0
- package/src/lib/xle/splice.mjs +137 -0
- package/src/lib/xle/validate.mjs +356 -0
- package/src/lib/xle/xle.test.mjs +333 -0
- package/src/template.mjs +73 -0
- package/src/template.test.mjs +127 -0
- package/src/types/api.d.ts +2 -6
- package/src/types/base.d.ts +3 -9
- package/src/types/codemod.d.ts +81 -0
- package/src/types/component.d.ts +27 -2
- package/src/types/config.d.ts +70 -0
- package/src/types/error-codes.d.ts +5 -3
- package/src/types/index.d.ts +0 -1
- package/src/types/integration.d.ts +29 -0
- package/src/types/swizzle.d.ts +9 -2
- package/src/types/template-api.d.ts +54 -0
- package/src/types/template.d.ts +10 -7
- package/src/types/upgrade.d.ts +29 -0
- package/src/types/validate-integration.d.ts +24 -0
- package/src/utils/github.mjs +0 -252
- package/src/utils/interactive.mjs +2 -2
- package/templates/blocks/components/Card/ClickableCardWithNestedButton.doc.mjs +1 -1
- package/templates/blocks/components/Card/SelectableCardMulti.doc.mjs +1 -1
- package/templates/blocks/components/ChatComposerDrawer/ChatComposerDrawerWithProgress.tsx +1 -1
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonShowcase.tsx +3 -3
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.doc.mjs +15 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyShowcase.tsx +26 -0
- package/templates/blocks/components/DateInput/DateInputDateRange.doc.mjs +2 -2
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
- package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
- package/templates/blocks/components/FileInput/FileInputShowcase.tsx +4 -2
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
- package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +14 -0
- package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.tsx +43 -0
- package/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx +6 -2
- package/templates/blocks/components/NumberInput/NumberInputShowcase.tsx +6 -2
- package/templates/blocks/components/Outline/OutlineShowcase.doc.mjs +15 -0
- package/templates/blocks/components/Outline/OutlineShowcase.tsx +22 -0
- package/templates/blocks/components/RadioList/RadioListShowcase.tsx +8 -1
- package/templates/blocks/components/SegmentedControl/SegmentedControlShowcase.tsx +3 -1
- package/templates/blocks/components/Selector/SelectorShowcase.tsx +4 -1
- package/templates/blocks/components/Slider/SliderShowcase.tsx +10 -1
- package/templates/blocks/components/Tab/TabShowcase.tsx +3 -1
- package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
- package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
- package/templates/blocks/components/TabList/TabListShowcase.tsx +3 -1
- package/templates/blocks/components/TabMenu/TabMenuShowcase.tsx +3 -1
- package/templates/blocks/components/Table/ColumnResizeHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/ColumnResizeHookUsage.tsx +59 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Table/StickyColumnsHookUsage.tsx +104 -0
- package/templates/blocks/components/Text/TextColors.tsx +20 -5
- package/templates/blocks/components/TextArea/TextAreaShowcase.tsx +4 -2
- package/templates/blocks/components/TextInput/TextInputShowcase.tsx +4 -2
- package/templates/blocks/components/Thumbnail/ThumbnailDisabled.tsx +11 -6
- package/templates/blocks/components/Thumbnail/ThumbnailGallery.tsx +43 -7
- package/templates/blocks/components/Thumbnail/ThumbnailRemovable.tsx +40 -3
- package/templates/blocks/components/Thumbnail/ThumbnailShowcase.tsx +6 -5
- package/templates/blocks/components/Thumbnail/ThumbnailStates.tsx +11 -6
- package/templates/blocks/components/Timestamp/TimestampColors.tsx +2 -2
- package/templates/blocks/components/ToggleButton/ToggleButtonGroup.doc.mjs +1 -1
- package/templates/blocks/components/Tokenizer/TokenizerShowcase.tsx +8 -6
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.doc.mjs +2 -2
- package/templates/blocks/components/Toolbar/ToolbarTableFilter.tsx +23 -14
- package/templates/blocks/components/Typeahead/TypeaheadShowcase.tsx +6 -2
- package/templates/pages/shell-nav/page.tsx +321 -0
- package/templates/pages/shell-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +242 -0
- package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
- package/templates/pages/shell-top-nav/page.tsx +224 -0
- package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
- package/templates/pages/theme-showcase/page.tsx +8 -19
- package/templates/themes/butter/butterTheme.ts +916 -0
- package/templates/themes/butter/icons.tsx +77 -0
- package/templates/themes/chocolate/chocolateTheme.ts +230 -0
- package/templates/themes/chocolate/icons.tsx +77 -0
- package/templates/themes/gothic/gothicTheme.ts +657 -0
- package/templates/themes/gothic/icons.tsx +77 -0
- package/templates/themes/manifest.json +90 -0
- package/templates/themes/matcha/icons.tsx +67 -0
- package/templates/themes/matcha/matchaTheme.ts +247 -0
- package/templates/themes/neutral/icons.tsx +77 -0
- package/templates/themes/neutral/neutralTheme.ts +603 -0
- package/templates/themes/stone/icons.tsx +77 -0
- package/templates/themes/stone/stoneTheme.ts +652 -0
- package/templates/themes/y2k/icons.tsx +67 -0
- package/templates/themes/y2k/y2kTheme.ts +617 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
- package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
- package/src/commands/gap-report.mjs +0 -456
- package/src/commands/gap-report.test.mjs +0 -163
- package/src/commands/swizzle-gap-safety.test.mjs +0 -273
- package/src/lib/config.mjs +0 -86
- package/src/lib/config.test.mjs +0 -42
- package/src/types/gap-report.d.ts +0 -29
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.doc.mjs +0 -14
- package/templates/blocks/components/MoreMenu/MoreMenuInToolbar.tsx +0 -57
- /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: migrate astryx.config `layout.components` to
|
|
5
|
+
* `experimental.xle.components`.
|
|
6
|
+
*
|
|
7
|
+
* The published 0.1.2 CLI read XLE app-component registration from
|
|
8
|
+
* `astryx.config.*` under `layout.components`. The next release relocates this
|
|
9
|
+
* to `experimental.xle.components`, validated by a strict schema that REJECTS
|
|
10
|
+
* unknown keys — so any consumer that still has `layout.components` would
|
|
11
|
+
* hard-error on upgrade. This codemod performs the straight relocation.
|
|
12
|
+
*
|
|
13
|
+
* The semantics are IDENTICAL between the old and new shapes; this is purely a
|
|
14
|
+
* MOVE plus a string→object normalization. No meaning is changed:
|
|
15
|
+
*
|
|
16
|
+
* OLD `layout.components` entry forms (from the 0.1.2 shim):
|
|
17
|
+
* - string `'X'` → a NAMED import; the KEY is both the local name and the
|
|
18
|
+
* export name.
|
|
19
|
+
* - object `{ from, description?, default? }` → `default: true` means a
|
|
20
|
+
* default import; otherwise a named import (key = export name).
|
|
21
|
+
*
|
|
22
|
+
* NEW `experimental.xle.components` entry shape (already shipped on the
|
|
23
|
+
* branch; unchanged here): `{ from, description?, default? }`, with the same
|
|
24
|
+
* semantics.
|
|
25
|
+
*
|
|
26
|
+
* So the migration is:
|
|
27
|
+
* - string `'X'` → `{ from: 'X' }`
|
|
28
|
+
* - object `{ from, description?, default? }` → carried over UNCHANGED
|
|
29
|
+
* (from/description/default preserved, key order preserved).
|
|
30
|
+
*
|
|
31
|
+
* The transform recognizes the config default export as EITHER a bare object
|
|
32
|
+
* literal (`export default { ... }`) or a `createConfig({ ... })` call wrapping
|
|
33
|
+
* an object literal (`export default createConfig({ ... })`). Anything else is
|
|
34
|
+
* not statically analyzable and the transform THROWS with a clear
|
|
35
|
+
* migrate-manually message (the unified config-codemod contract has no
|
|
36
|
+
* structured-error return — `runConfigCodemod` surfaces a thrown `err.message`
|
|
37
|
+
* as `{file, codemod, error}`).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export const meta = {
|
|
41
|
+
title: 'Migrate astryx.config layout.components to experimental.xle.components',
|
|
42
|
+
description:
|
|
43
|
+
'Relocates XLE app-component registration from the legacy ' +
|
|
44
|
+
'`layout.components` config key to `experimental.xle.components`. String ' +
|
|
45
|
+
'entries (`Name: "@/path"`) are normalized to `{ from: "@/path" }` ' +
|
|
46
|
+
'(named import, key = export name); object entries are carried over ' +
|
|
47
|
+
'unchanged (from/description/default preserved). Bails with a clear ' +
|
|
48
|
+
'migrate-manually error when the config cannot be statically analyzed or ' +
|
|
49
|
+
'when merging would be unsafe.',
|
|
50
|
+
codemodType: 'config',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const MANUAL =
|
|
54
|
+
'migrate layout.components to experimental.xle.components manually';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {{source: string, path: string}} file
|
|
58
|
+
* @param {{jscodeshift: Function}} api
|
|
59
|
+
* @returns {string|null}
|
|
60
|
+
*/
|
|
61
|
+
export default function transformer(file, api) {
|
|
62
|
+
const j = api.jscodeshift;
|
|
63
|
+
const root = j(file.source);
|
|
64
|
+
|
|
65
|
+
const defaultExports = root.find(j.ExportDefaultDeclaration);
|
|
66
|
+
if (defaultExports.size() === 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`could not statically analyze astryx config (no default export); ${MANUAL}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const declaration = defaultExports.paths()[0].node.declaration;
|
|
73
|
+
|
|
74
|
+
// Resolve the config object literal from either form:
|
|
75
|
+
// export default { ... }
|
|
76
|
+
// export default createConfig({ ... })
|
|
77
|
+
const configObject = resolveConfigObject(j, declaration);
|
|
78
|
+
if (!configObject) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`could not statically analyze astryx config; ${MANUAL}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Locate the top-level `layout` property and its `components` value.
|
|
85
|
+
const layoutProp = findObjectProperty(j, configObject, 'layout');
|
|
86
|
+
if (!layoutProp) {
|
|
87
|
+
return null; // no layout → nothing to migrate
|
|
88
|
+
}
|
|
89
|
+
if (!j.ObjectExpression.check(layoutProp.value)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`astryx config \`layout\` is not an object literal; ${MANUAL}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const componentsProp = findObjectProperty(j, layoutProp.value, 'components');
|
|
96
|
+
if (!componentsProp) {
|
|
97
|
+
return null; // no layout.components → nothing to migrate
|
|
98
|
+
}
|
|
99
|
+
if (!j.ObjectExpression.check(componentsProp.value)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`astryx config \`layout.components\` is not an object literal; ${MANUAL}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// `components` was the only documented `layout` key. Anything else is
|
|
106
|
+
// unexpected and we cannot safely guess intent.
|
|
107
|
+
const otherLayoutKeys = layoutProp.value.properties.filter(
|
|
108
|
+
p => p !== componentsProp,
|
|
109
|
+
);
|
|
110
|
+
if (otherLayoutKeys.length > 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`astryx config \`layout\` has keys other than \`components\`; ${MANUAL}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build the migrated `components` object literal entry by entry.
|
|
117
|
+
const migratedComponents = buildMigratedComponents(j, componentsProp.value);
|
|
118
|
+
|
|
119
|
+
// Placement into `experimental.xle.components`.
|
|
120
|
+
const experimentalProp = findObjectProperty(j, configObject, 'experimental');
|
|
121
|
+
if (experimentalProp) {
|
|
122
|
+
if (!j.ObjectExpression.check(experimentalProp.value)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`astryx config \`experimental\` is not an object literal; ${MANUAL}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const xleProp = findObjectProperty(j, experimentalProp.value, 'xle');
|
|
128
|
+
if (xleProp) {
|
|
129
|
+
// Can't safely merge into a pre-existing experimental.xle.
|
|
130
|
+
throw new Error(
|
|
131
|
+
`astryx config already defines \`experimental.xle\`; ${MANUAL}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
experimentalProp.value.properties.push(
|
|
135
|
+
j.objectProperty(j.identifier('xle'), buildXleObject(j, migratedComponents)),
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
configObject.properties.push(
|
|
139
|
+
j.objectProperty(
|
|
140
|
+
j.identifier('experimental'),
|
|
141
|
+
j.objectExpression([
|
|
142
|
+
j.objectProperty(j.identifier('xle'), buildXleObject(j, migratedComponents)),
|
|
143
|
+
]),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remove the migrated data from `layout`. Since `components` was the only
|
|
149
|
+
// key (asserted above), remove the entire `layout` property.
|
|
150
|
+
configObject.properties = configObject.properties.filter(
|
|
151
|
+
p => p !== layoutProp,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return root.toSource();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolve the wrapped config object literal from a default-export declaration.
|
|
159
|
+
* Handles `{ ... }` and `createConfig({ ... })`.
|
|
160
|
+
* @returns {object|null} the ObjectExpression node, or null if not analyzable
|
|
161
|
+
*/
|
|
162
|
+
function resolveConfigObject(j, declaration) {
|
|
163
|
+
if (!declaration) return null;
|
|
164
|
+
if (j.ObjectExpression.check(declaration)) {
|
|
165
|
+
return declaration;
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
j.CallExpression.check(declaration) &&
|
|
169
|
+
declaration.arguments.length >= 1 &&
|
|
170
|
+
j.ObjectExpression.check(declaration.arguments[0])
|
|
171
|
+
) {
|
|
172
|
+
return declaration.arguments[0];
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find a (non-computed, non-spread) property on an object literal by key name.
|
|
179
|
+
* Supports Identifier and string-literal keys.
|
|
180
|
+
* @returns {object|undefined} the property node
|
|
181
|
+
*/
|
|
182
|
+
function findObjectProperty(j, objectExpression, name) {
|
|
183
|
+
return objectExpression.properties.find(prop => {
|
|
184
|
+
if (
|
|
185
|
+
!j.ObjectProperty.check(prop) &&
|
|
186
|
+
!j.Property.check(prop)
|
|
187
|
+
) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (prop.computed) return false;
|
|
191
|
+
const key = prop.key;
|
|
192
|
+
if (j.Identifier.check(key)) return key.name === name;
|
|
193
|
+
if (j.StringLiteral.check(key)) return key.value === name;
|
|
194
|
+
if (j.Literal.check(key)) return key.value === name;
|
|
195
|
+
return false;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build the migrated `components` ObjectExpression from the old one.
|
|
201
|
+
* @returns {object} ObjectExpression
|
|
202
|
+
*/
|
|
203
|
+
function buildMigratedComponents(j, oldComponents) {
|
|
204
|
+
const newProps = oldComponents.properties.map(prop => {
|
|
205
|
+
if (
|
|
206
|
+
(!j.ObjectProperty.check(prop) && !j.Property.check(prop)) ||
|
|
207
|
+
prop.computed
|
|
208
|
+
) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`astryx config \`layout.components\` has a non-literal entry; ${MANUAL}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const value = prop.value;
|
|
215
|
+
|
|
216
|
+
// string `'X'` → `{ from: 'X' }`
|
|
217
|
+
if (j.StringLiteral.check(value) || (j.Literal.check(value) && typeof value.value === 'string')) {
|
|
218
|
+
return j.objectProperty(
|
|
219
|
+
cloneKey(j, prop.key),
|
|
220
|
+
j.objectExpression([
|
|
221
|
+
j.objectProperty(j.identifier('from'), cloneStringLiteral(j, value)),
|
|
222
|
+
]),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// object `{ from, description?, default? }` → carried over UNCHANGED
|
|
227
|
+
if (j.ObjectExpression.check(value)) {
|
|
228
|
+
return j.objectProperty(cloneKey(j, prop.key), value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// anything else (identifier, call, ternary, etc.) → cannot safely migrate
|
|
232
|
+
throw new Error(
|
|
233
|
+
`astryx config \`layout.components\` entry is neither a string nor an ` +
|
|
234
|
+
`object literal and cannot be safely migrated; ${MANUAL}`,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return j.objectExpression(newProps);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Build `{ components: <obj> }` for `experimental.xle`. */
|
|
242
|
+
function buildXleObject(j, migratedComponents) {
|
|
243
|
+
return j.objectExpression([
|
|
244
|
+
j.objectProperty(j.identifier('components'), migratedComponents),
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Clone a property key node so we don't reuse a node across the tree. */
|
|
249
|
+
function cloneKey(j, key) {
|
|
250
|
+
if (j.Identifier.check(key)) return j.identifier(key.name);
|
|
251
|
+
if (j.StringLiteral.check(key)) return j.stringLiteral(key.value);
|
|
252
|
+
if (j.Literal.check(key)) return j.literal(key.value);
|
|
253
|
+
return key;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Build a fresh string literal preserving the original raw quoting if possible. */
|
|
257
|
+
function cloneStringLiteral(j, value) {
|
|
258
|
+
if (j.StringLiteral.check(value)) return j.stringLiteral(value.value);
|
|
259
|
+
return j.literal(value.value);
|
|
260
|
+
}
|
|
@@ -77,7 +77,7 @@ afterEach(() => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
describe('theme build install-instructions import path', () => {
|
|
80
|
-
it('emits ./<name> (no double slash) when out dir equals cwd', () => {
|
|
80
|
+
it('emits bare ./<name> specifiers (no double slash) when out dir equals cwd', () => {
|
|
81
81
|
// Source theme lives directly in the project root, so the output dir
|
|
82
82
|
// is the cwd and the relative dir is an empty string.
|
|
83
83
|
const project = path.join(tmpDir, 'project');
|
|
@@ -97,7 +97,7 @@ describe('theme build install-instructions import path', () => {
|
|
|
97
97
|
expect(result.stdout).toContain('href="./cwd-theme.css"');
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
it('
|
|
100
|
+
it('keeps the subdir in the import path for a non-src subdirectory build', () => {
|
|
101
101
|
const project = path.join(tmpDir, 'project');
|
|
102
102
|
const themeFile = writeTheme(path.join(project, 'themes'), 'sub-theme');
|
|
103
103
|
|
|
@@ -112,4 +112,24 @@ describe('theme build install-instructions import path', () => {
|
|
|
112
112
|
expect(result.stdout).toContain("import './themes/sub-theme.css'");
|
|
113
113
|
expect(result.stdout).toContain('href="./themes/sub-theme.css"');
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
it('strips a leading src/ from the import path (relative to a file in src/)', () => {
|
|
117
|
+
const project = path.join(tmpDir, 'project');
|
|
118
|
+
const themeFile = writeTheme(
|
|
119
|
+
path.join(project, 'src', 'themes', 'ocean'),
|
|
120
|
+
'ocean',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const result = runCli(
|
|
124
|
+
['theme', 'build', path.relative(project, themeFile)],
|
|
125
|
+
project,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result.code).toBe(0);
|
|
129
|
+
expect(result.stdout).not.toContain('.//');
|
|
130
|
+
// The leading src/ is dropped, but themes/ocean/ is kept (people need it).
|
|
131
|
+
expect(result.stdout).toContain("from './themes/ocean/ocean'");
|
|
132
|
+
expect(result.stdout).toContain("import './themes/ocean/ocean.css'");
|
|
133
|
+
expect(result.stdout).not.toContain('./src/');
|
|
134
|
+
});
|
|
115
135
|
});
|
|
@@ -17,10 +17,15 @@ import * as path from 'node:path';
|
|
|
17
17
|
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
18
18
|
import {createJiti} from 'jiti';
|
|
19
19
|
import {getRunPrefix} from '../utils/package-manager.mjs';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
sanitizeName,
|
|
22
|
+
PathSafetyError,
|
|
23
|
+
isNonInteractive,
|
|
24
|
+
} from '../utils/path-safety.mjs';
|
|
21
25
|
import {jsonOut, humanLog} from '../lib/json.mjs';
|
|
22
26
|
import {cliError} from '../lib/cli-error.mjs';
|
|
23
27
|
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
28
|
+
import {themeAdd, listThemes} from '../api/theme-add.mjs';
|
|
24
29
|
|
|
25
30
|
// Import shared theme processing from core. `astryx theme build` MUST produce the
|
|
26
31
|
// exact same CSS as the `<Theme>` runtime, so it has exactly one generation
|
|
@@ -75,6 +80,18 @@ function toIdentifier(name) {
|
|
|
75
80
|
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Import specifier for install/scaffold instructions. Drops a leading `src/`
|
|
85
|
+
* from the cwd-relative dir (most consumers import from a file under src/) but
|
|
86
|
+
* keeps the rest of the path (e.g. `themes/gothic`). Callers note the path is
|
|
87
|
+
* relative to the consumer's file.
|
|
88
|
+
*/
|
|
89
|
+
function importSpecifier(relDir, base) {
|
|
90
|
+
const normalized = relDir === '.' ? '' : relDir;
|
|
91
|
+
const withinSrc = normalized.replace(/^src\/?/, '').replace(/\/+$/, '');
|
|
92
|
+
return withinSrc ? `./${withinSrc}/${base}` : `./${base}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
/**
|
|
79
96
|
* Convert a kebab-case component name to PascalCase.
|
|
80
97
|
* e.g. 'button' → 'Button', 'progress-bar' → 'ProgressBar', 'avatar-status-dot' → 'AvatarStatusDot'
|
|
@@ -443,7 +460,7 @@ const KNOWN_COMPONENTS = {
|
|
|
443
460
|
breadcrumbs: ['variant'],
|
|
444
461
|
button: ['variant', 'size'],
|
|
445
462
|
calendar: [],
|
|
446
|
-
card: [],
|
|
463
|
+
card: ['variant'],
|
|
447
464
|
center: [],
|
|
448
465
|
checkboxinput: [],
|
|
449
466
|
collapsible: [],
|
|
@@ -737,10 +754,12 @@ export function registerTheme(program) {
|
|
|
737
754
|
const sourceRelative = path.relative(process.cwd(), filePath);
|
|
738
755
|
const buildCommand = `astryx theme build ${sourceRelative}${options.out ? ' --out ' + path.relative(process.cwd(), path.resolve(process.cwd(), options.out)) : ''}`;
|
|
739
756
|
|
|
740
|
-
//
|
|
757
|
+
// Derive the default CSS name from the theme name so .css/.js/.d.ts
|
|
758
|
+
// share one scheme; an explicit --out still wins.
|
|
759
|
+
const baseName = themeDef.name;
|
|
741
760
|
const outPath = options.out
|
|
742
761
|
? path.resolve(process.cwd(), options.out)
|
|
743
|
-
:
|
|
762
|
+
: path.join(path.dirname(filePath), `${baseName}.css`);
|
|
744
763
|
|
|
745
764
|
const displayTheme = resolvedTheme || themeDef;
|
|
746
765
|
const tokenCount = displayTheme.tokens ? Object.keys(displayTheme.tokens).length : 0;
|
|
@@ -753,7 +772,6 @@ export function registerTheme(program) {
|
|
|
753
772
|
// was left as orphaned half-built output. Stage-then-commit avoids
|
|
754
773
|
// that.
|
|
755
774
|
const outDir = path.dirname(outPath);
|
|
756
|
-
const baseName = themeDef.name;
|
|
757
775
|
const jsPath = path.join(outDir, `${baseName}.js`);
|
|
758
776
|
const dtsPath = path.join(outDir, `${baseName}.d.ts`);
|
|
759
777
|
|
|
@@ -834,18 +852,16 @@ export function registerTheme(program) {
|
|
|
834
852
|
});
|
|
835
853
|
}
|
|
836
854
|
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
// prefix that collapses to './' when relDir is empty.
|
|
842
|
-
const importPrefix = relDir ? `./${relDir}/` : './';
|
|
855
|
+
const relOutDir = path.relative(process.cwd(), outDir) || '.';
|
|
856
|
+
const cssBase = path.basename(outPath, '.css');
|
|
857
|
+
const jsImport = importSpecifier(relOutDir, baseName);
|
|
858
|
+
const cssImport = importSpecifier(relOutDir, cssBase) + '.css';
|
|
843
859
|
const exportName = `${toIdentifier(baseName)}Theme`;
|
|
844
860
|
humanLog(`
|
|
845
|
-
Install in your app:
|
|
861
|
+
Install in your app (paths are relative to a file in src/ — adjust if yours lives elsewhere):
|
|
846
862
|
|
|
847
|
-
import { ${exportName} } from '${
|
|
848
|
-
import '${
|
|
863
|
+
import { ${exportName} } from '${jsImport}';
|
|
864
|
+
import '${cssImport}';
|
|
849
865
|
|
|
850
866
|
<Theme theme={${exportName}}>
|
|
851
867
|
<App />
|
|
@@ -853,9 +869,9 @@ Install in your app:
|
|
|
853
869
|
|
|
854
870
|
Or with a <link> tag:
|
|
855
871
|
|
|
856
|
-
import { ${exportName} } from '${
|
|
872
|
+
import { ${exportName} } from '${jsImport}';
|
|
857
873
|
|
|
858
|
-
<link rel="stylesheet" href="${
|
|
874
|
+
<link rel="stylesheet" href="${cssImport}" />
|
|
859
875
|
<Theme theme={${exportName}}>
|
|
860
876
|
<App />
|
|
861
877
|
</Theme>
|
|
@@ -871,4 +887,157 @@ Or with a <link> tag:
|
|
|
871
887
|
humanLog('');
|
|
872
888
|
}
|
|
873
889
|
});
|
|
890
|
+
|
|
891
|
+
theme
|
|
892
|
+
.command('list')
|
|
893
|
+
.description('List themes available to add')
|
|
894
|
+
.action(async () => {
|
|
895
|
+
const json = program.opts().json || false;
|
|
896
|
+
let result;
|
|
897
|
+
try {
|
|
898
|
+
result = await themeAdd(undefined, {list: true, cwd: process.cwd()});
|
|
899
|
+
} catch (e) {
|
|
900
|
+
cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (json) return jsonOut(result.type, result.data);
|
|
905
|
+
|
|
906
|
+
const themes = result.data;
|
|
907
|
+
if (themes.length === 0) {
|
|
908
|
+
humanLog('\nNo themes are bundled with this CLI build.\n');
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
humanLog('\nThemes:\n');
|
|
912
|
+
for (const t of themes) {
|
|
913
|
+
const tag = t.maintained ? ' (maintained)' : '';
|
|
914
|
+
humanLog(` ${t.slug}${tag}`);
|
|
915
|
+
if (t.description) humanLog(` ${t.description}`);
|
|
916
|
+
}
|
|
917
|
+
humanLog('\nUsage:');
|
|
918
|
+
humanLog(' astryx theme add <slug> [target-path] Scaffold a theme file you own\n');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
theme
|
|
922
|
+
.command('add [slug] [path]')
|
|
923
|
+
.description('Scaffold a theme into your project as editable source')
|
|
924
|
+
.option('-f, --overwrite', 'Overwrite existing files without prompting')
|
|
925
|
+
.option('--list', 'List available themes')
|
|
926
|
+
.action(async (slug, targetPath, options) => {
|
|
927
|
+
const json = program.opts().json || false;
|
|
928
|
+
|
|
929
|
+
// Only prompt with a real TTY on stdin — a piped/redirected stdin would
|
|
930
|
+
// make clack hang. Non-interactive callers fall through to the API's
|
|
931
|
+
// ERR_FILE_EXISTS guard.
|
|
932
|
+
const interactive =
|
|
933
|
+
!json && !isNonInteractive({json}) && Boolean(process.stdin.isTTY);
|
|
934
|
+
if (slug && !options.list && !options.overwrite && interactive) {
|
|
935
|
+
const collision = await detectThemeCollision(slug, targetPath);
|
|
936
|
+
if (collision) {
|
|
937
|
+
const rel = path.relative(process.cwd(), collision) || collision;
|
|
938
|
+
const p = await import('@clack/prompts');
|
|
939
|
+
const confirmed = await p.confirm({
|
|
940
|
+
message: `Overwrite existing file ${rel}?`,
|
|
941
|
+
initialValue: false,
|
|
942
|
+
});
|
|
943
|
+
if (p.isCancel(confirmed)) {
|
|
944
|
+
p.cancel('Cancelled.');
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (!confirmed) {
|
|
948
|
+
humanLog('Aborted. Re-run with --overwrite to replace the file.');
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
options.overwrite = true;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
let result;
|
|
956
|
+
try {
|
|
957
|
+
result = await themeAdd(slug, {
|
|
958
|
+
list: options.list,
|
|
959
|
+
targetPath,
|
|
960
|
+
overwrite: options.overwrite,
|
|
961
|
+
cwd: process.cwd(),
|
|
962
|
+
});
|
|
963
|
+
} catch (e) {
|
|
964
|
+
cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (json) return jsonOut(result.type, result.data);
|
|
969
|
+
|
|
970
|
+
if (result.type === 'theme.list') {
|
|
971
|
+
const themes = result.data;
|
|
972
|
+
humanLog('\nThemes:\n');
|
|
973
|
+
for (const t of themes) {
|
|
974
|
+
const tag = t.maintained ? ' (maintained)' : '';
|
|
975
|
+
humanLog(` ${t.slug}${tag}`);
|
|
976
|
+
if (t.description) humanLog(` ${t.description}`);
|
|
977
|
+
}
|
|
978
|
+
humanLog('\nUsage:');
|
|
979
|
+
humanLog(' astryx theme add <slug> [target-path] Scaffold a theme file you own\n');
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// theme.add — print where files landed + how to use the theme.
|
|
984
|
+
const {displayName, outputDir, entry, exportName, files} = result.data;
|
|
985
|
+
humanLog(`\n✓ Added ${displayName} theme to ${outputDir}/`);
|
|
986
|
+
for (const f of files) {
|
|
987
|
+
humanLog(` ${outputDir}/${f}`);
|
|
988
|
+
}
|
|
989
|
+
const entryModule = importSpecifier(
|
|
990
|
+
outputDir,
|
|
991
|
+
entry.replace(/\.tsx?$/, ''),
|
|
992
|
+
);
|
|
993
|
+
humanLog(`
|
|
994
|
+
Use it in your app (import path is relative to a file in src/ — adjust if yours lives elsewhere):
|
|
995
|
+
|
|
996
|
+
import { ${exportName} } from '${entryModule}';
|
|
997
|
+
|
|
998
|
+
<Theme theme={${exportName}}>
|
|
999
|
+
<App />
|
|
1000
|
+
</Theme>
|
|
1001
|
+
|
|
1002
|
+
This is your copy of the ${displayName} theme — edit ${entry} to make it your own.
|
|
1003
|
+
`);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* First existing file that scaffolding <slug> into <targetPath> would clobber,
|
|
1009
|
+
* or null. Used to prompt before invoking the API; the API re-validates and
|
|
1010
|
+
* owns any authoritative error.
|
|
1011
|
+
*
|
|
1012
|
+
* @param {string} slug
|
|
1013
|
+
* @param {string} [targetPath]
|
|
1014
|
+
* @returns {Promise<string|null>}
|
|
1015
|
+
*/
|
|
1016
|
+
async function detectThemeCollision(slug, targetPath) {
|
|
1017
|
+
let themes;
|
|
1018
|
+
try {
|
|
1019
|
+
themes = listThemes();
|
|
1020
|
+
} catch {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
const match = themes.find(t => t.slug.toLowerCase() === slug.toLowerCase());
|
|
1024
|
+
if (!match) return null;
|
|
1025
|
+
|
|
1026
|
+
const rawTarget = targetPath || path.join('src', 'themes', match.slug);
|
|
1027
|
+
let resolvedDir;
|
|
1028
|
+
try {
|
|
1029
|
+
// Fail soft (null) on traversal; the API surfaces the real error.
|
|
1030
|
+
const {assertWithin} = await import('../utils/path-safety.mjs');
|
|
1031
|
+
resolvedDir = assertWithin(rawTarget, process.cwd(), {
|
|
1032
|
+
label: 'theme target path',
|
|
1033
|
+
});
|
|
1034
|
+
} catch {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
for (const name of match.files) {
|
|
1039
|
+
const dest = path.join(resolvedDir, name);
|
|
1040
|
+
if (fs.existsSync(dest)) return dest;
|
|
1041
|
+
}
|
|
1042
|
+
return null;
|
|
874
1043
|
}
|
|
@@ -24,6 +24,8 @@ import {cliError} from '../../lib/cli-error.mjs';
|
|
|
24
24
|
import {ERROR_CODES} from '../../lib/error-codes.mjs';
|
|
25
25
|
import {component as componentApi} from '../../api/component.mjs';
|
|
26
26
|
import {findRelatedBlocks} from '../../api/template.mjs';
|
|
27
|
+
import {Project} from '../../lib/project.mjs';
|
|
28
|
+
import {warnOnIntegrationIssues} from '../../lib/integration-warnings.mjs';
|
|
27
29
|
|
|
28
30
|
export function registerComponent(program) {
|
|
29
31
|
program
|
|
@@ -55,6 +57,16 @@ export function registerComponent(program) {
|
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
// Non-blocking nudge: if any configured integration has validation
|
|
61
|
+
// issues, print one compact line to stderr pointing at
|
|
62
|
+
// validate-integration. Best-effort; suppressed in --json mode.
|
|
63
|
+
try {
|
|
64
|
+
const project = await Project.load(process.cwd());
|
|
65
|
+
await warnOnIntegrationIssues(project.loadedIntegrations, {json});
|
|
66
|
+
} catch {
|
|
67
|
+
// Never let the nudge break the command.
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
let result;
|
|
59
71
|
try {
|
|
60
72
|
result = await componentApi(name, {
|
|
@@ -82,27 +94,49 @@ export function registerComponent(program) {
|
|
|
82
94
|
|
|
83
95
|
switch (result.type) {
|
|
84
96
|
case 'component.list': {
|
|
85
|
-
// --detail brief (default for list views)
|
|
97
|
+
// --detail brief (default for list views). The API now returns
|
|
98
|
+
// package-qualified entries ({name, package}); the human view omits
|
|
99
|
+
// the core package label for readability but ALWAYS shows the package
|
|
100
|
+
// for integration components (and whenever names collide).
|
|
101
|
+
const CORE_PKG = '@astryxdesign/core';
|
|
102
|
+
// Names that appear under more than one package across the whole
|
|
103
|
+
// listing — these must always be package-qualified to disambiguate.
|
|
104
|
+
const nameCounts = new Map();
|
|
105
|
+
for (const items of Object.values(result.data)) {
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
const set = nameCounts.get(item.name) ?? new Set();
|
|
108
|
+
set.add(item.package);
|
|
109
|
+
nameCounts.set(item.name, set);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const isCollision = n => (nameCounts.get(n)?.size ?? 0) > 1;
|
|
113
|
+
const pkgSuffix = item => {
|
|
114
|
+
if (item.package !== CORE_PKG) return ` [${item.package}]`;
|
|
115
|
+
if (isCollision(item.name)) return ` [${item.package}]`;
|
|
116
|
+
return '';
|
|
117
|
+
};
|
|
118
|
+
|
|
86
119
|
if (options.category) {
|
|
87
120
|
const [cat, comps] = Object.entries(result.data)[0];
|
|
88
121
|
humanLog(`\n${cat}:`);
|
|
89
|
-
for (const
|
|
90
|
-
const importPath = resolveImportPath(coreDir,
|
|
91
|
-
humanLog(` ${
|
|
122
|
+
for (const item of comps) {
|
|
123
|
+
const importPath = resolveImportPath(coreDir, item.name);
|
|
124
|
+
humanLog(` ${item.name} ← ${importPath}${pkgSuffix(item)}`);
|
|
92
125
|
}
|
|
93
126
|
humanLog('');
|
|
94
127
|
} else {
|
|
95
128
|
humanLog('');
|
|
96
129
|
for (const [key, comps] of Object.entries(result.data)) {
|
|
97
|
-
const isUngrouped = comps.length === 1 && comps[0] === key;
|
|
130
|
+
const isUngrouped = comps.length === 1 && comps[0]?.name === key;
|
|
98
131
|
if (isUngrouped) {
|
|
99
|
-
const
|
|
100
|
-
|
|
132
|
+
const item = comps[0];
|
|
133
|
+
const importPath = resolveImportPath(coreDir, item.name);
|
|
134
|
+
humanLog(`${item.name} ← ${importPath}${pkgSuffix(item)}`);
|
|
101
135
|
} else {
|
|
102
136
|
humanLog(`${key} (group)`);
|
|
103
|
-
for (const
|
|
104
|
-
const importPath = resolveImportPath(coreDir,
|
|
105
|
-
humanLog(` ${
|
|
137
|
+
for (const item of comps) {
|
|
138
|
+
const importPath = resolveImportPath(coreDir, item.name);
|
|
139
|
+
humanLog(` ${item.name} ← ${importPath}${pkgSuffix(item)}`);
|
|
106
140
|
}
|
|
107
141
|
}
|
|
108
142
|
}
|