@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4
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 +2 -1
- package/api/build/build.type.d.mts +2 -2
- package/api/build/build.type.mjs +2 -2
- package/api/component/component.type.d.mts +6 -6
- package/api/component/component.type.mjs +19 -19
- package/api/discover/discover.type.d.mts +4 -4
- package/api/discover/discover.type.mjs +10 -10
- package/api/docs/_adapter.d.mts +37 -24
- package/api/docs/_adapter.mjs +169 -83
- package/api/docs/compiled-topics.test.mjs +78 -0
- package/api/docs/detail/detail.mjs +14 -63
- package/api/docs/detail/section/section.d.mts +1 -1
- package/api/docs/detail/section/section.mjs +44 -20
- package/api/docs/detail/section/section.test.mjs +41 -0
- package/api/docs/docs.d.mts +7 -2
- package/api/docs/docs.doc.mjs +27 -10
- package/api/docs/docs.mjs +16 -9
- package/api/docs/docs.test.mjs +6 -0
- package/api/docs/docs.type.d.mts +40 -3
- package/api/docs/docs.type.mjs +36 -8
- package/api/docs/index/index.d.mts +18 -0
- package/api/docs/index/index.mjs +32 -0
- package/api/docs/index/index.test.mjs +62 -0
- package/api/docs/integrationDocs.test.mjs +106 -0
- package/api/doctor/doctor.d.mts +48 -0
- package/api/doctor/doctor.mjs +232 -0
- package/api/doctor/doctor.test.mjs +196 -0
- package/api/hook/hook.type.d.mts +3 -3
- package/api/hook/hook.type.mjs +11 -11
- package/api/hook/list/list.d.mts +1 -1
- package/api/integration/add-contribution.mjs +5 -3
- package/api/integration/add-contribution.test.mjs +4 -4
- package/api/integration/integration-authoring.type.d.mts +1 -1
- package/api/integration/pack-check.mjs +49 -7
- package/api/integration/pack-check.test.mjs +249 -0
- package/api/search/search.d.mts +1 -1
- package/api/search/search.mjs +5 -5
- package/api/search/search.type.d.mts +2 -2
- package/api/search/search.type.mjs +1 -1
- package/api/swizzle/swizzle.type.d.mts +2 -2
- package/api/swizzle/swizzle.type.mjs +2 -2
- package/api/template/template.d.mts +1 -1
- package/api/template/template.type.d.mts +6 -6
- package/api/template/template.type.mjs +12 -12
- package/api/theme/build/build.mjs +20 -6
- package/api/theme/build/build.test.mjs +127 -0
- package/api/theme/palette/generate/generate.mjs +1 -1
- package/api/theme/palette/generate/generator.d.mts +10 -13
- package/api/theme/palette/generate/generator.mjs +7 -3
- package/api/theme/theme.type.d.mts +170 -11
- package/api/theme/theme.type.mjs +94 -27
- package/api/upgrade/_adapter.mjs +71 -5
- package/api/upgrade/project-context.test.mjs +272 -0
- package/api/upgrade/upgrade.doc.mjs +4 -3
- package/api/upgrade/upgrade.type.d.mts +5 -5
- package/api/upgrade/upgrade.type.mjs +11 -11
- package/assets/codemods/integration-discovery.mjs +40 -2
- package/assets/codemods/integration-discovery.test.mjs +58 -0
- package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
- package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
- package/assets/docs/README.md +9 -0
- package/assets/docs/authoring.doc.mjs +14 -0
- package/assets/docs/cli-integrations.doc.mjs +86 -15
- package/assets/docs/styling-libraries.doc.mjs +1 -1
- package/assets/docs/working-with-ai.doc.mjs +1 -1
- package/authoring/_shared/contract.ts +22 -0
- package/authoring/codemod/codemod.doc.mjs +6 -1
- package/authoring/codemod/parse.d.mts +8 -8
- package/authoring/codemod/parse.mjs +8 -6
- package/authoring/config/parse.d.mts +13 -13
- package/authoring/config/parse.mjs +8 -8
- package/authoring/config/type.ts +3 -3
- package/authoring/debug/parse.d.mts +5 -5
- package/authoring/debug/parse.mjs +3 -3
- package/authoring/doctypes/_schema.d.mts +788 -23
- package/authoring/doctypes/_schema.mjs +492 -39
- package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
- package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
- package/authoring/doctypes/base/type.ts +40 -0
- package/authoring/doctypes/command/command.doc.mjs +3 -2
- package/authoring/doctypes/command/parse.d.mts +2 -2
- package/authoring/doctypes/command/parse.mjs +1 -1
- package/authoring/doctypes/command/type.ts +3 -2
- package/authoring/doctypes/component/component.doc.mjs +6 -3
- package/authoring/doctypes/component/parse.d.mts +2 -2
- package/authoring/doctypes/component/parse.mjs +1 -1
- package/authoring/doctypes/component/type.ts +4 -3
- package/authoring/doctypes/enum/parse.d.mts +2 -2
- package/authoring/doctypes/enum/parse.mjs +1 -1
- package/authoring/doctypes/enum/type.ts +3 -1
- package/authoring/doctypes/function/function.doc.mjs +4 -0
- package/authoring/doctypes/function/parse.d.mts +2 -2
- package/authoring/doctypes/function/parse.mjs +1 -1
- package/authoring/doctypes/function/type.ts +6 -2
- package/authoring/doctypes/hook/hook.doc.mjs +4 -0
- package/authoring/doctypes/hook/parse.d.mts +2 -2
- package/authoring/doctypes/hook/parse.mjs +1 -1
- package/authoring/doctypes/hook/type.ts +3 -2
- package/authoring/doctypes/legacy.d.mts +8 -6
- package/authoring/doctypes/legacy.mjs +5 -4
- package/authoring/doctypes/load-contract.test.mjs +207 -0
- package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
- package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
- package/authoring/doctypes/namespace/parse.d.mts +12 -0
- package/authoring/doctypes/namespace/parse.mjs +25 -0
- package/authoring/doctypes/namespace/parse.test.mjs +165 -0
- package/authoring/doctypes/namespace/type.ts +71 -0
- package/authoring/doctypes/parse.d.mts +20 -18
- package/authoring/doctypes/parse.mjs +16 -10
- package/authoring/doctypes/parse.test.mjs +77 -3
- package/authoring/doctypes/reference/parse.d.mts +2 -2
- package/authoring/doctypes/reference/parse.mjs +8 -5
- package/authoring/doctypes/reference/reference.doc.mjs +17 -4
- package/authoring/doctypes/reference/type.ts +51 -5
- package/authoring/doctypes/schema/parse.d.mts +2 -2
- package/authoring/doctypes/schema/parse.mjs +1 -1
- package/authoring/doctypes/schema/type.ts +3 -2
- package/authoring/doctypes/template/parse.d.mts +92 -1
- package/authoring/doctypes/template/parse.mjs +36 -2
- package/authoring/doctypes/template/parse.test.mjs +8 -2
- package/authoring/doctypes/template/template.doc.mjs +4 -0
- package/authoring/doctypes/template/type.ts +5 -2
- package/authoring/doctypes/types.ts +10 -9
- package/authoring/gap-report/parse.d.mts +10 -10
- package/authoring/gap-report/parse.mjs +6 -6
- package/authoring/gap-report/type.ts +1 -1
- package/authoring/identity/identity.doc.d.mts +9 -0
- package/authoring/identity/identity.doc.mjs +61 -0
- package/authoring/identity/type.ts +132 -0
- package/authoring/index.d.mts +1 -0
- package/authoring/index.d.ts +49 -17
- package/authoring/index.mjs +1 -0
- package/authoring/integration/integration.doc.mjs +13 -6
- package/authoring/integration/parse.d.mts +2 -2
- package/authoring/integration/parse.mjs +1 -1
- package/authoring/integration/parse.test.mjs +10 -1
- package/authoring/integration/schema.d.mts +6 -4
- package/authoring/integration/schema.mjs +9 -3
- package/authoring/integration/type.ts +23 -6
- package/authoring/shadcn/receipt.d.mts +6 -6
- package/clients/cli/commands/docs.doc.mjs +13 -3
- package/clients/cli/commands/docs.mjs +121 -21
- package/clients/cli/commands/docs.test.mjs +88 -0
- package/clients/cli/commands/integration-authoring.test.mjs +13 -9
- package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
- package/clients/cli/commands/upgrade.doc.mjs +2 -2
- package/clients/cli/formatters/index.mjs +162 -1
- package/clients/cli/formatters/index.test.mjs +91 -0
- package/clients/cli/lib/manifest.mjs +7 -2
- package/foundation/config/project.mjs +21 -6
- package/foundation/discovery/authoring-self-docs.d.mts +69 -0
- package/foundation/discovery/authoring-self-docs.mjs +214 -0
- package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
- package/foundation/discovery/component-discovery.d.mts +1 -1
- package/foundation/discovery/component-discovery.mjs +2 -1
- package/foundation/discovery/docs-discovery.d.mts +11 -4
- package/foundation/discovery/docs-discovery.mjs +208 -88
- package/foundation/discovery/docs-discovery.test.mjs +279 -13
- package/foundation/discovery/docs-output-budget.d.mts +28 -0
- package/foundation/discovery/docs-output-budget.mjs +50 -0
- package/foundation/discovery/docs-section-key.d.mts +98 -0
- package/foundation/discovery/docs-section-key.mjs +221 -0
- package/foundation/discovery/docs-section-key.test.mjs +224 -0
- package/foundation/discovery/template-adapter.mjs +2 -1
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/doc-compiler/compile.d.mts +162 -0
- package/foundation/doc-compiler/compile.mjs +262 -0
- package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
- package/foundation/doc-compiler/ir.d.mts +9 -0
- package/foundation/doc-compiler/ir.mjs +287 -0
- package/foundation/doc-compiler/lenses.d.mts +33 -0
- package/foundation/doc-compiler/lenses.mjs +127 -0
- package/foundation/identity/provider-identity.d.mts +90 -0
- package/foundation/identity/provider-identity.mjs +320 -0
- package/foundation/identity/provider-identity.test.mjs +254 -0
- package/foundation/identity/providers.d.mts +7 -0
- package/foundation/identity/providers.mjs +16 -0
- package/foundation/integrations/autolink.mjs +12 -5
- package/foundation/integrations/integration-warnings.mjs +6 -0
- package/foundation/integrations/integrations.d.mts +46 -2
- package/foundation/integrations/integrations.mjs +167 -8
- package/foundation/integrations/integrations.test.mjs +384 -1
- package/foundation/integrations/provider-conflicts.test.mjs +125 -0
- package/foundation/integrations/validate-contributions.d.mts +2 -0
- package/foundation/integrations/validate-contributions.mjs +10 -0
- package/foundation/response/json-contract.test.mjs +46 -17
- package/foundation/response/response-types.doc.mjs +6 -1
- package/package.json +9 -11
|
@@ -77,6 +77,7 @@ describe('configured integrations', () => {
|
|
|
77
77
|
const loaded = project.loadedIntegrations[0];
|
|
78
78
|
// Identity comes from package.json, not the manifest.
|
|
79
79
|
expect(loaded.name).toBe('@acme/widgets');
|
|
80
|
+
expect(loaded.providerId).toBe('@acme/widgets');
|
|
80
81
|
expect(loaded.version).toBe('1.2.3');
|
|
81
82
|
expect(loaded.components).toBe(path.join(pkgDir, 'docs'));
|
|
82
83
|
expect(loaded.templates).toBe(path.join(pkgDir, 'blocks'));
|
|
@@ -84,6 +85,43 @@ describe('configured integrations', () => {
|
|
|
84
85
|
expect(loaded.issuesUrl).toBe('https://example.com/issues');
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
it('preserves provider identity across an explicit package rename', async () => {
|
|
89
|
+
writeManifestPackage(tmpDir, {
|
|
90
|
+
body: `export default {providerId: '@acme/legacy-widgets'};\n`,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const [loaded] = await loadIntegrations(['@acme/widgets'], {cwd: tmpDir});
|
|
94
|
+
expect(loaded.name).toBe('@acme/widgets');
|
|
95
|
+
expect(loaded.providerId).toBe('@acme/legacy-widgets');
|
|
96
|
+
expect(loaded.__unknownKeys).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('deduplicates package aliases by canonical provider identity', async () => {
|
|
100
|
+
writeManifestPackage(tmpDir, {body: `export default {};\n`});
|
|
101
|
+
const aliasDir = path.join(
|
|
102
|
+
tmpDir,
|
|
103
|
+
'node_modules',
|
|
104
|
+
'@acme',
|
|
105
|
+
'widgets-alias',
|
|
106
|
+
);
|
|
107
|
+
fs.mkdirSync(aliasDir, {recursive: true});
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(aliasDir, 'package.json'),
|
|
110
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
111
|
+
);
|
|
112
|
+
fs.writeFileSync(
|
|
113
|
+
path.join(aliasDir, 'astryx.integration.mjs'),
|
|
114
|
+
`export default {};\n`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const loaded = await loadIntegrations(
|
|
118
|
+
['@acme/widgets', '@acme/widgets-alias'],
|
|
119
|
+
{cwd: tmpDir},
|
|
120
|
+
);
|
|
121
|
+
expect(loaded).toHaveLength(1);
|
|
122
|
+
expect(loaded[0].providerId).toBe('@acme/widgets');
|
|
123
|
+
});
|
|
124
|
+
|
|
87
125
|
it('makes integration components discoverable', async () => {
|
|
88
126
|
const pkgDir = writeManifestPackage(tmpDir, {
|
|
89
127
|
body: `export default { components: './docs' };\n`,
|
|
@@ -207,6 +245,347 @@ describe('configured integrations', () => {
|
|
|
207
245
|
});
|
|
208
246
|
});
|
|
209
247
|
|
|
248
|
+
describe('provider identity conflicts', () => {
|
|
249
|
+
/**
|
|
250
|
+
* Install one integration package that contributes a single component.
|
|
251
|
+
* @param {string} dir directory under node_modules
|
|
252
|
+
* @param {{name: string, version: string}} pkg package.json identity
|
|
253
|
+
* @param {Record<string, unknown>} manifest extra manifest fields
|
|
254
|
+
* @param {string} component the component it contributes
|
|
255
|
+
*/
|
|
256
|
+
function installPackage(dir, pkg, manifest, component) {
|
|
257
|
+
const pkgDir = path.join(tmpDir, 'node_modules', ...dir.split('/'));
|
|
258
|
+
const componentsDir = path.join(pkgDir, 'components');
|
|
259
|
+
fs.mkdirSync(componentsDir, {recursive: true});
|
|
260
|
+
fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify(pkg));
|
|
261
|
+
fs.writeFileSync(
|
|
262
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
263
|
+
`export default ${JSON.stringify({components: './components', ...manifest})};\n`,
|
|
264
|
+
);
|
|
265
|
+
fs.writeFileSync(
|
|
266
|
+
path.join(componentsDir, `${component}.doc.mjs`),
|
|
267
|
+
`export default {type: 'component', name: '${component}', props: []};\n`,
|
|
268
|
+
);
|
|
269
|
+
fs.writeFileSync(
|
|
270
|
+
path.join(componentsDir, `${component}.tsx`),
|
|
271
|
+
`export function ${component}() { return null; }\n`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** @param {string[]} integrations */
|
|
276
|
+
function configure(integrations) {
|
|
277
|
+
fs.writeFileSync(
|
|
278
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
279
|
+
`export default ${JSON.stringify({integrations})};\n`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Make the project directory itself the package being authored.
|
|
285
|
+
* @param {Record<string, unknown>} pkg package.json fields
|
|
286
|
+
* @param {Record<string, unknown>} manifest extra manifest fields
|
|
287
|
+
* @param {string} component the component it contributes
|
|
288
|
+
*/
|
|
289
|
+
function writeLocalPackage(pkg, manifest, component) {
|
|
290
|
+
const componentsDir = path.join(tmpDir, 'local-components');
|
|
291
|
+
fs.mkdirSync(componentsDir, {recursive: true});
|
|
292
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify(pkg));
|
|
293
|
+
fs.writeFileSync(
|
|
294
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
295
|
+
`export default ${JSON.stringify({components: './local-components', ...manifest})};\n`,
|
|
296
|
+
);
|
|
297
|
+
fs.writeFileSync(
|
|
298
|
+
path.join(componentsDir, `${component}.doc.mjs`),
|
|
299
|
+
`export default {type: 'component', name: '${component}', props: []};\n`,
|
|
300
|
+
);
|
|
301
|
+
fs.writeFileSync(
|
|
302
|
+
path.join(componentsDir, `${component}.tsx`),
|
|
303
|
+
`export function ${component}() { return null; }\n`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** @param {import('../config/project.mjs').Project} project */
|
|
308
|
+
async function conflictIssues(project) {
|
|
309
|
+
return (await project.issues()).filter(
|
|
310
|
+
issue => issue.code === 'duplicate_provider',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
it('keeps the first package and reports a different package claiming its ID', async () => {
|
|
315
|
+
installPackage(
|
|
316
|
+
'@acme/widgets',
|
|
317
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
318
|
+
{},
|
|
319
|
+
'Widget',
|
|
320
|
+
);
|
|
321
|
+
installPackage(
|
|
322
|
+
'@acme/renamed',
|
|
323
|
+
{name: '@acme/renamed', version: '2.0.0'},
|
|
324
|
+
{providerId: '@acme/widgets'},
|
|
325
|
+
'RenamedWidget',
|
|
326
|
+
);
|
|
327
|
+
configure(['@acme/widgets', '@acme/renamed']);
|
|
328
|
+
|
|
329
|
+
const loaded = await loadIntegrations(['@acme/widgets', '@acme/renamed'], {
|
|
330
|
+
cwd: tmpDir,
|
|
331
|
+
});
|
|
332
|
+
expect(loaded.map(integration => integration.name)).toEqual([
|
|
333
|
+
'@acme/widgets',
|
|
334
|
+
'@acme/renamed',
|
|
335
|
+
]);
|
|
336
|
+
expect(loaded[1]).toMatchObject({
|
|
337
|
+
providerId: '@acme/widgets',
|
|
338
|
+
__providerConflict: {
|
|
339
|
+
providerId: '@acme/widgets',
|
|
340
|
+
claimedBy: '@acme/widgets',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
expect(loaded[1].components).toBeUndefined();
|
|
344
|
+
|
|
345
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
346
|
+
const owned = (await project.components()).map(component => [
|
|
347
|
+
component.package,
|
|
348
|
+
component.name,
|
|
349
|
+
]);
|
|
350
|
+
expect(owned).toContainEqual(['@acme/widgets', 'Widget']);
|
|
351
|
+
expect(owned.some(([pkg]) => pkg === '@acme/renamed')).toBe(false);
|
|
352
|
+
expect(await project.issues()).toContainEqual(
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
package: '@acme/renamed@2.0.0',
|
|
355
|
+
code: 'duplicate_provider',
|
|
356
|
+
severity: 'warning',
|
|
357
|
+
message: expect.stringContaining(
|
|
358
|
+
'@acme/widgets@1.0.0 loads first and is used',
|
|
359
|
+
),
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('reports the same package configured again at another version', async () => {
|
|
365
|
+
installPackage(
|
|
366
|
+
'@acme/widgets',
|
|
367
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
368
|
+
{},
|
|
369
|
+
'Widget',
|
|
370
|
+
);
|
|
371
|
+
installPackage(
|
|
372
|
+
'widgets-next',
|
|
373
|
+
{name: '@acme/widgets', version: '2.0.0'},
|
|
374
|
+
{},
|
|
375
|
+
'NextWidget',
|
|
376
|
+
);
|
|
377
|
+
configure(['@acme/widgets', 'widgets-next']);
|
|
378
|
+
|
|
379
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
380
|
+
const owned = (await project.components()).map(component => component.name);
|
|
381
|
+
expect(owned).toContain('Widget');
|
|
382
|
+
expect(owned).not.toContain('NextWidget');
|
|
383
|
+
expect(await project.issues()).toContainEqual(
|
|
384
|
+
expect.objectContaining({
|
|
385
|
+
package: '@acme/widgets@2.0.0',
|
|
386
|
+
code: 'duplicate_provider',
|
|
387
|
+
severity: 'warning',
|
|
388
|
+
message: expect.stringContaining('(from "widgets-next")'),
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('reports an autolinked dependency that claims a configured ID', async () => {
|
|
394
|
+
installPackage(
|
|
395
|
+
'@acme/widgets',
|
|
396
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
397
|
+
{},
|
|
398
|
+
'Widget',
|
|
399
|
+
);
|
|
400
|
+
installPackage(
|
|
401
|
+
'@acme/renamed',
|
|
402
|
+
{name: '@acme/renamed', version: '2.0.0'},
|
|
403
|
+
{providerId: '@acme/widgets'},
|
|
404
|
+
'RenamedWidget',
|
|
405
|
+
);
|
|
406
|
+
configure(['@acme/widgets']);
|
|
407
|
+
fs.writeFileSync(
|
|
408
|
+
path.join(tmpDir, 'package.json'),
|
|
409
|
+
JSON.stringify({
|
|
410
|
+
name: 'consumer',
|
|
411
|
+
dependencies: {'@acme/renamed': '2.0.0'},
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
416
|
+
expect(
|
|
417
|
+
project.loadedIntegrations.find(
|
|
418
|
+
integration => integration.name === '@acme/renamed',
|
|
419
|
+
),
|
|
420
|
+
).toMatchObject({
|
|
421
|
+
__autolinked: true,
|
|
422
|
+
__providerConflict: {claimedBy: '@acme/widgets'},
|
|
423
|
+
});
|
|
424
|
+
expect(await project.issues()).toContainEqual(
|
|
425
|
+
expect.objectContaining({
|
|
426
|
+
package: '@acme/renamed@2.0.0',
|
|
427
|
+
code: 'duplicate_provider',
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("reports a set-aside package whose spec is the winner's name", async () => {
|
|
433
|
+
installPackage(
|
|
434
|
+
'widgets-old',
|
|
435
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
436
|
+
{},
|
|
437
|
+
'OldWidget',
|
|
438
|
+
);
|
|
439
|
+
installPackage(
|
|
440
|
+
'@acme/widgets',
|
|
441
|
+
{name: '@acme/widgets', version: '2.0.0'},
|
|
442
|
+
{},
|
|
443
|
+
'NewWidget',
|
|
444
|
+
);
|
|
445
|
+
configure(['widgets-old', '@acme/widgets']);
|
|
446
|
+
|
|
447
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
448
|
+
const owned = (await project.components()).map(component => component.name);
|
|
449
|
+
expect(owned).toContain('OldWidget');
|
|
450
|
+
expect(owned).not.toContain('NewWidget');
|
|
451
|
+
expect(await conflictIssues(project)).toEqual([
|
|
452
|
+
expect.objectContaining({
|
|
453
|
+
package: '@acme/widgets@2.0.0',
|
|
454
|
+
severity: 'warning',
|
|
455
|
+
message: expect.stringContaining(
|
|
456
|
+
'@acme/widgets@2.0.0 and @acme/widgets@1.0.0 (from "widgets-old") both claim',
|
|
457
|
+
),
|
|
458
|
+
}),
|
|
459
|
+
]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('reports an unversioned set-aside package whose spec is the winner\'s name', async () => {
|
|
463
|
+
installPackage(
|
|
464
|
+
'widgets-old',
|
|
465
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
466
|
+
{},
|
|
467
|
+
'OldWidget',
|
|
468
|
+
);
|
|
469
|
+
installPackage('@acme/widgets', {name: '@acme/widgets'}, {}, 'NewWidget');
|
|
470
|
+
configure(['widgets-old', '@acme/widgets']);
|
|
471
|
+
|
|
472
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
473
|
+
expect(await conflictIssues(project)).toEqual([
|
|
474
|
+
expect.objectContaining({
|
|
475
|
+
package: '@acme/widgets',
|
|
476
|
+
message: expect.stringContaining(
|
|
477
|
+
'@acme/widgets@1.0.0 (from "widgets-old") loads first and is used',
|
|
478
|
+
),
|
|
479
|
+
}),
|
|
480
|
+
]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('reports an autolinked alias of the same package at another version', async () => {
|
|
484
|
+
installPackage(
|
|
485
|
+
'@acme/legacy-ui',
|
|
486
|
+
{name: '@acme/ui', version: '0.1.22'},
|
|
487
|
+
{},
|
|
488
|
+
'LegacyButton',
|
|
489
|
+
);
|
|
490
|
+
installPackage(
|
|
491
|
+
'@acme/ui',
|
|
492
|
+
{name: '@acme/ui', version: '1.0.0'},
|
|
493
|
+
{},
|
|
494
|
+
'NewButton',
|
|
495
|
+
);
|
|
496
|
+
fs.rmSync(path.join(tmpDir, 'astryx.config.mjs'));
|
|
497
|
+
fs.writeFileSync(
|
|
498
|
+
path.join(tmpDir, 'package.json'),
|
|
499
|
+
JSON.stringify({
|
|
500
|
+
name: 'consumer',
|
|
501
|
+
dependencies: {
|
|
502
|
+
'@acme/legacy-ui': 'npm:@acme/ui@0.1.22',
|
|
503
|
+
'@acme/ui': '1.0.0',
|
|
504
|
+
},
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
509
|
+
const owned = (await project.components()).map(component => component.name);
|
|
510
|
+
expect(owned).toContain('LegacyButton');
|
|
511
|
+
expect(owned).not.toContain('NewButton');
|
|
512
|
+
expect(await conflictIssues(project)).toEqual([
|
|
513
|
+
expect.objectContaining({
|
|
514
|
+
package: '@acme/ui@1.0.0',
|
|
515
|
+
message: expect.stringContaining(
|
|
516
|
+
'@acme/ui@1.0.0 and @acme/ui@0.1.22 (from "@acme/legacy-ui") both claim',
|
|
517
|
+
),
|
|
518
|
+
}),
|
|
519
|
+
]);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('recomputes conflicts after the authored package replaces its installed copy', async () => {
|
|
523
|
+
installPackage(
|
|
524
|
+
'@acme/widgets',
|
|
525
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
526
|
+
{},
|
|
527
|
+
'Widget',
|
|
528
|
+
);
|
|
529
|
+
installPackage(
|
|
530
|
+
'@acme/renamed',
|
|
531
|
+
{name: '@acme/renamed', version: '2.0.0'},
|
|
532
|
+
{providerId: '@acme/widgets'},
|
|
533
|
+
'RenamedWidget',
|
|
534
|
+
);
|
|
535
|
+
configure(['@acme/widgets', '@acme/renamed']);
|
|
536
|
+
writeLocalPackage(
|
|
537
|
+
{name: '@acme/widgets', version: '3.0.0-dev'},
|
|
538
|
+
{providerId: '@acme/widgets-next'},
|
|
539
|
+
'LocalWidget',
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
543
|
+
expect(
|
|
544
|
+
project.loadedIntegrations.some(
|
|
545
|
+
integration => integration.__providerConflict,
|
|
546
|
+
),
|
|
547
|
+
).toBe(false);
|
|
548
|
+
const owned = (await project.components()).map(component => component.name);
|
|
549
|
+
expect(owned).toEqual(
|
|
550
|
+
expect.arrayContaining(['LocalWidget', 'RenamedWidget']),
|
|
551
|
+
);
|
|
552
|
+
expect(owned).not.toContain('Widget');
|
|
553
|
+
expect(await conflictIssues(project)).toEqual([]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('lets the package being authored win over its installed predecessor', async () => {
|
|
557
|
+
installPackage(
|
|
558
|
+
'@acme/widgets',
|
|
559
|
+
{name: '@acme/widgets', version: '1.0.0'},
|
|
560
|
+
{},
|
|
561
|
+
'Widget',
|
|
562
|
+
);
|
|
563
|
+
fs.rmSync(path.join(tmpDir, 'astryx.config.mjs'));
|
|
564
|
+
writeLocalPackage(
|
|
565
|
+
{
|
|
566
|
+
name: '@acme/renamed',
|
|
567
|
+
version: '2.0.0-dev',
|
|
568
|
+
devDependencies: {'@acme/widgets': '1.0.0'},
|
|
569
|
+
},
|
|
570
|
+
{providerId: '@acme/widgets'},
|
|
571
|
+
'RenamedWidget',
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const project = await Project.load(tmpDir, {fresh: true});
|
|
575
|
+
const owned = (await project.components()).map(component => component.name);
|
|
576
|
+
expect(owned).toContain('RenamedWidget');
|
|
577
|
+
expect(owned).not.toContain('Widget');
|
|
578
|
+
expect(await conflictIssues(project)).toEqual([
|
|
579
|
+
expect.objectContaining({
|
|
580
|
+
package: '@acme/widgets@1.0.0',
|
|
581
|
+
message: expect.stringContaining(
|
|
582
|
+
'@acme/renamed@2.0.0-dev is the package being authored, so it is used',
|
|
583
|
+
),
|
|
584
|
+
}),
|
|
585
|
+
]);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
|
|
210
589
|
describe('local integration self-resolution', () => {
|
|
211
590
|
it('preserves a valid gapReport named export', async () => {
|
|
212
591
|
fs.writeFileSync(
|
|
@@ -225,7 +604,11 @@ export default {};
|
|
|
225
604
|
|
|
226
605
|
const loaded = await loadLocalIntegration(tmpDir, {fresh: true});
|
|
227
606
|
|
|
228
|
-
expect(loaded).toMatchObject({
|
|
607
|
+
expect(loaded).toMatchObject({
|
|
608
|
+
name: '@acme/local',
|
|
609
|
+
providerId: '@acme/local',
|
|
610
|
+
__local: true,
|
|
611
|
+
});
|
|
229
612
|
expect(loaded?.__gapReport?.audience).toBe('internal');
|
|
230
613
|
expect(
|
|
231
614
|
loaded?.__gapReport?.handle(
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Provider-identity conflicts proved against the repository's real
|
|
5
|
+
* integration packages instead of hand-written fixtures: the Lab package as it
|
|
6
|
+
* ships, beside a copy of Charts whose manifest also claims Lab's provider ID.
|
|
7
|
+
*
|
|
8
|
+
* Fixtures live under a repo-local temp dir, not /tmp, because Vite refuses to
|
|
9
|
+
* dynamically import a module from outside the project root.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import {fileURLToPath} from 'node:url';
|
|
16
|
+
import {Project} from '../config/project.mjs';
|
|
17
|
+
import {doctor} from '../../api/doctor/doctor.mjs';
|
|
18
|
+
import {runCli} from '../../test-utils/run-cli.mjs';
|
|
19
|
+
|
|
20
|
+
const REPO = path.resolve(
|
|
21
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
22
|
+
'../../../..',
|
|
23
|
+
);
|
|
24
|
+
const CHARTS = path.join(REPO, 'packages', 'charts');
|
|
25
|
+
const SLOW = 60_000;
|
|
26
|
+
|
|
27
|
+
/** Both packages are named; Lab is named as the one used, Charts as set aside. */
|
|
28
|
+
const WINNER_NAMED =
|
|
29
|
+
/@astryxdesign\/charts@\S+ and @astryxdesign\/lab@\S+ both claim provider ID "@astryxdesign\/lab"\. @astryxdesign\/lab@\S+ loads first and is used; @astryxdesign\/charts@\S+ contributes nothing/u;
|
|
30
|
+
|
|
31
|
+
let consumer;
|
|
32
|
+
let chartsManifest;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
consumer = fs.mkdtempSync(
|
|
36
|
+
path.join(process.cwd(), '.astryx-provider-conflict-'),
|
|
37
|
+
);
|
|
38
|
+
const scope = path.join(consumer, 'node_modules', '@astryxdesign');
|
|
39
|
+
fs.mkdirSync(scope, {recursive: true});
|
|
40
|
+
fs.symlinkSync(
|
|
41
|
+
path.join(REPO, 'packages', 'lab'),
|
|
42
|
+
path.join(scope, 'lab'),
|
|
43
|
+
'dir',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const charts = path.join(scope, 'charts');
|
|
47
|
+
fs.cpSync(CHARTS, charts, {
|
|
48
|
+
recursive: true,
|
|
49
|
+
filter: source => {
|
|
50
|
+
const [top] = path.relative(CHARTS, source).split(path.sep);
|
|
51
|
+
return top !== 'node_modules' && top !== 'dist';
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
chartsManifest = path.join(charts, 'astryx.integration.mjs');
|
|
55
|
+
const shipped = fs.readFileSync(chartsManifest, 'utf8');
|
|
56
|
+
const claimed = shipped.replace(
|
|
57
|
+
'export default {',
|
|
58
|
+
"export default {\n providerId: '@astryxdesign/lab',",
|
|
59
|
+
);
|
|
60
|
+
expect(claimed).not.toBe(shipped);
|
|
61
|
+
fs.writeFileSync(chartsManifest, claimed);
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(
|
|
64
|
+
path.join(consumer, 'package.json'),
|
|
65
|
+
JSON.stringify({name: 'consumer', private: true}),
|
|
66
|
+
);
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(consumer, 'astryx.config.mjs'),
|
|
69
|
+
"export default {integrations: ['@astryxdesign/lab', '@astryxdesign/charts']};\n",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
fs.rmSync(consumer, {recursive: true, force: true});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('provider identity conflicts in the real integration packages', () => {
|
|
78
|
+
it('keeps Lab, sets Charts aside, and names both on every surface', async () => {
|
|
79
|
+
const project = await Project.load(consumer, {fresh: true});
|
|
80
|
+
const components = await project.components();
|
|
81
|
+
expect(
|
|
82
|
+
components.filter(component => component.package === '@astryxdesign/lab')
|
|
83
|
+
.length,
|
|
84
|
+
).toBeGreaterThan(0);
|
|
85
|
+
expect(
|
|
86
|
+
components.some(component => component.package === '@astryxdesign/charts'),
|
|
87
|
+
).toBe(false);
|
|
88
|
+
const conflicts = (await project.issues()).filter(
|
|
89
|
+
issue => issue.code === 'duplicate_provider',
|
|
90
|
+
);
|
|
91
|
+
expect(conflicts).toEqual([
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
package: expect.stringMatching(/^@astryxdesign\/charts@\S+$/u),
|
|
94
|
+
severity: 'warning',
|
|
95
|
+
message: expect.stringMatching(WINNER_NAMED),
|
|
96
|
+
}),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const report = await doctor({cwd: consumer});
|
|
100
|
+
const check = report.data.checks.find(
|
|
101
|
+
candidate => candidate.id === 'provider-identity',
|
|
102
|
+
);
|
|
103
|
+
expect(check?.status).toBe('warn');
|
|
104
|
+
expect(check?.message).toMatch(WINNER_NAMED);
|
|
105
|
+
|
|
106
|
+
const cli = await runCli(['component', '--list'], consumer);
|
|
107
|
+
expect(cli.status).toBe(0);
|
|
108
|
+
expect(cli.stderr).toMatch(WINNER_NAMED);
|
|
109
|
+
}, SLOW);
|
|
110
|
+
|
|
111
|
+
it('loads both real packages without an issue once their IDs differ', async () => {
|
|
112
|
+
fs.copyFileSync(path.join(CHARTS, 'astryx.integration.mjs'), chartsManifest);
|
|
113
|
+
|
|
114
|
+
const project = await Project.load(consumer, {fresh: true});
|
|
115
|
+
const components = await project.components();
|
|
116
|
+
expect(
|
|
117
|
+
components.some(component => component.package === '@astryxdesign/charts'),
|
|
118
|
+
).toBe(true);
|
|
119
|
+
expect(
|
|
120
|
+
(await project.issues()).filter(
|
|
121
|
+
issue => issue.code === 'duplicate_provider',
|
|
122
|
+
),
|
|
123
|
+
).toEqual([]);
|
|
124
|
+
}, SLOW);
|
|
125
|
+
});
|
|
@@ -22,5 +22,7 @@ export function validateLoadedIntegration(loaded: LoadedIntegration): Promise<Is
|
|
|
22
22
|
*/
|
|
23
23
|
/** Stable issue code for an invalid optional agent-doc contribution. */
|
|
24
24
|
export const INVALID_AGENT_DOCS: "invalid_agent_docs";
|
|
25
|
+
/** Stable issue code for a package whose provider ID is already claimed. */
|
|
26
|
+
export const DUPLICATE_PROVIDER: "duplicate_provider";
|
|
25
27
|
export type Issue = import("./issue").AstryxIntegrationIssue;
|
|
26
28
|
export type LoadedIntegration = import("./integrations.mjs").LoadedIntegration;
|
|
@@ -39,6 +39,9 @@ import {discoverIntegrationThemes} from '../discovery/theme-discovery.mjs';
|
|
|
39
39
|
/** Stable issue code for an invalid optional agent-doc contribution. */
|
|
40
40
|
export const INVALID_AGENT_DOCS = 'invalid_agent_docs';
|
|
41
41
|
|
|
42
|
+
/** Stable issue code for a package whose provider ID is already claimed. */
|
|
43
|
+
export const DUPLICATE_PROVIDER = 'duplicate_provider';
|
|
44
|
+
|
|
42
45
|
/** @param {string} code @param {string} message @returns {Issue} */
|
|
43
46
|
export function issueError(code, message) {
|
|
44
47
|
return {code, severity: 'error', message};
|
|
@@ -289,6 +292,13 @@ export async function validateLoadedIntegration(loaded) {
|
|
|
289
292
|
/** @type {Issue[]} */
|
|
290
293
|
const issues = [];
|
|
291
294
|
if (!loaded || typeof loaded !== 'object') return issues;
|
|
295
|
+
// A package set aside for claiming another package's provider ID has no
|
|
296
|
+
// contribution roots to check; the conflict itself is what to report.
|
|
297
|
+
if (loaded.__providerConflict) {
|
|
298
|
+
return [
|
|
299
|
+
issueWarning(DUPLICATE_PROVIDER, loaded.__providerConflict.message),
|
|
300
|
+
];
|
|
301
|
+
}
|
|
292
302
|
// A manifest that threw on import (or failed the schema) carries a load-error
|
|
293
303
|
// marker and no contribution roots, so every check below would find nothing
|
|
294
304
|
// and report a clean integration — which is how a stale manifest used to go
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
15
15
|
import {spawnSync} from 'node:child_process';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as os from 'node:os';
|
|
16
18
|
import * as path from 'node:path';
|
|
17
19
|
import {fileURLToPath} from 'node:url';
|
|
18
20
|
import {
|
|
@@ -27,10 +29,20 @@ import {
|
|
|
27
29
|
} from './json.mjs';
|
|
28
30
|
|
|
29
31
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const CLI = path.resolve(
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const CLI = path.resolve(
|
|
33
|
+
__dirname,
|
|
34
|
+
'..',
|
|
35
|
+
'..',
|
|
36
|
+
'clients',
|
|
37
|
+
'cli',
|
|
38
|
+
'bin',
|
|
39
|
+
'astryx.mjs',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/** @param {string[]} args @param {{cwd?: string}} [options] */
|
|
43
|
+
function runCli(args, {cwd = process.cwd()} = {}) {
|
|
33
44
|
return spawnSync(process.execPath, [CLI, ...args], {
|
|
45
|
+
cwd,
|
|
34
46
|
encoding: 'utf8',
|
|
35
47
|
timeout: 20_000,
|
|
36
48
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -42,7 +54,9 @@ describe('json envelope shape', () => {
|
|
|
42
54
|
let logs;
|
|
43
55
|
beforeEach(() => {
|
|
44
56
|
logs = [];
|
|
45
|
-
vi.spyOn(console, 'log').mockImplementation((...a) =>
|
|
57
|
+
vi.spyOn(console, 'log').mockImplementation((...a) =>
|
|
58
|
+
logs.push(a.join(' ')),
|
|
59
|
+
);
|
|
46
60
|
setJsonMode(false);
|
|
47
61
|
});
|
|
48
62
|
afterEach(() => {
|
|
@@ -80,11 +94,13 @@ describe('json envelope shape', () => {
|
|
|
80
94
|
});
|
|
81
95
|
|
|
82
96
|
it('toErrorEnvelope honors an explicit code argument', () => {
|
|
83
|
-
expect(toErrorEnvelope('boom', undefined, 'ERR_UNKNOWN_COMPONENT')).toEqual(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
expect(toErrorEnvelope('boom', undefined, 'ERR_UNKNOWN_COMPONENT')).toEqual(
|
|
98
|
+
{
|
|
99
|
+
apiVersion: API_VERSION,
|
|
100
|
+
error: 'boom',
|
|
101
|
+
code: 'ERR_UNKNOWN_COMPONENT',
|
|
102
|
+
},
|
|
103
|
+
);
|
|
88
104
|
});
|
|
89
105
|
|
|
90
106
|
it('toErrorEnvelope reads a code carried on a thrown Error', () => {
|
|
@@ -109,8 +125,12 @@ describe('json envelope shape', () => {
|
|
|
109
125
|
// The guard is `Array.isArray(...) && .length`, not a bare `?.length` — a
|
|
110
126
|
// string ("hello".length === 5) or a `{length: n}` object must NOT slip
|
|
111
127
|
// through as `suggestions`, which is contractually a Suggestion[].
|
|
112
|
-
expect(
|
|
113
|
-
|
|
128
|
+
expect(
|
|
129
|
+
'suggestions' in toErrorEnvelope('x', /** @type {any} */ ('hello')),
|
|
130
|
+
).toBe(false);
|
|
131
|
+
expect(
|
|
132
|
+
'suggestions' in toErrorEnvelope('x', /** @type {any} */ ({length: 3})),
|
|
133
|
+
).toBe(false);
|
|
114
134
|
});
|
|
115
135
|
});
|
|
116
136
|
|
|
@@ -119,8 +139,12 @@ describe('stdout discipline (humanLog / humanWarn)', () => {
|
|
|
119
139
|
beforeEach(() => {
|
|
120
140
|
logs = [];
|
|
121
141
|
errs = [];
|
|
122
|
-
vi.spyOn(console, 'log').mockImplementation((...a) =>
|
|
123
|
-
|
|
142
|
+
vi.spyOn(console, 'log').mockImplementation((...a) =>
|
|
143
|
+
logs.push(a.join(' ')),
|
|
144
|
+
);
|
|
145
|
+
vi.spyOn(console, 'error').mockImplementation((...a) =>
|
|
146
|
+
errs.push(a.join(' ')),
|
|
147
|
+
);
|
|
124
148
|
});
|
|
125
149
|
afterEach(() => {
|
|
126
150
|
vi.restoreAllMocks();
|
|
@@ -174,10 +198,15 @@ describe('contract: every --json emission is valid JSON with apiVersion', () =>
|
|
|
174
198
|
});
|
|
175
199
|
|
|
176
200
|
it('init emits its install receipt', () => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-json-contract-'));
|
|
202
|
+
try {
|
|
203
|
+
const r = runCli(['init', '--json'], {cwd});
|
|
204
|
+
const env = JSON.parse(r.stdout);
|
|
205
|
+
expect(env.apiVersion).toBe(API_VERSION);
|
|
206
|
+
expect(env.type).toBe('init.run');
|
|
207
|
+
} finally {
|
|
208
|
+
fs.rmSync(cwd, {recursive: true, force: true});
|
|
209
|
+
}
|
|
181
210
|
});
|
|
182
211
|
|
|
183
212
|
it('supported command (discover) emits clean JSON, no human chatter leak', () => {
|