@astryxdesign/cli 0.6.5-canary.1bbbf63 → 0.6.5-canary.1d8fca6
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/api/gap-report/gap-report.d.mts +3 -1
- package/api/gap-report/gap-report.mjs +12 -2
- package/api/gap-report/gap-report.test.mjs +106 -0
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +1 -1
- package/clients/cli/commands/gap-report.mjs +12 -0
- package/clients/cli/commands/gap-report.test.mjs +63 -0
- package/package.json +9 -9
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Collect the effective handler set in delivery order: project config first,
|
|
6
6
|
* then integrations in config order. Valid handlers dedupe by handle identity;
|
|
7
|
-
* malformed integration exports
|
|
7
|
+
* malformed integration exports and integrations that failed to load retain
|
|
8
|
+
* their position as failed deliveries. An unloadable integration may export a
|
|
9
|
+
* handler, so it never counts as an empty set that enables the fallback.
|
|
8
10
|
*
|
|
9
11
|
* @param {Project} project
|
|
10
12
|
* @returns {HandlerEntry[]}
|
|
@@ -189,7 +189,9 @@ async function selectTarget(project, component, packageName) {
|
|
|
189
189
|
/**
|
|
190
190
|
* Collect the effective handler set in delivery order: project config first,
|
|
191
191
|
* then integrations in config order. Valid handlers dedupe by handle identity;
|
|
192
|
-
* malformed integration exports
|
|
192
|
+
* malformed integration exports and integrations that failed to load retain
|
|
193
|
+
* their position as failed deliveries. An unloadable integration may export a
|
|
194
|
+
* handler, so it never counts as an empty set that enables the fallback.
|
|
193
195
|
*
|
|
194
196
|
* @param {Project} project
|
|
195
197
|
* @returns {HandlerEntry[]}
|
|
@@ -214,7 +216,15 @@ export function collectGapReportHandlers(project) {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
for (const integration of project.loadedIntegrations) {
|
|
217
|
-
if (integration.__loadError)
|
|
219
|
+
if (integration.__loadError) {
|
|
220
|
+
entries.push({
|
|
221
|
+
kind: 'malformed',
|
|
222
|
+
handlerType: 'integration',
|
|
223
|
+
handlerName: integration.name,
|
|
224
|
+
error: `Integration failed to load, so it could not receive this report: ${integration.__loadError}. Fix the package or remove it from integrations in astryx.config, then rerun.`,
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
218
228
|
if (integration.__gapReportError) {
|
|
219
229
|
entries.push({
|
|
220
230
|
kind: 'malformed',
|
|
@@ -105,6 +105,45 @@ function addIntegration({
|
|
|
105
105
|
return {dir};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Add an integration whose module throws on import, so it never loads.
|
|
110
|
+
* @param {string} name
|
|
111
|
+
*/
|
|
112
|
+
function addBrokenIntegration(name) {
|
|
113
|
+
const dir = packageDir(name);
|
|
114
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(dir, 'package.json'),
|
|
117
|
+
JSON.stringify({name, version: '1.0.0', type: 'module'}),
|
|
118
|
+
);
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(dir, 'astryx.integration.mjs'),
|
|
121
|
+
"throw new Error('intentional integration load failure');\n",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Put a fake `gh` first on PATH that records its arguments if it is called.
|
|
127
|
+
* @returns {string} the file the fake writes when invoked
|
|
128
|
+
*/
|
|
129
|
+
function stubGh() {
|
|
130
|
+
const binDir = path.join(projectDir, 'bin');
|
|
131
|
+
const argsFile = path.join(projectDir, 'gh-args.json');
|
|
132
|
+
fs.mkdirSync(binDir, {recursive: true});
|
|
133
|
+
fs.writeFileSync(
|
|
134
|
+
path.join(binDir, 'gh'),
|
|
135
|
+
`#!/usr/bin/env node
|
|
136
|
+
import fs from 'node:fs';
|
|
137
|
+
fs.writeFileSync(${JSON.stringify(argsFile)}, JSON.stringify(process.argv.slice(2)));
|
|
138
|
+
console.log('https://github.com/acme/core/issues/42');
|
|
139
|
+
`,
|
|
140
|
+
{mode: 0o755},
|
|
141
|
+
);
|
|
142
|
+
fs.chmodSync(path.join(binDir, 'gh'), 0o755);
|
|
143
|
+
vi.stubEnv('PATH', `${binDir}${path.delimiter}${process.env.PATH ?? ''}`);
|
|
144
|
+
return argsFile;
|
|
145
|
+
}
|
|
146
|
+
|
|
108
147
|
const reportOptions = {
|
|
109
148
|
category: 'missing_variant',
|
|
110
149
|
reason: 'Need a compact size',
|
|
@@ -681,6 +720,73 @@ describe('gapReport handler isolation', () => {
|
|
|
681
720
|
});
|
|
682
721
|
});
|
|
683
722
|
|
|
723
|
+
it('an integration that fails to load is a failed delivery, not a missing handler', async () => {
|
|
724
|
+
addBrokenIntegration('@test/broken');
|
|
725
|
+
configure(['@test/broken'], {
|
|
726
|
+
issuesUrl: 'https://github.com/acme/core/issues/new',
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const result = await gapReport('General', {
|
|
730
|
+
...reportOptions,
|
|
731
|
+
cwd: projectDir,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(result.data.status).toBe('failed');
|
|
735
|
+
expect(result.data.deliveries).toEqual([
|
|
736
|
+
{
|
|
737
|
+
handlerType: 'integration',
|
|
738
|
+
handler: '@test/broken',
|
|
739
|
+
audience: null,
|
|
740
|
+
status: 'failed',
|
|
741
|
+
url: null,
|
|
742
|
+
message: expect.stringContaining(
|
|
743
|
+
'intentional integration load failure',
|
|
744
|
+
),
|
|
745
|
+
},
|
|
746
|
+
]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('an integration that fails to load keeps the public fallback off, even with consent', async () => {
|
|
750
|
+
addBrokenIntegration('@test/broken');
|
|
751
|
+
configure(['@test/broken'], {
|
|
752
|
+
issuesUrl: 'https://github.com/acme/core/issues/new',
|
|
753
|
+
});
|
|
754
|
+
const argsFile = stubGh();
|
|
755
|
+
|
|
756
|
+
const result = await gapReport('General', {
|
|
757
|
+
...reportOptions,
|
|
758
|
+
confirmPublic: true,
|
|
759
|
+
cwd: projectDir,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
expect(result.data.status).toBe('failed');
|
|
763
|
+
expect(result.data.deliveries.map(d => d.handlerType)).toEqual([
|
|
764
|
+
'integration',
|
|
765
|
+
]);
|
|
766
|
+
expect(fs.existsSync(argsFile)).toBe(false);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it('an integration that fails to load keeps its place beside working handlers', async () => {
|
|
770
|
+
addBrokenIntegration('@test/broken');
|
|
771
|
+
addIntegration({
|
|
772
|
+
name: '@test/valid',
|
|
773
|
+
audience: 'internal',
|
|
774
|
+
handleBody: "return {status: 'filed', message: 'ok'};",
|
|
775
|
+
});
|
|
776
|
+
configure(['@test/broken', '@test/valid']);
|
|
777
|
+
|
|
778
|
+
const result = await gapReport('General', {
|
|
779
|
+
...reportOptions,
|
|
780
|
+
cwd: projectDir,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
expect(result.data.status).toBe('partial');
|
|
784
|
+
expect(result.data.deliveries.map(d => [d.handler, d.status])).toEqual([
|
|
785
|
+
['@test/broken', 'failed'],
|
|
786
|
+
['@test/valid', 'filed'],
|
|
787
|
+
]);
|
|
788
|
+
});
|
|
789
|
+
|
|
684
790
|
it('invalid handler receipt is recorded as failed delivery', async () => {
|
|
685
791
|
addIntegration({
|
|
686
792
|
name: '@test/bad-receipt',
|
|
@@ -174,7 +174,7 @@ message: Rerun with --confirm-public to file this report.`,
|
|
|
174
174
|
},
|
|
175
175
|
{
|
|
176
176
|
type: 'prose',
|
|
177
|
-
text: 'One handler anywhere in the app, from the app or from any package, turns the fallback off for every report. See {@link command:gap-report}.',
|
|
177
|
+
text: 'One handler anywhere in the app, from the app or from any package, turns the fallback off for every report. So does a listed package that fails to load: the CLI cannot tell whether it has a handler, so the report fails for that package instead of going to another tracker. See {@link command:gap-report}.',
|
|
178
178
|
},
|
|
179
179
|
],
|
|
180
180
|
},
|
|
@@ -55,6 +55,18 @@ export function registerGapReport(program) {
|
|
|
55
55
|
result.type === 'gap-report.file' &&
|
|
56
56
|
(result.data.status === 'failed' || result.data.status === 'partial');
|
|
57
57
|
|
|
58
|
+
// stderr in both modes, so a caller that reads only the exit code or the
|
|
59
|
+
// JSON envelope still sees which delivery failed and why.
|
|
60
|
+
if (isFailure && result.type === 'gap-report.file') {
|
|
61
|
+
for (const delivery of result.data.deliveries) {
|
|
62
|
+
if (delivery.status === 'failed') {
|
|
63
|
+
console.error(
|
|
64
|
+
`gap-report: delivery to ${delivery.handler} failed: ${delivery.message ?? 'no message'}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
if (program.opts().json) {
|
|
59
71
|
jsonOut(result);
|
|
60
72
|
if (isFailure) process.exitCode = 1;
|
|
@@ -172,6 +172,69 @@ export default {};\n`,
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
it('exits nonzero and names the integration on stderr when one fails to load', async () => {
|
|
176
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-gap-broken-'));
|
|
177
|
+
try {
|
|
178
|
+
fs.writeFileSync(
|
|
179
|
+
path.join(cwd, 'package.json'),
|
|
180
|
+
JSON.stringify({name: 'fixture-app'}),
|
|
181
|
+
);
|
|
182
|
+
fs.writeFileSync(
|
|
183
|
+
path.join(cwd, 'astryx.config.mjs'),
|
|
184
|
+
"export default {integrations: ['@test/broken']};\n",
|
|
185
|
+
);
|
|
186
|
+
const integrationDir = path.join(cwd, 'node_modules', '@test', 'broken');
|
|
187
|
+
fs.mkdirSync(integrationDir, {recursive: true});
|
|
188
|
+
fs.writeFileSync(
|
|
189
|
+
path.join(integrationDir, 'package.json'),
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
name: '@test/broken',
|
|
192
|
+
version: '1.0.0',
|
|
193
|
+
type: 'module',
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
fs.writeFileSync(
|
|
197
|
+
path.join(integrationDir, 'astryx.integration.mjs'),
|
|
198
|
+
"throw new Error('intentional integration load failure');\n",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const cli = await runCli(
|
|
202
|
+
[
|
|
203
|
+
'gap-report',
|
|
204
|
+
'General',
|
|
205
|
+
'--category',
|
|
206
|
+
'docs_gap',
|
|
207
|
+
'--reason',
|
|
208
|
+
'Need a keyboard example',
|
|
209
|
+
'--json',
|
|
210
|
+
],
|
|
211
|
+
{cwd},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(cli.status).toBe(1);
|
|
215
|
+
expect(JSON.parse(cli.stdout)).toMatchObject({
|
|
216
|
+
type: 'gap-report.file',
|
|
217
|
+
data: {
|
|
218
|
+
status: 'failed',
|
|
219
|
+
filedCount: 0,
|
|
220
|
+
deliveries: [
|
|
221
|
+
{
|
|
222
|
+
handlerType: 'integration',
|
|
223
|
+
handler: '@test/broken',
|
|
224
|
+
status: 'failed',
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
expect(cli.stderr).toContain(
|
|
230
|
+
'gap-report: delivery to @test/broken failed:',
|
|
231
|
+
);
|
|
232
|
+
expect(cli.stderr).toContain('intentional integration load failure');
|
|
233
|
+
} finally {
|
|
234
|
+
fs.rmSync(cwd, {recursive: true, force: true});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
175
238
|
it('reports missing required fields through the JSON error contract', async () => {
|
|
176
239
|
const cli = await runCli(['gap-report', 'Button', '--json']);
|
|
177
240
|
expect(cli.status).toBe(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.6.5-canary.
|
|
3
|
+
"version": "0.6.5-canary.1d8fca6",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"zod": "^4.4.3"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
|
-
"@astryxdesign/charts": "0.6.5-canary.
|
|
105
|
-
"@astryxdesign/core": "0.6.5-canary.
|
|
106
|
-
"@astryxdesign/lab": "0.6.5-canary.
|
|
107
|
-
"@astryxdesign/theme-neutral": "0.6.5-canary.
|
|
104
|
+
"@astryxdesign/charts": "0.6.5-canary.1d8fca6",
|
|
105
|
+
"@astryxdesign/core": "0.6.5-canary.1d8fca6",
|
|
106
|
+
"@astryxdesign/lab": "0.6.5-canary.1d8fca6",
|
|
107
|
+
"@astryxdesign/theme-neutral": "0.6.5-canary.1d8fca6"
|
|
108
108
|
},
|
|
109
109
|
"peerDependenciesMeta": {
|
|
110
110
|
"@astryxdesign/charts": {
|
|
@@ -121,10 +121,10 @@
|
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
|
-
"@astryxdesign/charts": "0.6.5-canary.
|
|
125
|
-
"@astryxdesign/core": "0.6.5-canary.
|
|
126
|
-
"@astryxdesign/lab": "0.6.5-canary.
|
|
127
|
-
"@astryxdesign/theme-neutral": "0.6.5-canary.
|
|
124
|
+
"@astryxdesign/charts": "0.6.5-canary.1d8fca6",
|
|
125
|
+
"@astryxdesign/core": "0.6.5-canary.1d8fca6",
|
|
126
|
+
"@astryxdesign/lab": "0.6.5-canary.1d8fca6",
|
|
127
|
+
"@astryxdesign/theme-neutral": "0.6.5-canary.1d8fca6",
|
|
128
128
|
"@heroicons/react": "^2.2.0",
|
|
129
129
|
"@stylexjs/stylex": "^0.19.0",
|
|
130
130
|
"@types/babel__core": "^7.20.5",
|