@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.dea6813
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 +32 -18
- 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 +11 -2
- package/api/component/component.mjs +29 -0
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +11 -1
- package/api/component/component.type.mjs +8 -1
- 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/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/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 +18 -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 +3 -3
- 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
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
* - Theme name with `..` segments cannot escape the output directory.
|
|
8
8
|
* - Theme name with `/` is rejected with a clear error (vs. ENOENT).
|
|
9
9
|
* - Multi-file write is atomic: if one file fails, none are left behind.
|
|
10
|
+
* - An `--out` escape, or an output directory that cannot be created, fails
|
|
11
|
+
* with a registered error code.
|
|
10
12
|
*
|
|
11
13
|
* Runs the CLI in-process (shared runCli harness) against a tiny synthetic theme file.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
16
|
+
import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
|
|
15
17
|
import * as fs from 'node:fs';
|
|
16
18
|
import * as path from 'node:path';
|
|
17
19
|
import * as os from 'node:os';
|
|
20
|
+
import {ensureCoreBuilt} from './ensure-core-built.mjs';
|
|
18
21
|
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
22
|
+
import {themeBuild} from '../../../api/theme/build/build.mjs';
|
|
23
|
+
import {AstryxError} from '../../../api/error.mjs';
|
|
24
|
+
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
19
25
|
|
|
20
26
|
function writeTheme(dir, name) {
|
|
21
27
|
fs.mkdirSync(dir, {recursive: true});
|
|
@@ -93,3 +99,82 @@ describe('theme build path safety', () => {
|
|
|
93
99
|
expect(entries).toEqual(['theme.mjs']);
|
|
94
100
|
});
|
|
95
101
|
});
|
|
102
|
+
|
|
103
|
+
describe('theme build output-path failures carry registered codes', () => {
|
|
104
|
+
// These reach the output-path step, which runs after CSS generation.
|
|
105
|
+
beforeAll(() => {
|
|
106
|
+
ensureCoreBuilt();
|
|
107
|
+
}, 200_000);
|
|
108
|
+
|
|
109
|
+
const registered = new Set(Object.values(ERROR_CODES));
|
|
110
|
+
|
|
111
|
+
/** @param {string[]} args @param {string} cwd */
|
|
112
|
+
async function buildJson(args, cwd) {
|
|
113
|
+
const result = await runCli(['--json', 'theme', 'build', ...args], cwd);
|
|
114
|
+
return {result, envelope: JSON.parse(result.stdout)};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
it('reports an --out path outside the project as ERR_PATH_TRAVERSAL', async () => {
|
|
118
|
+
const project = path.join(tmpDir, 'project');
|
|
119
|
+
const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
|
|
120
|
+
|
|
121
|
+
const {result, envelope} = await buildJson(
|
|
122
|
+
[path.relative(project, themeFile), '--out', '../escaped.css'],
|
|
123
|
+
project,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(result.code).toBe(1);
|
|
127
|
+
expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
|
|
128
|
+
expect(registered.has(envelope.code)).toBe(true);
|
|
129
|
+
expect(fs.existsSync(path.join(tmpDir, 'escaped.css'))).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('reports an --out path that escapes through a symlink as ERR_PATH_TRAVERSAL', async () => {
|
|
133
|
+
const project = path.join(tmpDir, 'project');
|
|
134
|
+
const outside = path.join(tmpDir, 'outside');
|
|
135
|
+
fs.mkdirSync(outside, {recursive: true});
|
|
136
|
+
const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
|
|
137
|
+
fs.symlinkSync(outside, path.join(project, 'link'), 'dir');
|
|
138
|
+
|
|
139
|
+
const {result, envelope} = await buildJson(
|
|
140
|
+
[path.relative(project, themeFile), '--out', 'link/escaped.css'],
|
|
141
|
+
project,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(result.code).toBe(1);
|
|
145
|
+
expect(envelope.code).toBe(ERROR_CODES.ERR_PATH_TRAVERSAL);
|
|
146
|
+
expect(fs.readdirSync(outside)).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('reports an output directory that cannot be created as ERR_WRITE_FAILED', async () => {
|
|
150
|
+
const project = path.join(tmpDir, 'project');
|
|
151
|
+
const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
|
|
152
|
+
// A regular file where the output directory would have to be created.
|
|
153
|
+
fs.writeFileSync(path.join(project, 'blocker'), '');
|
|
154
|
+
|
|
155
|
+
const {result, envelope} = await buildJson(
|
|
156
|
+
[path.relative(project, themeFile), '--out', 'blocker/ocean.css'],
|
|
157
|
+
project,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(result.code).toBe(1);
|
|
161
|
+
expect(envelope.code).toBe(ERROR_CODES.ERR_WRITE_FAILED);
|
|
162
|
+
expect(registered.has(envelope.code)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('themeBuild() rejects an escaping out path with an AstryxError', async () => {
|
|
166
|
+
const project = path.join(tmpDir, 'project');
|
|
167
|
+
const themeFile = writeTheme(path.join(project, 'themes'), 'ocean');
|
|
168
|
+
|
|
169
|
+
const build = themeBuild(
|
|
170
|
+
path.relative(project, themeFile),
|
|
171
|
+
{out: '../escaped.css'},
|
|
172
|
+
{cwd: project},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await expect(build).rejects.toBeInstanceOf(AstryxError);
|
|
176
|
+
await expect(build).rejects.toMatchObject({
|
|
177
|
+
code: ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -324,6 +324,82 @@ describe('theme build custom-variant augmentations', () => {
|
|
|
324
324
|
);
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
+
it('rejects a custom Heading type whose only declaration is blank', async () => {
|
|
328
|
+
const themeFile = writeTheme(
|
|
329
|
+
tmpDir,
|
|
330
|
+
`export default {
|
|
331
|
+
name: 'variants-theme',
|
|
332
|
+
tokens: { '--color-bg': '#fff' },
|
|
333
|
+
components: { heading: { 'type:hero': { fontSize: '' } } },
|
|
334
|
+
};\n`,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const result = await runCli(
|
|
338
|
+
['theme', 'build', path.relative(tmpDir, themeFile)],
|
|
339
|
+
tmpDir,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(result.code).toBe(1);
|
|
343
|
+
expect(result.stderr).toContain(
|
|
344
|
+
'Custom Heading type "hero" needs a non-empty standalone',
|
|
345
|
+
);
|
|
346
|
+
expect(fs.existsSync(path.join(tmpDir, 'variants-theme.css'))).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('rejects a custom Heading type whose every declaration the compiler drops', async () => {
|
|
350
|
+
const themeFile = writeTheme(
|
|
351
|
+
tmpDir,
|
|
352
|
+
`export default {
|
|
353
|
+
name: 'variants-theme',
|
|
354
|
+
tokens: { '--color-bg': '#fff' },
|
|
355
|
+
components: {
|
|
356
|
+
heading: { 'type:hero': { color: 'red; } body { color: blue' } },
|
|
357
|
+
},
|
|
358
|
+
};\n`,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const result = await runCli(
|
|
362
|
+
['--json', 'theme', 'build', path.relative(tmpDir, themeFile)],
|
|
363
|
+
tmpDir,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(result.code).toBe(1);
|
|
367
|
+
const envelope = JSON.parse(result.stdout);
|
|
368
|
+
expect(envelope.code).toBe('ERR_THEME_INVALID');
|
|
369
|
+
expect(envelope.error).toContain('Custom Heading type "hero"');
|
|
370
|
+
expect(fs.existsSync(path.join(tmpDir, 'variants-theme.css'))).toBe(false);
|
|
371
|
+
expect(
|
|
372
|
+
fs.existsSync(path.join(tmpDir, 'variants-theme.variants.d.ts')),
|
|
373
|
+
).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('builds a custom Heading type that keeps one valid declaration', async () => {
|
|
377
|
+
const themeFile = writeTheme(
|
|
378
|
+
tmpDir,
|
|
379
|
+
`export default {
|
|
380
|
+
name: 'variants-theme',
|
|
381
|
+
tokens: { '--color-bg': '#fff' },
|
|
382
|
+
components: {
|
|
383
|
+
heading: {
|
|
384
|
+
'type:hero': { fontSize: '80px', color: 'red; } body { color: blue' },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
};\n`,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const result = await runCli(
|
|
391
|
+
['--json', 'theme', 'build', path.relative(tmpDir, themeFile)],
|
|
392
|
+
tmpDir,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(result.code).toBe(0);
|
|
396
|
+
const envelope = JSON.parse(result.stdout);
|
|
397
|
+
expect(envelope.data.outputs.variantsDts).toBe(
|
|
398
|
+
'variants-theme.variants.d.ts',
|
|
399
|
+
);
|
|
400
|
+
expect(envelope.data.warnings.join('\n')).toContain('Declaration dropped');
|
|
401
|
+
});
|
|
402
|
+
|
|
327
403
|
it('makes generated custom component prop values type-check through public subpaths', async () => {
|
|
328
404
|
const themeFile = writeTheme(
|
|
329
405
|
tmpDir,
|
|
@@ -44,7 +44,10 @@ export const doc = {
|
|
|
44
44
|
],
|
|
45
45
|
exitCodes: [
|
|
46
46
|
{code: 0, when: 'success (including zero matches)'},
|
|
47
|
-
{
|
|
47
|
+
{
|
|
48
|
+
code: 1,
|
|
49
|
+
when: 'invalid --type, a non-positive --limit, or a query when @astryxdesign/core cannot be found (the playbook needs no core)',
|
|
50
|
+
},
|
|
48
51
|
],
|
|
49
52
|
related: ['search', 'template', 'component', 'hook', 'init'],
|
|
50
53
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `astryx build` documents that a query needs @astryxdesign/core while
|
|
5
|
+
* the playbook does not, and exits as documented without it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
13
|
+
import {doc as buildDoc} from './build.doc.mjs';
|
|
14
|
+
|
|
15
|
+
const SLOW = 30_000;
|
|
16
|
+
|
|
17
|
+
/** @type {string} */
|
|
18
|
+
let tmpDir;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-no-core-'));
|
|
22
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({name: 'consumer'}));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** @param {number} code */
|
|
30
|
+
const exitWhen = code => buildDoc.exitCodes?.find(e => e.code === code)?.when ?? '';
|
|
31
|
+
|
|
32
|
+
describe('astryx build without @astryxdesign/core', () => {
|
|
33
|
+
it('the CommandDoc lists a query without core under exit 1, and both modes exit 1', async () => {
|
|
34
|
+
expect(exitWhen(1)).toMatch(/a query when @astryxdesign\/core cannot be found/);
|
|
35
|
+
|
|
36
|
+
const text = await runCli(['build', 'dashboard'], tmpDir);
|
|
37
|
+
const json = await runCli(['--json', 'build', 'dashboard'], tmpDir);
|
|
38
|
+
expect(text.status).toBe(1);
|
|
39
|
+
expect(json.status).toBe(1);
|
|
40
|
+
expect(JSON.parse(json.stdout).error).toMatch(/@astryxdesign\/core/);
|
|
41
|
+
}, SLOW);
|
|
42
|
+
|
|
43
|
+
it('the CommandDoc says the playbook needs no core, and it exits 0', async () => {
|
|
44
|
+
expect(exitWhen(1)).toMatch(/the playbook needs no core/);
|
|
45
|
+
|
|
46
|
+
const {status, stdout} = await runCli(['--json', 'build'], tmpDir);
|
|
47
|
+
expect(status).toBe(0);
|
|
48
|
+
expect(JSON.parse(stdout).type).toBe('build.help');
|
|
49
|
+
}, SLOW);
|
|
50
|
+
});
|
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
emit,
|
|
23
23
|
section,
|
|
24
24
|
text,
|
|
25
|
+
list,
|
|
25
26
|
record,
|
|
26
27
|
records,
|
|
27
|
-
ARROW,
|
|
28
28
|
} from '../formatters/index.mjs';
|
|
29
29
|
import {cliError} from '../lib/cli-error.mjs';
|
|
30
30
|
import {defineCommand} from '../lib/define-command.mjs';
|
|
@@ -33,46 +33,33 @@ import {doc as buildCommand} from './build.doc.mjs';
|
|
|
33
33
|
import {doc as buildFn} from '../../../api/build/build.doc.mjs';
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Playbook commands as records whose field names are the JSON keys, so a
|
|
37
|
+
* reader can grep `^command:`. The command is run with the caller's invocation.
|
|
38
|
+
* @type {import('../formatters/index.mjs').RecordOptions}
|
|
38
39
|
*/
|
|
39
|
-
|
|
40
|
+
const COMMAND_RECORDS = {
|
|
41
|
+
fields: ['command', 'purpose'],
|
|
42
|
+
format: {command: command => formatCliCommand(command)},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Emit the build playbook (shown when `build` is run with no query) — a
|
|
47
|
+
* projection of the `build.help` data, so text and JSON carry the same steps.
|
|
48
|
+
* @param {import('../../../api/build/build.type.mjs').BuildHelpResponse['data']} playbook
|
|
49
|
+
*/
|
|
50
|
+
function printPlaybook(playbook) {
|
|
40
51
|
emit(
|
|
41
|
-
section(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
[
|
|
52
|
-
`2. If a [page] template matches ${ARROW} scaffold it and adapt:`,
|
|
53
|
-
` ${run} template <name> [path]`,
|
|
54
|
-
].join('\n'),
|
|
55
|
-
),
|
|
56
|
-
text(
|
|
57
|
-
[
|
|
58
|
-
`3. If nothing matches exactly ${ARROW} compose:`,
|
|
59
|
-
` ${run} template <name> --skeleton # study a close page's layout`,
|
|
60
|
-
` ${run} template <BlockName> # drop in each block from the kit`,
|
|
61
|
-
` ${run} component <Name> # fill remaining gaps (read props)`,
|
|
62
|
-
].join('\n'),
|
|
63
|
-
),
|
|
64
|
-
text(
|
|
65
|
-
[
|
|
66
|
-
'4. Rules (keep it on-system):',
|
|
67
|
-
' - No <div>/raw HTML for layout — use VStack/HStack/Grid/Stack/Card etc.',
|
|
68
|
-
` - No style={{}} — use component props; design tokens via \`${run} docs tokens\`.`,
|
|
69
|
-
' - Wrap the app in <Theme theme={...}> and import core reset.css + astryx.css.',
|
|
70
|
-
].join('\n'),
|
|
71
|
-
),
|
|
72
|
-
text(
|
|
73
|
-
`Tip: \`${run} build "<idea>"\` is the fastest way in. For a neutral ` +
|
|
74
|
-
`lookup of any component/doc/template, use \`${run} search <query>\`.`,
|
|
75
|
-
),
|
|
52
|
+
section(playbook.title),
|
|
53
|
+
...playbook.steps.flatMap((step, i) => [
|
|
54
|
+
section(`${i + 1}. ${step.title}`),
|
|
55
|
+
records(step.commands, COMMAND_RECORDS),
|
|
56
|
+
step.returns ? record({returns: step.returns}) : null,
|
|
57
|
+
]),
|
|
58
|
+
section(`${playbook.steps.length + 1}. Rules (keep it on-system)`),
|
|
59
|
+
list(playbook.rules),
|
|
60
|
+
...(playbook.related.length > 0
|
|
61
|
+
? [section('Related'), records(playbook.related, COMMAND_RECORDS)]
|
|
62
|
+
: []),
|
|
76
63
|
);
|
|
77
64
|
}
|
|
78
65
|
|
|
@@ -91,7 +78,10 @@ export function registerBuild(program) {
|
|
|
91
78
|
|
|
92
79
|
// No query → the playbook. Still routed through the API for the envelope.
|
|
93
80
|
if (!query || !String(query).trim()) {
|
|
94
|
-
const result =
|
|
81
|
+
const result =
|
|
82
|
+
/** @type {import('../../../api/build/build.type.mjs').BuildHelpResponse} */ (
|
|
83
|
+
await buildApi(undefined, {cwd: process.cwd()})
|
|
84
|
+
);
|
|
95
85
|
// The playbook is a document, not a lookup: one doc, always the same
|
|
96
86
|
// one. Counting it as a result keeps "what did this run answer with"
|
|
97
87
|
// true for the no-argument form too.
|
|
@@ -100,7 +90,7 @@ export function registerBuild(program) {
|
|
|
100
90
|
jsonOut(result);
|
|
101
91
|
return playbook;
|
|
102
92
|
}
|
|
103
|
-
printPlaybook(
|
|
93
|
+
printPlaybook(result.data);
|
|
104
94
|
return playbook;
|
|
105
95
|
}
|
|
106
96
|
|
|
@@ -251,12 +241,7 @@ export function registerBuild(program) {
|
|
|
251
241
|
'FRAME + FOUNDATION',
|
|
252
242
|
'Always-available shell + layout/text/action primitives.',
|
|
253
243
|
),
|
|
254
|
-
record({
|
|
255
|
-
frame,
|
|
256
|
-
foundation,
|
|
257
|
-
setup:
|
|
258
|
-
'import "@astryxdesign/core/reset.css" + "astryx.css"; no <div>/style for layout — use Stack/Grid + tokens',
|
|
259
|
-
}),
|
|
244
|
+
record({frame, foundation}),
|
|
260
245
|
);
|
|
261
246
|
|
|
262
247
|
// Last, so it is the line the reader leaves with — and only when the kit
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file The no-query `build` playbook is data. `--json` carries every step,
|
|
5
|
+
* rule and command; the text output is rendered from those values and shows
|
|
6
|
+
* nothing the envelope lacks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect} from 'vitest';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import {fileURLToPath} from 'node:url';
|
|
12
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
13
|
+
import {program} from '../index.mjs';
|
|
14
|
+
|
|
15
|
+
// Run against the monorepo root so @astryxdesign/core is discoverable.
|
|
16
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
17
|
+
const SLOW = 30_000;
|
|
18
|
+
|
|
19
|
+
/** @param {string} s */
|
|
20
|
+
const ascii = s => s.replace(/[\u2014\u2013]/g, '-');
|
|
21
|
+
|
|
22
|
+
describe('build playbook: text is a projection of the JSON', () => {
|
|
23
|
+
it('prints exactly the commands, steps and rules the envelope carries', async () => {
|
|
24
|
+
const json = await runCli(['build', '--json'], REPO);
|
|
25
|
+
expect(json.status).toBe(0);
|
|
26
|
+
const {type, data} = JSON.parse(json.stdout);
|
|
27
|
+
expect(type).toBe('build.help');
|
|
28
|
+
expect(data.steps.length).toBeGreaterThan(0);
|
|
29
|
+
expect(data.rules.length).toBeGreaterThan(0);
|
|
30
|
+
|
|
31
|
+
const human = await runCli(['build'], REPO);
|
|
32
|
+
expect(human.status).toBe(0);
|
|
33
|
+
const out = human.stdout;
|
|
34
|
+
|
|
35
|
+
expect(out).toContain(ascii(data.title));
|
|
36
|
+
for (const step of data.steps) {
|
|
37
|
+
expect(out).toContain(ascii(step.title));
|
|
38
|
+
if (step.returns) expect(out).toContain(ascii(step.returns));
|
|
39
|
+
}
|
|
40
|
+
for (const rule of data.rules) expect(out).toContain(ascii(rule));
|
|
41
|
+
|
|
42
|
+
/** @type {Array<{command: string, purpose?: string}>} */
|
|
43
|
+
const commands = [
|
|
44
|
+
...data.steps.flatMap((/** @type {any} */ s) => s.commands),
|
|
45
|
+
...data.related,
|
|
46
|
+
];
|
|
47
|
+
// Each command is one record whose field names are the JSON keys:
|
|
48
|
+
// `command:` (run with the caller's invocation), then `purpose:`.
|
|
49
|
+
const printed = out
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter(line => line.startsWith('command: '))
|
|
52
|
+
.map(line => line.slice('command: '.length).trim());
|
|
53
|
+
expect(printed).toHaveLength(commands.length);
|
|
54
|
+
commands.forEach(({command, purpose}, i) => {
|
|
55
|
+
expect(printed[i].endsWith(` ${command}`)).toBe(true);
|
|
56
|
+
if (purpose) expect(out).toContain(`purpose: ${ascii(purpose)}`);
|
|
57
|
+
});
|
|
58
|
+
// No column the handler drew itself.
|
|
59
|
+
expect(out).not.toMatch(/ {2,}# /);
|
|
60
|
+
}, SLOW);
|
|
61
|
+
|
|
62
|
+
it('names only commands this CLI registers', async () => {
|
|
63
|
+
const {stdout} = await runCli(['build', '--json'], REPO);
|
|
64
|
+
const {data} = JSON.parse(stdout);
|
|
65
|
+
const registered = new Set(program.commands.map(c => c.name()));
|
|
66
|
+
const commands = [
|
|
67
|
+
...data.steps.flatMap((/** @type {any} */ s) => s.commands),
|
|
68
|
+
...data.related,
|
|
69
|
+
];
|
|
70
|
+
expect(commands.length).toBeGreaterThan(0);
|
|
71
|
+
for (const {command} of commands) {
|
|
72
|
+
expect(registered.has(command.split(' ')[0])).toBe(true);
|
|
73
|
+
}
|
|
74
|
+
}, SLOW);
|
|
75
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file `build "<idea>"` text output names only fields the `build.kit`
|
|
5
|
+
* envelope carries — on the kit itself or on its entries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {describe, it, expect} from 'vitest';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import {fileURLToPath} from 'node:url';
|
|
11
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
12
|
+
|
|
13
|
+
// Run against the monorepo root so @astryxdesign/core is discoverable.
|
|
14
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
15
|
+
const SLOW = 60_000;
|
|
16
|
+
|
|
17
|
+
describe('build kit text fields mirror the JSON keys', () => {
|
|
18
|
+
it('prints no field the envelope lacks', async () => {
|
|
19
|
+
const json = await runCli(['build', 'analytics dashboard', '--json', '--verbose'], REPO);
|
|
20
|
+
expect(json.status).toBe(0);
|
|
21
|
+
const {data} = JSON.parse(json.stdout);
|
|
22
|
+
const keys = new Set([
|
|
23
|
+
...Object.keys(data),
|
|
24
|
+
...[...data.pages, ...data.blocks, ...data.domain].flatMap(e => Object.keys(e)),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const human = await runCli(['build', 'analytics dashboard', '--verbose'], REPO);
|
|
28
|
+
expect(human.status).toBe(0);
|
|
29
|
+
const fields = [
|
|
30
|
+
...new Set(
|
|
31
|
+
human.stdout
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(line => /^([A-Za-z]+):\s/.exec(line)?.[1])
|
|
34
|
+
.filter(Boolean),
|
|
35
|
+
),
|
|
36
|
+
];
|
|
37
|
+
expect(fields).toContain('frame');
|
|
38
|
+
for (const field of fields) expect(keys).toContain(field);
|
|
39
|
+
}, SLOW);
|
|
40
|
+
});
|
|
@@ -26,7 +26,6 @@ import {defineCommand} from '../../lib/define-command.mjs';
|
|
|
26
26
|
import {resultSet} from '../../../../foundation/debug/index.mjs';
|
|
27
27
|
import {ERROR_CODES} from '../../../../foundation/response/error-codes.mjs';
|
|
28
28
|
import {component as componentApi} from '../../../../api/component/component.mjs';
|
|
29
|
-
import {findRelatedBlocks} from '../../../../api/template/template.mjs';
|
|
30
29
|
import {Project} from '../../../../foundation/config/project.mjs';
|
|
31
30
|
import {warnOnIntegrationIssues} from '../../../../foundation/integrations/integration-warnings.mjs';
|
|
32
31
|
import {doc as componentCommand} from '../component.doc.mjs';
|
|
@@ -235,18 +234,14 @@ export function registerComponent(program) {
|
|
|
235
234
|
case 'component.detail': {
|
|
236
235
|
const resolvedName = (name || '').replace(/^XDS/, '');
|
|
237
236
|
const importHint = result.data.import;
|
|
237
|
+
if (result.data.parentDoc) emit(record(result.data, {fields: ['parentDoc']}));
|
|
238
238
|
const doc =
|
|
239
239
|
detail === 'brief'
|
|
240
240
|
? code(formatBrief(result.data, resolvedName, importHint, {themeData}))
|
|
241
241
|
: detail === 'compact'
|
|
242
242
|
? code(formatCompact(result.data, resolvedName, importHint))
|
|
243
243
|
: code(formatFull(result.data, {themeData, importHint}));
|
|
244
|
-
|
|
245
|
-
emit(
|
|
246
|
-
doc,
|
|
247
|
-
related.length > 0 && section('Related block templates'),
|
|
248
|
-
related.length > 0 && records(related, {fields: ['dirName', 'description']}),
|
|
249
|
-
);
|
|
244
|
+
emit(doc);
|
|
250
245
|
break;
|
|
251
246
|
}
|
|
252
247
|
|
|
@@ -120,4 +120,22 @@ describe('component() with --package option', () => {
|
|
|
120
120
|
expect(result.data.source).toContain('ProfileCardShowcase');
|
|
121
121
|
expect(result.data.aspectRatio).toBeCloseTo(16 / 9);
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
// A legacy package ships docs only: --source and --blocks answer the same
|
|
125
|
+
// way scoped as unscoped, never falling back to the plain doc.
|
|
126
|
+
it('--package + --source reports the missing source, like the unscoped lookup', async () => {
|
|
127
|
+
await expect(
|
|
128
|
+
component('ProfileCard', {cwd: tmpDir, source: true}),
|
|
129
|
+
).rejects.toMatchObject({code: 'ERR_NO_SOURCE'});
|
|
130
|
+
await expect(
|
|
131
|
+
component('ProfileCard', {cwd: tmpDir, package: '@test/ext', source: true}),
|
|
132
|
+
).rejects.toMatchObject({code: 'ERR_NO_SOURCE'});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('--package + --blocks returns the blocks, like the unscoped lookup', async () => {
|
|
136
|
+
const unscoped = await component('ProfileCard', {cwd: tmpDir, blocks: true});
|
|
137
|
+
const scoped = await component('ProfileCard', {cwd: tmpDir, package: '@test/ext', blocks: true});
|
|
138
|
+
expect(scoped.type).toBe('component.detail.blocks');
|
|
139
|
+
expect(scoped.data).toEqual(unscoped.data);
|
|
140
|
+
});
|
|
123
141
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {describe, it, expect} from 'vitest';
|
|
4
4
|
import {component} from '../../../api/component/component.mjs';
|
|
5
5
|
import {findShowcase, findRelatedBlocks} from '../../../api/template/template.mjs';
|
|
6
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
6
7
|
|
|
7
8
|
const CWD = {cwd: '.'};
|
|
8
9
|
|
|
@@ -74,6 +75,26 @@ describe('component() sub-component scoping', () => {
|
|
|
74
75
|
const result = await component('SideNav', CWD);
|
|
75
76
|
expect(result.data.name).toBe('SideNav');
|
|
76
77
|
});
|
|
78
|
+
|
|
79
|
+
// `parentDoc` is part of the component.detail schema: the text view
|
|
80
|
+
// projects it under the same key.
|
|
81
|
+
it('component("HStack") names its parent doc in JSON and text', async () => {
|
|
82
|
+
const result = await component('HStack', CWD);
|
|
83
|
+
expect(result.data.parentDoc).toBe('Stack');
|
|
84
|
+
|
|
85
|
+
const text = await runCli(['component', 'HStack'], process.cwd());
|
|
86
|
+
expect(text.code).toBe(0);
|
|
87
|
+
expect(text.stdout).toMatch(/^parentDoc: +Stack$/m);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('component("Stack") carries no parentDoc in JSON or text', async () => {
|
|
91
|
+
const result = await component('Stack', CWD);
|
|
92
|
+
expect(result.data).not.toHaveProperty('parentDoc');
|
|
93
|
+
|
|
94
|
+
const text = await runCli(['component', 'Stack'], process.cwd());
|
|
95
|
+
expect(text.code).toBe(0);
|
|
96
|
+
expect(text.stdout).not.toMatch(/^parentDoc:/m);
|
|
97
|
+
});
|
|
77
98
|
}, SCAN_TIMEOUT);
|
|
78
99
|
|
|
79
100
|
// ─── Bug: findShowcase priority ─────────────────────────────────
|
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
levenshteinDistance,
|
|
14
14
|
findClosestComponents,
|
|
15
15
|
} from './component/index.mjs';
|
|
16
|
+
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT = path.resolve(import.meta.dirname, '../../../../..');
|
|
16
19
|
|
|
17
20
|
let tmpDir;
|
|
18
21
|
|
|
@@ -591,3 +594,19 @@ describe('searchComponents', () => {
|
|
|
591
594
|
expect(results.length).toBe(0);
|
|
592
595
|
});
|
|
593
596
|
});
|
|
597
|
+
|
|
598
|
+
describe('component <Name> text output', () => {
|
|
599
|
+
// The text is a projection of the JSON envelope: it may show less, never
|
|
600
|
+
// data the envelope does not carry.
|
|
601
|
+
it('shows no related-block records that the JSON lacks', async () => {
|
|
602
|
+
const json = await runCli(['--json', 'component', 'Button'], REPO_ROOT);
|
|
603
|
+
expect(json.code).toBe(0);
|
|
604
|
+
expect(JSON.stringify(JSON.parse(json.stdout).data)).not.toContain('dirName');
|
|
605
|
+
|
|
606
|
+
const text = await runCli(['component', 'Button'], REPO_ROOT);
|
|
607
|
+
expect(text.code).toBe(0);
|
|
608
|
+
expect(text.stdout).toContain('# Button');
|
|
609
|
+
expect(text.stdout).not.toMatch(/^dirName:/m);
|
|
610
|
+
expect(text.stdout).not.toContain('Related block templates');
|
|
611
|
+
}, 30_000);
|
|
612
|
+
});
|
|
@@ -52,7 +52,7 @@ describe('--detail level ordering: component --list', () => {
|
|
|
52
52
|
|
|
53
53
|
it('brief is names-only (no targets, import hints, or prose descriptions)', () => {
|
|
54
54
|
expect(brief).not.toMatch(/Targets:/);
|
|
55
|
-
expect(brief).not.toMatch(
|
|
55
|
+
expect(brief).not.toMatch(/<- from/);
|
|
56
56
|
// Names only — should contain XDS component names but no " — " desc separator.
|
|
57
57
|
expect(brief).toMatch(/Button/);
|
|
58
58
|
expect(brief).not.toMatch(/ \u2014 /);
|
|
@@ -66,7 +66,7 @@ describe('--detail level ordering: component --list', () => {
|
|
|
66
66
|
|
|
67
67
|
it('full has dense per-entry docs (props, targets, and import hints)', () => {
|
|
68
68
|
expect(full).toMatch(/Targets:/);
|
|
69
|
-
expect(full).toMatch(
|
|
69
|
+
expect(full).toMatch(/<- from/);
|
|
70
70
|
// Prop name lists appear in the dense brief-all rendering.
|
|
71
71
|
expect(full).toMatch(/children/);
|
|
72
72
|
});
|