@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.db4e378
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/README.md +86 -72
- package/api/build/build.doc.mjs +3 -3
- package/api/build/build.test.mjs +14 -3
- package/api/build/build.type.d.mts +35 -1
- package/api/build/build.type.mjs +24 -3
- package/api/build/help/help.d.mts +8 -5
- package/api/build/help/help.mjs +56 -6
- package/api/component/component.doc.mjs +12 -3
- package/api/component/component.mjs +31 -2
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +16 -5
- package/api/component/component.type.mjs +13 -5
- package/api/component/detail/blocks/blocks.d.mts +2 -1
- package/api/component/detail/blocks/blocks.mjs +4 -3
- package/api/component/list/list.d.mts +0 -5
- package/api/component/list/list.mjs +37 -9
- package/api/discover/discover.doc.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +7 -4
- package/api/init/init.doc.mjs +4 -0
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.mjs +11 -0
- package/api/integration/add-contribution.test.mjs +80 -0
- package/api/integration/integrationAdd.doc.mjs +7 -4
- package/api/integration/integrationAddComponent.doc.mjs +1 -1
- package/api/integration/validate-integration.mjs +2 -0
- package/api/integration/validate-integration.test.mjs +91 -0
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/parseResponse.doc.mjs +2 -2
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +2 -9
- package/api/search/search.doc.mjs +5 -1
- package/api/search/search.mjs +10 -3
- package/api/search/search.test.mjs +34 -0
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +1 -1
- package/api/template/copy/copy.mjs +17 -23
- package/api/template/copy/copy.test.mjs +17 -0
- package/api/template/template.doc.mjs +2 -1
- package/api/theme/add/add.binary.test.mjs +91 -0
- package/api/theme/add/add.mjs +20 -3
- package/api/theme/add/add.staging.test.mjs +66 -0
- package/api/theme/build/build.mjs +114 -37
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/palette/generate/generate.mjs +7 -2
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.mjs +8 -1
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +15 -0
- package/api/theme/themeBuild.doc.mjs +7 -4
- package/api/theme/themeTemplate.doc.mjs +4 -1
- package/api/upgrade/_adapter.mjs +27 -4
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +2 -1
- package/api/upgrade/upgrade.type.d.mts +4 -0
- package/api/upgrade/upgrade.type.mjs +1 -0
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transform-prop.mjs +109 -0
- package/assets/codemods/transform-prop.test.mjs +95 -0
- package/assets/codemods/transforms/v0.0.14/__tests__/rename-status-variants.test.mjs +86 -165
- package/assets/codemods/transforms/v0.0.14/rename-status-variants.mjs +72 -210
- package/assets/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +83 -115
- package/assets/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +57 -186
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaInlineRail.tsx +61 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaOverscrollChaining.tsx +126 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaShowcase.tsx +86 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyGroupHeaders.tsx +99 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaStickyPassthrough.tsx +122 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.doc.mjs +14 -0
- package/assets/templates/blocks/components/ScrollableArea/ScrollableAreaTwoAxisBoard.tsx +95 -0
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/authoring/config/config.doc.mjs +1 -1
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/type.ts +15 -3
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +5 -5
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +4 -1
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +32 -47
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +40 -0
- package/clients/cli/commands/component/index.mjs +2 -7
- package/clients/cli/commands/component-package.test.mjs +46 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +4 -2
- package/clients/cli/commands/discover.mjs +3 -3
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor.mjs +6 -16
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +16 -5
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +19 -8
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +21 -5
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +14 -3
- package/clients/cli/commands/layout-expand.doc.mjs +21 -4
- package/clients/cli/commands/layout.doc.mjs +2 -2
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +6 -3
- package/clients/cli/commands/search.mjs +17 -7
- package/clients/cli/commands/search.test.mjs +75 -0
- package/clients/cli/commands/swizzle.doc.mjs +2 -1
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +28 -6
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/theme-add.doc.mjs +2 -1
- package/clients/cli/commands/theme-build.doc.mjs +7 -6
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +1 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +17 -6
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/index.mjs +13 -30
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +32 -6
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +97 -0
- package/clients/cli/lib/hook-format.mjs +5 -5
- package/clients/cli/lib/json-shim.mjs +38 -2
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +22 -0
- package/clients/cli/lib/manifest.test.mjs +17 -0
- package/foundation/agent-docs/agent-docs.d.mts +4 -0
- package/foundation/agent-docs/agent-docs.mjs +69 -3
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project.mjs +123 -9
- package/foundation/config/project.test.mjs +126 -0
- package/foundation/discovery/template-adapter.d.mts +14 -5
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +292 -25
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/integrations/autolink.mjs +6 -1
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/error-codes.doc.mjs +1 -2
- package/foundation/response/error-codes.mjs +1 -1
- package/foundation/response/error-codes.test.mjs +83 -0
- package/foundation/response/json-contract.test.mjs +11 -0
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.mjs +10 -10
- package/foundation/response/response-types.doc.test.mjs +158 -0
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +3 -2
- package/foundation/xle/expand.test.mjs +54 -0
- package/package.json +9 -9
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
|
@@ -34,6 +34,11 @@ import {componentDetailSource} from './detail/source/source.mjs';
|
|
|
34
34
|
import {componentDetailShowcase} from './detail/showcase/showcase.mjs';
|
|
35
35
|
import {componentDetailBlocks} from './detail/blocks/blocks.mjs';
|
|
36
36
|
|
|
37
|
+
/** @type {ReadonlyArray<string>} */
|
|
38
|
+
const DETAIL_LEVELS = ['full', 'compact', 'brief'];
|
|
39
|
+
/** @type {ReadonlyArray<string>} */
|
|
40
|
+
const LANGS = ['en', 'zh', 'dense'];
|
|
41
|
+
|
|
37
42
|
/**
|
|
38
43
|
* @param {string} [name]
|
|
39
44
|
* @param {object} [options]
|
|
@@ -81,6 +86,23 @@ export async function component(name, options = {}) {
|
|
|
81
86
|
const isListView = list || category != null || !name;
|
|
82
87
|
const detail = detailOption ?? (isListView ? 'brief' : 'full');
|
|
83
88
|
|
|
89
|
+
// Same accepted values and codes as the CLI's --detail and --lang, checked
|
|
90
|
+
// first as the CLI parser does.
|
|
91
|
+
if (!DETAIL_LEVELS.includes(detail)) {
|
|
92
|
+
throw new AstryxError(
|
|
93
|
+
`Invalid detail "${String(detail)}". Valid levels: ${DETAIL_LEVELS.join(', ')}`,
|
|
94
|
+
undefined,
|
|
95
|
+
ERROR_CODES.ERR_INVALID_DETAIL,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (lang != null && !LANGS.includes(lang)) {
|
|
99
|
+
throw new AstryxError(
|
|
100
|
+
`Invalid lang "${String(lang)}". Valid values: ${LANGS.join(', ')}`,
|
|
101
|
+
undefined,
|
|
102
|
+
ERROR_CODES.ERR_INVALID_LANG,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
const coreDir = requireCoreDir(cwd);
|
|
85
107
|
|
|
86
108
|
// A public API caller could pass a non-string category; the list leaf does
|
|
@@ -143,7 +165,7 @@ export async function component(name, options = {}) {
|
|
|
143
165
|
: componentDetailShowcase(dirName, {cwd, name, resolve: false});
|
|
144
166
|
}
|
|
145
167
|
if (blocks) {
|
|
146
|
-
return componentDetailBlocks(dirName);
|
|
168
|
+
return componentDetailBlocks(dirName, cwd);
|
|
147
169
|
}
|
|
148
170
|
const docs = await loadComponentDoc(owner.docPath, docOpts);
|
|
149
171
|
if (props) return componentDetailProps(docs);
|
|
@@ -156,6 +178,13 @@ export async function component(name, options = {}) {
|
|
|
156
178
|
}
|
|
157
179
|
const extDocPath = resolveLegacyExternalDoc(scoped.ext, dirName);
|
|
158
180
|
if (extDocPath) {
|
|
181
|
+
// Legacy packages ship docs, never source.
|
|
182
|
+
if (source) {
|
|
183
|
+
return componentDetailSource(dirName, null, {name, notFoundInPackage: packageScope});
|
|
184
|
+
}
|
|
185
|
+
if (blocks) {
|
|
186
|
+
return componentDetailBlocks(dirName, cwd);
|
|
187
|
+
}
|
|
159
188
|
const docs = await loadComponentDoc(extDocPath, docOpts);
|
|
160
189
|
if (props) return componentDetailProps(docs);
|
|
161
190
|
return componentDetail(docs, {package: scoped.ext.name, sourcePath: null}, dirName, coreDir);
|
|
@@ -195,7 +224,7 @@ export async function component(name, options = {}) {
|
|
|
195
224
|
|
|
196
225
|
// ── Blocks mode ──────────────────────────────────────────────
|
|
197
226
|
if (blocks) {
|
|
198
|
-
return componentDetailBlocks(dirName);
|
|
227
|
+
return componentDetailBlocks(dirName, cwd);
|
|
199
228
|
}
|
|
200
229
|
|
|
201
230
|
// ── Sub-component scoping ────────────────────────────────────
|
|
@@ -14,6 +14,7 @@ import * as path from 'node:path';
|
|
|
14
14
|
import {fileURLToPath} from 'node:url';
|
|
15
15
|
import {component} from './component.mjs';
|
|
16
16
|
import {AstryxError} from '../error.mjs';
|
|
17
|
+
import {runCli} from '../../test-utils/run-cli.mjs';
|
|
17
18
|
|
|
18
19
|
// api/component/ -> up 4 = repo root (has packages/core, which findCoreDir walks to).
|
|
19
20
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
@@ -161,3 +162,40 @@ describe('component dispatcher — category guard', () => {
|
|
|
161
162
|
}
|
|
162
163
|
}, SLOW);
|
|
163
164
|
});
|
|
165
|
+
|
|
166
|
+
describe('component dispatcher — detail and lang guards', () => {
|
|
167
|
+
// `detail` and `lang` are the same controls as the CLI's --detail and --lang:
|
|
168
|
+
// same accepted values, same error codes.
|
|
169
|
+
it('rejects a detail level the CLI rejects, with the same code', async () => {
|
|
170
|
+
const cliRun = await runCli(['--json', 'component', '--list', '--detail', 'bogus'], cwd);
|
|
171
|
+
expect(cliRun.code).toBe(1);
|
|
172
|
+
expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_DETAIL');
|
|
173
|
+
|
|
174
|
+
const bogus = /** @type {any} */ ('bogus');
|
|
175
|
+
for (const call of [
|
|
176
|
+
() => component(undefined, {cwd, list: true, detail: bogus}),
|
|
177
|
+
() => component('Button', {cwd, detail: bogus}),
|
|
178
|
+
]) {
|
|
179
|
+
const err = await call().catch(e => e);
|
|
180
|
+
expect(err).toBeInstanceOf(AstryxError);
|
|
181
|
+
expect(err.code).toBe('ERR_INVALID_DETAIL');
|
|
182
|
+
}
|
|
183
|
+
}, SLOW);
|
|
184
|
+
|
|
185
|
+
it('rejects a lang the CLI rejects, with the same code', async () => {
|
|
186
|
+
const cliRun = await runCli(['--json', 'component', 'Button', '--lang', 'fr'], cwd);
|
|
187
|
+
expect(cliRun.code).toBe(1);
|
|
188
|
+
expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_LANG');
|
|
189
|
+
|
|
190
|
+
for (const call of [
|
|
191
|
+
() => component('Button', {cwd, lang: 'fr'}),
|
|
192
|
+
() => component(undefined, {cwd, list: true, detail: 'compact', lang: 'fr'}),
|
|
193
|
+
]) {
|
|
194
|
+
const err = await call().catch(e => e);
|
|
195
|
+
expect(err).toBeInstanceOf(AstryxError);
|
|
196
|
+
expect(err.code).toBe('ERR_INVALID_LANG');
|
|
197
|
+
}
|
|
198
|
+
const zh = await component('Button', {cwd, lang: 'zh'});
|
|
199
|
+
expect(zh.type).toBe('component.detail');
|
|
200
|
+
}, SLOW);
|
|
201
|
+
});
|
|
@@ -31,9 +31,10 @@ export type ComponentListData = ({
|
|
|
31
31
|
/**
|
|
32
32
|
* A single entry in a `component.list` group at `detail: 'names'`. Pre-1.0 the
|
|
33
33
|
* list moved from bare strings to package-qualified objects so consumers can
|
|
34
|
-
* disambiguate ownership (core vs. an integration package). Integration
|
|
35
|
-
* carry `import` — the
|
|
36
|
-
*
|
|
34
|
+
* disambiguate ownership (core vs. an integration package). Integration and
|
|
35
|
+
* legacy `astryx.docs` package entries carry `import` — the same specifier their
|
|
36
|
+
* `component.detail` reports; core entries omit it (the specifier is derived
|
|
37
|
+
* from the component name by the renderer).
|
|
37
38
|
*/
|
|
38
39
|
export type ComponentListEntry = {
|
|
39
40
|
name: string;
|
|
@@ -42,7 +43,7 @@ export type ComponentListEntry = {
|
|
|
42
43
|
*/
|
|
43
44
|
package: string;
|
|
44
45
|
/**
|
|
45
|
-
* - Import specifier; present for integration components, absent for core.
|
|
46
|
+
* - Import specifier; present for integration and legacy package components, absent for core.
|
|
46
47
|
*/
|
|
47
48
|
import?: string | undefined;
|
|
48
49
|
};
|
|
@@ -59,7 +60,17 @@ export type ComponentBriefEntry = {
|
|
|
59
60
|
*/
|
|
60
61
|
export type ComponentDetailResponse = {
|
|
61
62
|
type: "component.detail";
|
|
62
|
-
data: import("@astryxdesign/cli/authoring").ComponentDoc & ComponentOwnership;
|
|
63
|
+
data: import("@astryxdesign/cli/authoring").ComponentDoc & ComponentOwnership & ComponentDetailScope;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Present only when the requested name is a sub-component documented inside a
|
|
67
|
+
* parent's doc (e.g. `HStack` in the `Stack` doc); the payload is scoped to it.
|
|
68
|
+
*/
|
|
69
|
+
export type ComponentDetailScope = {
|
|
70
|
+
/**
|
|
71
|
+
* - Name of the parent doc the payload was scoped from, e.g. 'Stack'.
|
|
72
|
+
*/
|
|
73
|
+
parentDoc?: string | undefined;
|
|
63
74
|
};
|
|
64
75
|
/**
|
|
65
76
|
* Ownership metadata attached to every `component.detail` payload. Exposes the
|
|
@@ -52,13 +52,14 @@
|
|
|
52
52
|
/**
|
|
53
53
|
* A single entry in a `component.list` group at `detail: 'names'`. Pre-1.0 the
|
|
54
54
|
* list moved from bare strings to package-qualified objects so consumers can
|
|
55
|
-
* disambiguate ownership (core vs. an integration package). Integration
|
|
56
|
-
* carry `import` — the
|
|
57
|
-
*
|
|
55
|
+
* disambiguate ownership (core vs. an integration package). Integration and
|
|
56
|
+
* legacy `astryx.docs` package entries carry `import` — the same specifier their
|
|
57
|
+
* `component.detail` reports; core entries omit it (the specifier is derived
|
|
58
|
+
* from the component name by the renderer).
|
|
58
59
|
* @typedef {object} ComponentListEntry
|
|
59
60
|
* @property {string} name
|
|
60
61
|
* @property {string} package - Owner package, e.g. '@astryxdesign/core' or '@acme/astryx-meta'.
|
|
61
|
-
* @property {string} [import] - Import specifier; present for integration components, absent for core.
|
|
62
|
+
* @property {string} [import] - Import specifier; present for integration and legacy package components, absent for core.
|
|
62
63
|
*/
|
|
63
64
|
|
|
64
65
|
/**
|
|
@@ -73,7 +74,14 @@
|
|
|
73
74
|
* astryx --json component <name>
|
|
74
75
|
* @typedef {object} ComponentDetailResponse
|
|
75
76
|
* @property {'component.detail'} type
|
|
76
|
-
* @property {import('@astryxdesign/cli/authoring').ComponentDoc & ComponentOwnership} data
|
|
77
|
+
* @property {import('@astryxdesign/cli/authoring').ComponentDoc & ComponentOwnership & ComponentDetailScope} data
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Present only when the requested name is a sub-component documented inside a
|
|
82
|
+
* parent's doc (e.g. `HStack` in the `Stack` doc); the payload is scoped to it.
|
|
83
|
+
* @typedef {object} ComponentDetailScope
|
|
84
|
+
* @property {string} [parentDoc] - Name of the parent doc the payload was scoped from, e.g. 'Stack'.
|
|
77
85
|
*/
|
|
78
86
|
|
|
79
87
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* envelope, splitting them into the hero showcase, component-specific examples,
|
|
7
7
|
* and broader related blocks.
|
|
8
8
|
* @param {string} componentName
|
|
9
|
+
* @param {string} cwd - project to discover blocks from; never the process cwd
|
|
9
10
|
* @returns {Promise<import('../../component.type.mjs').ComponentDetailBlocksResponse>}
|
|
10
11
|
*/
|
|
11
|
-
export function componentDetailBlocks(componentName: string): Promise<import("../../component.type.mjs").ComponentDetailBlocksResponse>;
|
|
12
|
+
export function componentDetailBlocks(componentName: string, cwd: string): Promise<import("../../component.type.mjs").ComponentDetailBlocksResponse>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file `component.detail.blocks` leaf — a component's example/related blocks.
|
|
5
5
|
*
|
|
6
|
-
* @input a component name
|
|
6
|
+
* @input a component name + the project cwd
|
|
7
7
|
* @output the `component.detail.blocks` envelope (showcase, examples, related)
|
|
8
8
|
* @position api/component/detail/blocks (projection leaf; routed by component.mjs)
|
|
9
9
|
*/
|
|
@@ -16,10 +16,11 @@ import {findRelatedBlocks} from '../../../template/template.mjs';
|
|
|
16
16
|
* envelope, splitting them into the hero showcase, component-specific examples,
|
|
17
17
|
* and broader related blocks.
|
|
18
18
|
* @param {string} componentName
|
|
19
|
+
* @param {string} cwd - project to discover blocks from; never the process cwd
|
|
19
20
|
* @returns {Promise<import('../../component.type.mjs').ComponentDetailBlocksResponse>}
|
|
20
21
|
*/
|
|
21
|
-
export async function componentDetailBlocks(componentName) {
|
|
22
|
-
const allBlocks = await findRelatedBlocks(componentName);
|
|
22
|
+
export async function componentDetailBlocks(componentName, cwd) {
|
|
23
|
+
const allBlocks = await findRelatedBlocks(componentName, cwd);
|
|
23
24
|
const toEntry = (/** @type {any} */ b) => ({
|
|
24
25
|
name: b.dirName,
|
|
25
26
|
displayName: b.name,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
|
|
2
2
|
// DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* @typedef {import('../component.type.mjs').ComponentListResponse} ComponentListResponse
|
|
6
|
-
* @typedef {import('../component.type.mjs').ComponentListEntry} ComponentListEntry
|
|
7
|
-
* @typedef {import('../component.type.mjs').ComponentBriefEntry} ComponentBriefEntry
|
|
8
|
-
*/
|
|
9
4
|
/**
|
|
10
5
|
* Build the `component.list` envelope. The list taxonomy is collapsed: all
|
|
11
6
|
* three detail levels emit ONE `component.list` type; the depth rides in
|
|
@@ -17,13 +17,14 @@ import {
|
|
|
17
17
|
discoverExternalComponentsGrouped,
|
|
18
18
|
discoverIntegrationComponents,
|
|
19
19
|
findComponentReadme,
|
|
20
|
+
findExternalComponentDoc,
|
|
20
21
|
resolveImportPath,
|
|
21
22
|
resolveIntegrationImportPath,
|
|
22
23
|
} from '../../../foundation/discovery/component-discovery.mjs';
|
|
23
24
|
import {discoverExternalPackages} from '../../../foundation/fs/paths.mjs';
|
|
24
25
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
25
26
|
import {AstryxError} from '../../error.mjs';
|
|
26
|
-
import {loadComponentDoc, loadIntegrationsSafely} from '../_adapter.mjs';
|
|
27
|
+
import {loadComponentDoc, loadIntegrationsSafely, withOwnership} from '../_adapter.mjs';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* @typedef {import('../component.type.mjs').ComponentListResponse} ComponentListResponse
|
|
@@ -31,6 +32,29 @@ import {loadComponentDoc, loadIntegrationsSafely} from '../_adapter.mjs';
|
|
|
31
32
|
* @typedef {import('../component.type.mjs').ComponentBriefEntry} ComponentBriefEntry
|
|
32
33
|
*/
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The import a legacy `pkg.astryx.docs` component's detail reports, derived the
|
|
37
|
+
* same way (`withOwnership`) so list and detail agree.
|
|
38
|
+
* @param {{name: string, docsDir: string}} ext
|
|
39
|
+
* @param {string} name
|
|
40
|
+
* @param {string} coreDir
|
|
41
|
+
* @param {{zh: boolean, lang: string|null}} docOpts
|
|
42
|
+
* @returns {Promise<string>}
|
|
43
|
+
*/
|
|
44
|
+
async function legacyImport(ext, name, coreDir, docOpts) {
|
|
45
|
+
const docPath = findExternalComponentDoc(ext.docsDir, name);
|
|
46
|
+
/** @type {import('../_adapter.mjs').LoadedComponentDoc} */
|
|
47
|
+
let docs = {};
|
|
48
|
+
if (docPath && docPath.endsWith('.doc.mjs')) {
|
|
49
|
+
try {
|
|
50
|
+
docs = await loadComponentDoc(docPath, docOpts);
|
|
51
|
+
} catch {
|
|
52
|
+
// Keep list resilient; validation owns malformed docs.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return withOwnership(docs, {package: ext.name, sourcePath: null}, name, coreDir).import;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
/**
|
|
35
59
|
* Build the `component.list` envelope. The list taxonomy is collapsed: all
|
|
36
60
|
* three detail levels emit ONE `component.list` type; the depth rides in
|
|
@@ -267,20 +291,24 @@ export async function componentList(
|
|
|
267
291
|
k => grouped[k].length > 1 || grouped[k][0] !== k,
|
|
268
292
|
);
|
|
269
293
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
294
|
+
/** @param {string[]} names */
|
|
295
|
+
const entriesFor = names =>
|
|
296
|
+
Promise.all(
|
|
297
|
+
names.map(async n => ({
|
|
273
298
|
name: n,
|
|
274
299
|
package: ext.name,
|
|
275
|
-
|
|
300
|
+
import: await legacyImport(ext, n, coreDir, {zh, lang}),
|
|
301
|
+
})),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (hasGroups) {
|
|
305
|
+
for (const [group, members] of Object.entries(grouped)) {
|
|
306
|
+
listData[`${group} (${ext.name})`] = await entriesFor(members);
|
|
276
307
|
}
|
|
277
308
|
} else {
|
|
278
309
|
const allComps = Object.values(grouped).flat().sort();
|
|
279
310
|
if (allComps.length > 0) {
|
|
280
|
-
listData[`${ext.category} (${ext.name})`] =
|
|
281
|
-
name: n,
|
|
282
|
-
package: ext.name,
|
|
283
|
-
}));
|
|
311
|
+
listData[`${ext.category} (${ext.name})`] = await entriesFor(allComps);
|
|
284
312
|
}
|
|
285
313
|
}
|
|
286
314
|
}
|
|
@@ -41,7 +41,7 @@ export const doc = {
|
|
|
41
41
|
name: 'options.components',
|
|
42
42
|
type: 'boolean',
|
|
43
43
|
description:
|
|
44
|
-
'
|
|
44
|
+
'In the CLI package list, print every component of each package instead of the first 10. A display flag for the CLI renderer; the programmatic response is unchanged.',
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
name: 'options.lang',
|
|
@@ -32,17 +32,19 @@ export const doc = {
|
|
|
32
32
|
name: 'component',
|
|
33
33
|
type: 'string',
|
|
34
34
|
description:
|
|
35
|
-
'Component or general design-system area. Required unless listCategories is true.',
|
|
35
|
+
'Component or general design-system area, up to 120 characters. Required unless listCategories is true.',
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: 'options.category',
|
|
39
39
|
type: 'GapReportCategory',
|
|
40
|
-
description:
|
|
40
|
+
description:
|
|
41
|
+
'Fixed category from the reported category vocabulary. Required unless listCategories is true.',
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
44
|
name: 'options.reason',
|
|
44
45
|
type: 'string',
|
|
45
|
-
description:
|
|
46
|
+
description:
|
|
47
|
+
'What capability was missing or difficult, up to 2000 characters. Required unless listCategories is true.',
|
|
46
48
|
},
|
|
47
49
|
{
|
|
48
50
|
name: 'options.detail',
|
|
@@ -65,7 +67,8 @@ export const doc = {
|
|
|
65
67
|
{
|
|
66
68
|
name: 'options.listCategories',
|
|
67
69
|
type: 'boolean',
|
|
68
|
-
description:
|
|
70
|
+
description:
|
|
71
|
+
'Return categories without resolving a route or writing; the component and every other option are ignored.',
|
|
69
72
|
default: 'false',
|
|
70
73
|
},
|
|
71
74
|
{
|
package/api/init/init.doc.mjs
CHANGED
|
@@ -90,6 +90,10 @@ export const doc = {
|
|
|
90
90
|
code: 'ERR_FILE_EXISTS',
|
|
91
91
|
when: 'scaffolding a template would overwrite an existing page.tsx',
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
code: 'ERR_PATH_TRAVERSAL',
|
|
95
|
+
when: 'the template output path resolves outside cwd, for example through a symlinked src directory',
|
|
96
|
+
},
|
|
93
97
|
],
|
|
94
98
|
examples: [
|
|
95
99
|
{label: 'Default setup', code: 'const r = await init();'},
|
package/api/init/init.test.mjs
CHANGED
|
@@ -230,13 +230,40 @@ describe('init() — logger', () => {
|
|
|
230
230
|
errSpy.mockRestore();
|
|
231
231
|
}
|
|
232
232
|
const text = lines.join('\n');
|
|
233
|
-
expect(text).toContain('
|
|
233
|
+
expect(text).toContain('[ok] AI agent docs installed -> AGENTS.md');
|
|
234
234
|
expect(text).toContain(' Next steps:');
|
|
235
235
|
// The exact next-steps block the CLI prints comes from getNextSteps().
|
|
236
236
|
expect(text).toContain(getNextSteps('npx astryx')[2].slice(0, 20));
|
|
237
237
|
});
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
describe('init() — ASCII output', () => {
|
|
241
|
+
it('prints only ASCII on the default, all-features, re-run, template, and remove paths', async () => {
|
|
242
|
+
/** @type {string[]} */
|
|
243
|
+
const lines = [];
|
|
244
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation((...a) => lines.push(a.join(' ')));
|
|
245
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation((...a) => lines.push(a.join(' ')));
|
|
246
|
+
const empty = fs.mkdtempSync(path.join(process.cwd(), '.astryx-init-remove-'));
|
|
247
|
+
logger.setSilent(false);
|
|
248
|
+
try {
|
|
249
|
+
await init({}, {cwd: tmpDir});
|
|
250
|
+
await init({all: true}, {cwd: tmpDir});
|
|
251
|
+
await init({all: true}, {cwd: tmpDir});
|
|
252
|
+
await init({features: 'template', templateName: 'blank'}, {cwd: tmpDir});
|
|
253
|
+
await init({removeAgents: true}, {cwd: tmpDir});
|
|
254
|
+
await init({removeAgents: true}, {cwd: empty});
|
|
255
|
+
} finally {
|
|
256
|
+
logger.setSilent(true);
|
|
257
|
+
logSpy.mockRestore();
|
|
258
|
+
errSpy.mockRestore();
|
|
259
|
+
fs.rmSync(empty, {recursive: true, force: true});
|
|
260
|
+
}
|
|
261
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
262
|
+
expect(lines.some(line => line.startsWith('[ok] Removed'))).toBe(true);
|
|
263
|
+
expect(lines.filter(line => /[\u0080-\uFFFF]/.test(line))).toEqual([]);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
240
267
|
describe('init() — write-path safety', () => {
|
|
241
268
|
it('template scaffold refuses to clobber an existing page.tsx', async () => {
|
|
242
269
|
const dest = path.join(tmpDir, 'src', 'pages', 'blank');
|
|
@@ -260,4 +287,17 @@ describe('init() — write-path safety', () => {
|
|
|
260
287
|
init({features: 'template', templateName: '../../etc/evil'}, {cwd: tmpDir}),
|
|
261
288
|
).rejects.toMatchObject({code: ERROR_CODES.ERR_UNKNOWN_TEMPLATE});
|
|
262
289
|
});
|
|
290
|
+
|
|
291
|
+
it('refuses a template write that a symlinked src would carry outside cwd (ERR_PATH_TRAVERSAL)', async () => {
|
|
292
|
+
const outside = fs.mkdtempSync(path.join(process.cwd(), '.astryx-init-outside-'));
|
|
293
|
+
try {
|
|
294
|
+
fs.symlinkSync(outside, path.join(tmpDir, 'src'), 'dir');
|
|
295
|
+
await expect(
|
|
296
|
+
init({features: 'template', templateName: 'blank'}, {cwd: tmpDir}),
|
|
297
|
+
).rejects.toMatchObject({code: ERROR_CODES.ERR_PATH_TRAVERSAL});
|
|
298
|
+
expect(fs.readdirSync(outside)).toEqual([]);
|
|
299
|
+
} finally {
|
|
300
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
263
303
|
});
|
|
@@ -24,6 +24,6 @@ import {logger} from '../../logger.mjs';
|
|
|
24
24
|
*/
|
|
25
25
|
export async function remove({cwd = process.cwd()} = {}) {
|
|
26
26
|
removeAgentDocs(cwd);
|
|
27
|
-
logger.log('
|
|
27
|
+
logger.log('[ok] AI agent docs removed.');
|
|
28
28
|
return {type: 'init.remove', data: {removed: true}};
|
|
29
29
|
}
|
package/api/init/run/run.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import * as fs from 'node:fs';
|
|
19
19
|
import {CLI_ROOT} from '../../../foundation/fs/paths.mjs';
|
|
20
|
-
import {PathSafetyError} from '../../../foundation/fs/path-safety.mjs';
|
|
20
|
+
import {PathSafetyError, assertWithin} from '../../../foundation/fs/path-safety.mjs';
|
|
21
21
|
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
22
22
|
import {
|
|
23
23
|
installAgentDocs,
|
|
@@ -98,7 +98,7 @@ async function applyAgents(cwd, options, invocation, data) {
|
|
|
98
98
|
renderedBlock,
|
|
99
99
|
});
|
|
100
100
|
data.docsWritten = written;
|
|
101
|
-
logger.log(
|
|
101
|
+
logger.log(`[ok] AI agent docs installed -> ${written.join(', ')}`);
|
|
102
102
|
} catch (err) {
|
|
103
103
|
// PathSafetyError carries a precise, user-actionable message — surface it
|
|
104
104
|
// (and flag exit 1) instead of the generic "could not install" warning so
|
|
@@ -132,8 +132,8 @@ function applyTheme(cwd, invocation, data) {
|
|
|
132
132
|
data.themeTemplatePath = didWrite ? written : null;
|
|
133
133
|
logger.log(
|
|
134
134
|
didWrite
|
|
135
|
-
?
|
|
136
|
-
:
|
|
135
|
+
? `[ok] Theme template written -> ${written}`
|
|
136
|
+
: `- ${written} already exists - left as is.`,
|
|
137
137
|
);
|
|
138
138
|
} catch {
|
|
139
139
|
// Soft failure, like agent docs: the guidance below is still useful.
|
|
@@ -166,16 +166,16 @@ function applyTemplate(cwd, {templateName}, invocation, data) {
|
|
|
166
166
|
// Point agents at the build workflow rather than dumping page-template
|
|
167
167
|
// names — `build` surfaces pages AND blocks AND components for an idea,
|
|
168
168
|
// and `build` with no args is the full how-to-build playbook.
|
|
169
|
-
logger.log('
|
|
169
|
+
logger.log('[ok] To build UI, use these commands:');
|
|
170
170
|
logger.log('');
|
|
171
171
|
logger.log(
|
|
172
|
-
` ${invocation} build "<what you're building>" build a page
|
|
172
|
+
` ${invocation} build "<what you're building>" build a page - kit: closest template + blocks + components`,
|
|
173
173
|
);
|
|
174
174
|
logger.log(
|
|
175
175
|
` ${invocation} build the how-to-build workflow (read this first)`,
|
|
176
176
|
);
|
|
177
177
|
logger.log(
|
|
178
|
-
` ${invocation} search <query> find anything
|
|
178
|
+
` ${invocation} search <query> find anything - components, docs, templates, blocks`,
|
|
179
179
|
);
|
|
180
180
|
logger.log('');
|
|
181
181
|
data.template = 'workflow';
|
|
@@ -190,9 +190,19 @@ function applyTemplate(cwd, {templateName}, invocation, data) {
|
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
let destFile;
|
|
194
|
+
try {
|
|
195
|
+
destFile = assertWithin(path.join('src', 'pages', templateName, 'page.tsx'), cwd, {
|
|
196
|
+
label: 'template output path',
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err instanceof PathSafetyError) {
|
|
200
|
+
throw new AstryxError(err.message, undefined, ERROR_CODES.ERR_PATH_TRAVERSAL);
|
|
201
|
+
}
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
const outputDir = path.dirname(destFile);
|
|
194
205
|
const srcPath = path.join(CLI_ROOT, 'assets', 'templates', 'pages', templateName, 'page.tsx');
|
|
195
|
-
const destFile = path.join(outputDir, 'page.tsx');
|
|
196
206
|
// Don't clobber a user's existing page — same guard the peer template/copy and
|
|
197
207
|
// theme/add write-leaves apply (init is a public API surface too).
|
|
198
208
|
if (fs.existsSync(destFile)) {
|
|
@@ -206,7 +216,7 @@ function applyTemplate(cwd, {templateName}, invocation, data) {
|
|
|
206
216
|
fs.mkdirSync(outputDir, {recursive: true});
|
|
207
217
|
fs.copyFileSync(srcPath, destFile);
|
|
208
218
|
const rel = path.relative(cwd, outputDir);
|
|
209
|
-
logger.log(
|
|
219
|
+
logger.log(`[ok] Template created at ${rel}/page.tsx`);
|
|
210
220
|
data.template = 'created';
|
|
211
221
|
data.templatePath = rel;
|
|
212
222
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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 {integrationAdd, integrationAddComponent} from './add-contribution.mjs';
|
|
7
|
+
import {discoverIntegrationComponents} from '../../foundation/discovery/component-discovery.mjs';
|
|
8
|
+
|
|
9
|
+
let tmpDir;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-add-component-'));
|
|
13
|
+
fs.writeFileSync(
|
|
14
|
+
path.join(tmpDir, 'package.json'),
|
|
15
|
+
`${JSON.stringify({name: '@acme/widgets', version: '1.0.0', files: ['dist']}, null, 2)}\n`,
|
|
16
|
+
);
|
|
17
|
+
fs.writeFileSync(
|
|
18
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
19
|
+
"export default {\n components: './components',\n};\n",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** @param {string} relativeDoc */
|
|
28
|
+
function writeAuthoredComponent(relativeDoc) {
|
|
29
|
+
const doc = path.join(tmpDir, relativeDoc);
|
|
30
|
+
fs.mkdirSync(path.dirname(doc), {recursive: true});
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
doc,
|
|
33
|
+
"export default {\n type: 'component',\n name: 'AcmeWidget',\n description: 'The authored AcmeWidget.',\n props: [],\n};\n",
|
|
34
|
+
);
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
doc.replace(/\.doc\.(?:ts|mjs|js)$/u, '.tsx'),
|
|
37
|
+
'export function AcmeWidget() {\n return <div>Authored</div>;\n}\n',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Every file under the package, with its bytes. */
|
|
42
|
+
function snapshot() {
|
|
43
|
+
/** @type {Record<string, string>} */
|
|
44
|
+
const files = {};
|
|
45
|
+
/** @param {string} dir */
|
|
46
|
+
const walk = dir => {
|
|
47
|
+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
48
|
+
const full = path.join(dir, entry.name);
|
|
49
|
+
if (entry.isDirectory()) walk(full);
|
|
50
|
+
else files[path.relative(tmpDir, full)] = fs.readFileSync(full, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
walk(tmpDir);
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('integrationAddComponent name collisions', () => {
|
|
58
|
+
it('refuses a name a nested component doc already defines', async () => {
|
|
59
|
+
writeAuthoredComponent('components/AcmeWidget/AcmeWidget.doc.mjs');
|
|
60
|
+
const before = snapshot();
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
integrationAddComponent('AcmeWidget', {cwd: tmpDir}),
|
|
64
|
+
).rejects.toMatchObject({
|
|
65
|
+
code: 'ERR_FILE_EXISTS',
|
|
66
|
+
message: expect.stringContaining(
|
|
67
|
+
'components/AcmeWidget/AcmeWidget.doc.mjs',
|
|
68
|
+
),
|
|
69
|
+
});
|
|
70
|
+
expect(snapshot()).toEqual(before);
|
|
71
|
+
expect(
|
|
72
|
+
discoverIntegrationComponents({
|
|
73
|
+
name: '@acme/widgets',
|
|
74
|
+
components: path.join(tmpDir, 'components'),
|
|
75
|
+
}).map(component => path.relative(tmpDir, component.docPath)),
|
|
76
|
+
).toEqual([path.join('components', 'AcmeWidget', 'AcmeWidget.doc.mjs')]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('refuses the same collision under --dry-run', async () => {
|
|
80
|
+
writeAuthoredComponent('components/widgets/AcmeWidget.doc.mjs');
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
integrationAdd('component', 'AcmeWidget', {cwd: tmpDir, dryRun: true}),
|
|
84
|
+
).rejects.toMatchObject({code: 'ERR_FILE_EXISTS'});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('refuses a same-stem doc with another extension in the root', async () => {
|
|
88
|
+
fs.mkdirSync(path.join(tmpDir, 'components'));
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(tmpDir, 'components', 'AcmeWidget.doc.ts'),
|
|
91
|
+
"export default {type: 'component', name: 'AcmeWidget', props: []};\n",
|
|
92
|
+
);
|
|
93
|
+
const before = snapshot();
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
integrationAddComponent('AcmeWidget', {cwd: tmpDir}),
|
|
97
|
+
).rejects.toMatchObject({code: 'ERR_FILE_EXISTS'});
|
|
98
|
+
expect(snapshot()).toEqual(before);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('still adds a component whose name is free', async () => {
|
|
102
|
+
writeAuthoredComponent('components/AcmeWidget/AcmeWidget.doc.mjs');
|
|
103
|
+
|
|
104
|
+
const result = await integrationAddComponent('AcmeBadge', {cwd: tmpDir});
|
|
105
|
+
|
|
106
|
+
expect(result.data.files).toEqual([
|
|
107
|
+
'components/AcmeBadge.doc.mjs',
|
|
108
|
+
'components/AcmeBadge.tsx',
|
|
109
|
+
'package.json',
|
|
110
|
+
]);
|
|
111
|
+
expect(
|
|
112
|
+
discoverIntegrationComponents({
|
|
113
|
+
name: '@acme/widgets',
|
|
114
|
+
components: path.join(tmpDir, 'components'),
|
|
115
|
+
})
|
|
116
|
+
.map(component => component.name)
|
|
117
|
+
.sort(),
|
|
118
|
+
).toEqual(['AcmeBadge', 'AcmeWidget']);
|
|
119
|
+
});
|
|
120
|
+
});
|