@astryxdesign/cli 0.7.2-canary.e2909cf → 0.7.2-canary.e4bdd70
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 +15 -14
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +244 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
- package/api/integration/pack-check.mjs +51 -97
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.mjs +8 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/integration/validate-integration.type.d.mts +8 -0
- package/api/integration/validate-integration.type.mjs +12 -1
- package/api/integration/validateIntegration.doc.mjs +17 -3
- package/api/search/search.d.mts +22 -6
- package/api/search/search.domain-priority.test.mjs +93 -18
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +133 -22
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +114 -17
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/styling-overview.doc.mjs +1 -1
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +11 -1
- package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
- package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
- package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
- package/assets/docs/tree/block-template.doc.mjs +8 -20
- package/assets/docs/tree/build-the-template.doc.mjs +1 -1
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
- package/assets/docs/tree/checks.doc.mjs +24 -16
- package/assets/docs/tree/codemods.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +8 -6
- package/assets/docs/tree/component-imports.doc.mjs +10 -2
- package/assets/docs/tree/components.doc.mjs +1 -1
- package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
- package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
- package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
- package/assets/docs/tree/docs.doc.mjs +3 -3
- package/assets/docs/tree/document-the-template.doc.mjs +1 -1
- package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
- package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
- package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
- package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
- package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
- package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
- package/assets/docs/tree/integrations.doc.mjs +8 -0
- package/assets/docs/tree/links.doc.mjs +6 -5
- package/assets/docs/tree/package-and-test.doc.mjs +1 -1
- package/assets/docs/tree/page-template.doc.mjs +4 -8
- package/assets/docs/tree/publishing.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +37 -21
- package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
- package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
- package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
- package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
- package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
- package/assets/docs/tree/single-component.doc.mjs +15 -6
- package/assets/docs/tree/start-a-template.doc.mjs +3 -3
- package/assets/docs/tree/subcomponent.doc.mjs +6 -6
- package/assets/docs/tree/template-assets.doc.mjs +2 -2
- package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
- package/assets/docs/tree/template-fonts.doc.mjs +1 -1
- package/assets/docs/tree/template-icons.doc.mjs +1 -1
- package/assets/docs/tree/template-images-media.doc.mjs +1 -1
- package/assets/docs/tree/template-styles.doc.mjs +2 -2
- package/assets/docs/tree/templates.doc.mjs +1 -1
- package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
- package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
- package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
- package/assets/docs/tree/troubleshooting.doc.mjs +13 -5
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/upgrading.doc.mjs +3 -3
- package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
- package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
- package/assets/docs/tree/versioning.doc.mjs +7 -6
- package/assets/docs/tree/write-the-template-file.doc.mjs +4 -3
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/theme.template.ts +1 -1
- package/authoring/codemod/codemod.doc.mjs +7 -3
- package/authoring/config/config.doc.mjs +9 -6
- package/authoring/config/type.ts +4 -2
- package/authoring/discover/discover.doc.mjs +1 -1
- package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
- package/authoring/doctypes/command/command.doc.mjs +14 -12
- package/authoring/doctypes/component/component.doc.mjs +29 -26
- package/authoring/doctypes/component/type.ts +13 -8
- package/authoring/doctypes/enum/enum.doc.mjs +7 -4
- package/authoring/doctypes/function/function.doc.mjs +9 -8
- package/authoring/doctypes/hook/hook.doc.mjs +5 -4
- package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
- package/authoring/doctypes/reference/reference.doc.mjs +11 -10
- package/authoring/doctypes/schema/schema.doc.mjs +9 -9
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/doctypes/theme/theme.doc.mjs +2 -2
- package/authoring/gap-report/gap-report.doc.mjs +2 -2
- package/authoring/identity/identity.doc.mjs +3 -3
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +12 -4
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
- package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +837 -0
- package/clients/cli/commands/doctor.mjs +190 -0
- package/clients/cli/commands/integration-check.doc.mjs +85 -0
- package/clients/cli/commands/integration-check.test.mjs +267 -0
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +107 -1
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +2 -1
- package/foundation/discovery/component-discovery.d.mts +178 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +659 -62
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -0
- package/foundation/discovery/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +278 -0
- package/foundation/discovery/prop-drift.mjs +2263 -0
- package/foundation/discovery/prop-drift.test.mjs +2219 -0
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
|
@@ -21,6 +21,7 @@ import {discoverOwnedComponents} from '../../../foundation/discovery/component-d
|
|
|
21
21
|
import {discoverBuiltinTopics} from '../../../foundation/discovery/docs-discovery.mjs';
|
|
22
22
|
import {findCoreDir} from '../../../foundation/fs/paths.mjs';
|
|
23
23
|
import {registerDoctor} from './doctor.mjs';
|
|
24
|
+
import {expectRecordsCarry} from '../../../test-utils/text-projection.mjs';
|
|
24
25
|
|
|
25
26
|
let tmpDir;
|
|
26
27
|
let previousCwd;
|
|
@@ -209,6 +210,425 @@ describe('doctor integration — command', () => {
|
|
|
209
210
|
expect(process.exitCode).toBeUndefined();
|
|
210
211
|
});
|
|
211
212
|
|
|
213
|
+
it('validate warns about each doc field that does not match its authoring type, and still exits 0', async () => {
|
|
214
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
215
|
+
process.chdir(tmpDir);
|
|
216
|
+
|
|
217
|
+
await createProgram().parseAsync([
|
|
218
|
+
'node',
|
|
219
|
+
'astryx',
|
|
220
|
+
'doctor',
|
|
221
|
+
'integration',
|
|
222
|
+
'validate',
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const output = logCalls.join('\n');
|
|
226
|
+
expect(output).toContain('[ok] No integration issues found.');
|
|
227
|
+
expect(output).toMatch(
|
|
228
|
+
/Checking 1 doc\(s\) against the authoring types \(TypeScript \d+\.\d+/,
|
|
229
|
+
);
|
|
230
|
+
expect(output).toContain('templates/hero.doc.mjs');
|
|
231
|
+
expect(output).toMatch(/export:\s+default/);
|
|
232
|
+
expect(output).toMatch(/type:\s+IntegrationTemplateDoc/);
|
|
233
|
+
expect(output).toContain(
|
|
234
|
+
'Add `aspectRatio`: IntegrationBlockTemplateDoc requires it.',
|
|
235
|
+
);
|
|
236
|
+
expect(output).toContain(
|
|
237
|
+
'Add `displayName`: IntegrationBlockTemplateDoc requires it.',
|
|
238
|
+
);
|
|
239
|
+
expect(output).toContain(
|
|
240
|
+
'2 doc field(s) in 1 file(s) do not match the authoring types.',
|
|
241
|
+
);
|
|
242
|
+
expect(process.exitCode).toBeUndefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('validate says there is no doc to type-check, rather than asking for TypeScript', async () => {
|
|
246
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-docs-'));
|
|
247
|
+
try {
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(outside, 'package.json'),
|
|
250
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
251
|
+
);
|
|
252
|
+
fs.writeFileSync(
|
|
253
|
+
path.join(outside, 'astryx.integration.mjs'),
|
|
254
|
+
"export default {components: './components'};\n",
|
|
255
|
+
);
|
|
256
|
+
fs.mkdirSync(path.join(outside, 'components'));
|
|
257
|
+
fs.writeFileSync(
|
|
258
|
+
path.join(outside, 'components', 'Widget.tsx'),
|
|
259
|
+
'export function Widget() { return null; }\n',
|
|
260
|
+
);
|
|
261
|
+
process.chdir(outside);
|
|
262
|
+
|
|
263
|
+
await createProgram().parseAsync([
|
|
264
|
+
'node',
|
|
265
|
+
'astryx',
|
|
266
|
+
'doctor',
|
|
267
|
+
'integration',
|
|
268
|
+
'validate',
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const output = logCalls.join('\n');
|
|
272
|
+
expect(output).toContain('[info] No doc files to type-check.');
|
|
273
|
+
expect(output).not.toContain('Docs not type-checked');
|
|
274
|
+
expect(output).not.toContain('against the authoring types');
|
|
275
|
+
} finally {
|
|
276
|
+
process.chdir(previousCwd);
|
|
277
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('validate says the docs matched even when another check failed', async () => {
|
|
282
|
+
writeIntegration({id: 'hero', missingRoot: true});
|
|
283
|
+
fs.writeFileSync(
|
|
284
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
285
|
+
"export default {templates: './missing', components: './components'};\n",
|
|
286
|
+
);
|
|
287
|
+
fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
|
|
288
|
+
fs.writeFileSync(
|
|
289
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
290
|
+
"export default {type: 'component', name: 'AcmeBadge', displayName: 'Badge', usage: {description: 'A badge.'}, props: []};\n",
|
|
291
|
+
);
|
|
292
|
+
fs.writeFileSync(
|
|
293
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
294
|
+
'export function AcmeBadge() { return null; }\n',
|
|
295
|
+
);
|
|
296
|
+
process.chdir(tmpDir);
|
|
297
|
+
|
|
298
|
+
await createProgram().parseAsync([
|
|
299
|
+
'node',
|
|
300
|
+
'astryx',
|
|
301
|
+
'doctor',
|
|
302
|
+
'integration',
|
|
303
|
+
'validate',
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
const output = logCalls.join('\n');
|
|
307
|
+
expect(process.exitCode).toBe(1);
|
|
308
|
+
expect(output).toContain(
|
|
309
|
+
'[info] No doc field differs from its authoring type (1 checked).',
|
|
310
|
+
);
|
|
311
|
+
expect(output).not.toContain('[ok] Every doc');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('validate prints every field of the doc type check that JSON carries', async () => {
|
|
315
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
316
|
+
fs.writeFileSync(
|
|
317
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
318
|
+
"export default {templates: './templates', components: './components'};\n",
|
|
319
|
+
);
|
|
320
|
+
fs.mkdirSync(path.join(tmpDir, 'components'), {recursive: true});
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
323
|
+
"export default {type: 'component', name: 'AcmeBadge', displayName: 5, usage: {description: 'A badge.'}, props: [], keyword: ['x']};\n",
|
|
324
|
+
);
|
|
325
|
+
fs.writeFileSync(
|
|
326
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
327
|
+
'export function AcmeBadge() { return null; }\n',
|
|
328
|
+
);
|
|
329
|
+
fs.writeFileSync(
|
|
330
|
+
path.join(tmpDir, 'components', 'shared.mjs'),
|
|
331
|
+
`export default {type: 'component', name: 'AcmeAgain', displayName: 'Again', usage: {description: 'x'}, props: []};\n`,
|
|
332
|
+
);
|
|
333
|
+
fs.writeFileSync(
|
|
334
|
+
path.join(tmpDir, 'components', 'AcmeAgain.doc.mjs'),
|
|
335
|
+
"export {default} from './shared.mjs';\n",
|
|
336
|
+
);
|
|
337
|
+
fs.writeFileSync(
|
|
338
|
+
path.join(tmpDir, 'components', 'AcmeAgain.tsx'),
|
|
339
|
+
'export function AcmeAgain() { return null; }\n',
|
|
340
|
+
);
|
|
341
|
+
process.chdir(tmpDir);
|
|
342
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
343
|
+
logCalls.length = 0;
|
|
344
|
+
await createProgram().parseAsync([
|
|
345
|
+
'node',
|
|
346
|
+
'astryx',
|
|
347
|
+
...flags,
|
|
348
|
+
'doctor',
|
|
349
|
+
'integration',
|
|
350
|
+
'validate',
|
|
351
|
+
]);
|
|
352
|
+
return logCalls.join('\n');
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const report = JSON.parse(await run(['--json'])).data.docTypes;
|
|
356
|
+
const output = await run([]);
|
|
357
|
+
|
|
358
|
+
expect([...new Set(report.findings.map(f => f.problem))].sort()).toEqual([
|
|
359
|
+
'missing_field',
|
|
360
|
+
'unknown_field',
|
|
361
|
+
'wrong_type',
|
|
362
|
+
]);
|
|
363
|
+
expect(report.unchecked).toHaveLength(1);
|
|
364
|
+
expectRecordsCarry(output, report.findings, ['doc', 'field', 'problem']);
|
|
365
|
+
expectRecordsCarry(output, report.unchecked, ['doc', 'reason']);
|
|
366
|
+
expect(output).toContain(
|
|
367
|
+
`Checking ${report.docs} doc(s) against the authoring types (TypeScript ${report.typescript})`,
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('validate type-checks each doc component discovery serves, in __fixtures__, in a dot-folder or behind a symlink', async () => {
|
|
372
|
+
fs.writeFileSync(
|
|
373
|
+
path.join(tmpDir, 'package.json'),
|
|
374
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
375
|
+
);
|
|
376
|
+
fs.writeFileSync(
|
|
377
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
378
|
+
`export default {components: './components'};\n`,
|
|
379
|
+
);
|
|
380
|
+
for (const [rel, name] of [
|
|
381
|
+
['components/__fixtures__/AcmeFixture.doc.mjs', 'AcmeFixture'],
|
|
382
|
+
['components/.drafts/AcmeDraft.doc.mjs', 'AcmeDraft'],
|
|
383
|
+
['shared/AcmeLinked.doc.mjs', 'AcmeLinked'],
|
|
384
|
+
]) {
|
|
385
|
+
fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
|
|
386
|
+
fs.writeFileSync(
|
|
387
|
+
path.join(tmpDir, rel),
|
|
388
|
+
`export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: [], keyword: []};\n`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
fs.symlinkSync(
|
|
392
|
+
path.join(tmpDir, 'shared', 'AcmeLinked.doc.mjs'),
|
|
393
|
+
path.join(tmpDir, 'components', 'AcmeLinked.doc.mjs'),
|
|
394
|
+
);
|
|
395
|
+
process.chdir(tmpDir);
|
|
396
|
+
|
|
397
|
+
await createProgram().parseAsync([
|
|
398
|
+
'node',
|
|
399
|
+
'astryx',
|
|
400
|
+
'--json',
|
|
401
|
+
'doctor',
|
|
402
|
+
'integration',
|
|
403
|
+
'validate',
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
const {docTypes} = JSON.parse(logCalls.join('\n')).data;
|
|
407
|
+
expect(docTypes).toMatchObject({status: 'checked', docs: 3, unchecked: []});
|
|
408
|
+
expect(
|
|
409
|
+
docTypes.findings.map(
|
|
410
|
+
(/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
|
|
411
|
+
),
|
|
412
|
+
).toEqual([
|
|
413
|
+
'components/__fixtures__/AcmeFixture.doc.mjs keyword unknown_field',
|
|
414
|
+
'components/.drafts/AcmeDraft.doc.mjs keyword unknown_field',
|
|
415
|
+
'components/AcmeLinked.doc.mjs keyword unknown_field',
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('validate type-checks a hidden doc and a second doc for one name, and counts a test fixture', async () => {
|
|
420
|
+
fs.writeFileSync(
|
|
421
|
+
path.join(tmpDir, 'package.json'),
|
|
422
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
423
|
+
);
|
|
424
|
+
fs.writeFileSync(
|
|
425
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
426
|
+
`export default {components: './components'};\n`,
|
|
427
|
+
);
|
|
428
|
+
for (const [rel, name, extra] of [
|
|
429
|
+
['components/AcmeBadge.doc.mjs', 'AcmeBadge', ''],
|
|
430
|
+
['components/again/AcmeBadge.doc.mjs', 'AcmeBadge', ', keyword: []'],
|
|
431
|
+
[
|
|
432
|
+
'components/AcmeHidden.doc.mjs',
|
|
433
|
+
'AcmeHidden',
|
|
434
|
+
', hidden: true, keyword: []',
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
'components/__tests__/AcmeTested.doc.mjs',
|
|
438
|
+
'AcmeTested',
|
|
439
|
+
', keyword: []',
|
|
440
|
+
],
|
|
441
|
+
]) {
|
|
442
|
+
fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
|
|
443
|
+
fs.writeFileSync(
|
|
444
|
+
path.join(tmpDir, rel),
|
|
445
|
+
`export default {type: 'component', name: '${name}', displayName: '${name}', usage: {description: 'Fixture.'}, props: []${extra}};\n`,
|
|
446
|
+
);
|
|
447
|
+
fs.writeFileSync(
|
|
448
|
+
path.join(tmpDir, rel.replace('.doc.mjs', '.tsx')),
|
|
449
|
+
'export default function Fixture() { return null; }\n',
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
process.chdir(tmpDir);
|
|
453
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
454
|
+
logCalls.length = 0;
|
|
455
|
+
await createProgram().parseAsync([
|
|
456
|
+
'node',
|
|
457
|
+
'astryx',
|
|
458
|
+
...flags,
|
|
459
|
+
'doctor',
|
|
460
|
+
'integration',
|
|
461
|
+
'validate',
|
|
462
|
+
]);
|
|
463
|
+
return logCalls.join('\n');
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const {docTypes} = JSON.parse(await run(['--json'])).data;
|
|
467
|
+
const output = await run([]);
|
|
468
|
+
|
|
469
|
+
expect(docTypes).toMatchObject({docs: 3, fixtures: 1, unchecked: []});
|
|
470
|
+
expect(
|
|
471
|
+
docTypes.findings.map(
|
|
472
|
+
(/** @type {any} */ f) => `${f.doc} ${f.field} ${f.problem}`,
|
|
473
|
+
),
|
|
474
|
+
).toEqual([
|
|
475
|
+
'components/AcmeHidden.doc.mjs keyword unknown_field',
|
|
476
|
+
'components/again/AcmeBadge.doc.mjs keyword unknown_field',
|
|
477
|
+
]);
|
|
478
|
+
expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
|
|
479
|
+
expect(output).not.toContain('[ok] Every doc matches');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('validate keeps the fixture count when test fixtures are the only docs', async () => {
|
|
483
|
+
fs.writeFileSync(
|
|
484
|
+
path.join(tmpDir, 'package.json'),
|
|
485
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
486
|
+
);
|
|
487
|
+
fs.writeFileSync(
|
|
488
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
489
|
+
`export default {components: './components'};\n`,
|
|
490
|
+
);
|
|
491
|
+
const rel = 'components/__tests__/AcmeTested.doc.mjs';
|
|
492
|
+
fs.mkdirSync(path.dirname(path.join(tmpDir, rel)), {recursive: true});
|
|
493
|
+
fs.writeFileSync(
|
|
494
|
+
path.join(tmpDir, rel),
|
|
495
|
+
`export default {type: 'component', name: 'AcmeTested', displayName: 'AcmeTested', usage: {description: 'Fixture.'}, props: []};\n`,
|
|
496
|
+
);
|
|
497
|
+
process.chdir(tmpDir);
|
|
498
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
499
|
+
logCalls.length = 0;
|
|
500
|
+
await createProgram().parseAsync([
|
|
501
|
+
'node',
|
|
502
|
+
'astryx',
|
|
503
|
+
...flags,
|
|
504
|
+
'doctor',
|
|
505
|
+
'integration',
|
|
506
|
+
'validate',
|
|
507
|
+
]);
|
|
508
|
+
return logCalls.join('\n');
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const {docTypes} = JSON.parse(await run(['--json'])).data;
|
|
512
|
+
const output = await run([]);
|
|
513
|
+
|
|
514
|
+
expect(docTypes).toMatchObject({docs: 0, fixtures: 1, findings: []});
|
|
515
|
+
expect(output).toContain('[info] No doc files to type-check.');
|
|
516
|
+
expect(output).toContain('[info] 1 test fixture doc(s) not checked.');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("validate keeps main's issue for a root it cannot read, and lists a root with a folder it cannot read", async () => {
|
|
520
|
+
fs.writeFileSync(
|
|
521
|
+
path.join(tmpDir, 'package.json'),
|
|
522
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
523
|
+
);
|
|
524
|
+
fs.writeFileSync(
|
|
525
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
526
|
+
`export default {components: './components', docs: './docs'};\n`,
|
|
527
|
+
);
|
|
528
|
+
fs.mkdirSync(path.join(tmpDir, 'components', 'locked'), {recursive: true});
|
|
529
|
+
fs.mkdirSync(path.join(tmpDir, 'docs'));
|
|
530
|
+
const locked = [
|
|
531
|
+
path.join(tmpDir, 'components', 'locked'),
|
|
532
|
+
path.join(tmpDir, 'docs'),
|
|
533
|
+
];
|
|
534
|
+
for (const dir of locked) fs.chmodSync(dir, 0o000);
|
|
535
|
+
process.chdir(tmpDir);
|
|
536
|
+
try {
|
|
537
|
+
await createProgram().parseAsync([
|
|
538
|
+
'node',
|
|
539
|
+
'astryx',
|
|
540
|
+
'--json',
|
|
541
|
+
'doctor',
|
|
542
|
+
'integration',
|
|
543
|
+
'validate',
|
|
544
|
+
]);
|
|
545
|
+
} finally {
|
|
546
|
+
for (const dir of locked) fs.chmodSync(dir, 0o755);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const {data} = JSON.parse(logCalls.join('\n'));
|
|
550
|
+
expect(data.issues.map((/** @type {any} */ i) => i.code).sort()).toEqual([
|
|
551
|
+
'invalid_component',
|
|
552
|
+
'unreadable_root',
|
|
553
|
+
]);
|
|
554
|
+
expect(data.docTypes.unchecked).toEqual([
|
|
555
|
+
{
|
|
556
|
+
doc: 'components',
|
|
557
|
+
reason: expect.stringMatching(
|
|
558
|
+
/^the components root does not load, so no doc in it was checked: .*EACCES/,
|
|
559
|
+
),
|
|
560
|
+
},
|
|
561
|
+
]);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('validate reports the doc type check as additive JSON', async () => {
|
|
565
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
566
|
+
process.chdir(tmpDir);
|
|
567
|
+
|
|
568
|
+
await createProgram().parseAsync([
|
|
569
|
+
'node',
|
|
570
|
+
'astryx',
|
|
571
|
+
'--json',
|
|
572
|
+
'doctor',
|
|
573
|
+
'integration',
|
|
574
|
+
'validate',
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
const parsed = JSON.parse(logCalls.join('\n'));
|
|
578
|
+
expect(parsed.data.issues).toEqual([]);
|
|
579
|
+
expect(parsed.data.docTypes).toMatchObject({
|
|
580
|
+
status: 'checked',
|
|
581
|
+
docs: 1,
|
|
582
|
+
unchecked: [],
|
|
583
|
+
});
|
|
584
|
+
expect(parsed.data.docTypes.typescript).toMatch(/^\d+\.\d+/);
|
|
585
|
+
expect(parsed.data.docTypes.findings).toEqual([
|
|
586
|
+
expect.objectContaining({
|
|
587
|
+
doc: 'templates/hero.doc.mjs',
|
|
588
|
+
export: 'default',
|
|
589
|
+
type: 'IntegrationTemplateDoc',
|
|
590
|
+
field: 'aspectRatio',
|
|
591
|
+
problem: 'missing_field',
|
|
592
|
+
severity: 'warning',
|
|
593
|
+
}),
|
|
594
|
+
expect.objectContaining({field: 'displayName', problem: 'missing_field'}),
|
|
595
|
+
]);
|
|
596
|
+
expect(process.exitCode).toBeUndefined();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('validate says, in one visible line, that docs were not type-checked when the project has no TypeScript', async () => {
|
|
600
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-no-ts-'));
|
|
601
|
+
const inside = tmpDir;
|
|
602
|
+
try {
|
|
603
|
+
// The fixture helpers write to tmpDir; point it outside the repository,
|
|
604
|
+
// where no TypeScript resolves, and restore it for afterEach.
|
|
605
|
+
tmpDir = outside;
|
|
606
|
+
writeIntegration({id: 'hero', name: 'Hero', type: 'block'});
|
|
607
|
+
tmpDir = inside;
|
|
608
|
+
process.chdir(outside);
|
|
609
|
+
|
|
610
|
+
await createProgram().parseAsync([
|
|
611
|
+
'node',
|
|
612
|
+
'astryx',
|
|
613
|
+
'doctor',
|
|
614
|
+
'integration',
|
|
615
|
+
'validate',
|
|
616
|
+
]);
|
|
617
|
+
|
|
618
|
+
const output = logCalls.join('\n');
|
|
619
|
+
expect(output).toContain(
|
|
620
|
+
'[skip] Docs not type-checked: TypeScript is not installed in this project',
|
|
621
|
+
);
|
|
622
|
+
expect(output).not.toMatch(
|
|
623
|
+
/Checking \d+ doc\(s\) against the authoring types/,
|
|
624
|
+
);
|
|
625
|
+
expect(process.exitCode).toBeUndefined();
|
|
626
|
+
} finally {
|
|
627
|
+
process.chdir(previousCwd);
|
|
628
|
+
fs.rmSync(outside, {recursive: true, force: true});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
212
632
|
it('templates warns on a Core id and writes the exact --package command', async () => {
|
|
213
633
|
const [core] = await discoverCoreTemplates();
|
|
214
634
|
expect(core).toBeDefined();
|
|
@@ -366,6 +786,10 @@ describe('doctor integration — command', () => {
|
|
|
366
786
|
const printed = logCalls.join('\n');
|
|
367
787
|
expect(printed).toContain('core_not_found');
|
|
368
788
|
expect(printed).not.toContain('[ok]');
|
|
789
|
+
// No TypeScript out here either: the props comparison says it skipped.
|
|
790
|
+
expect(printed).toContain(
|
|
791
|
+
'[skip] Props not compared: TypeScript is not installed in this project',
|
|
792
|
+
);
|
|
369
793
|
expect(process.exitCode).toBe(1);
|
|
370
794
|
} finally {
|
|
371
795
|
tmpDir = previousTmp;
|
|
@@ -396,6 +820,354 @@ describe('doctor integration — command', () => {
|
|
|
396
820
|
expect(process.exitCode).toBeUndefined();
|
|
397
821
|
});
|
|
398
822
|
|
|
823
|
+
it('components prints each documented prop that differs from the code, and still exits 0', async () => {
|
|
824
|
+
writeComponentIntegration('AcmeBadge');
|
|
825
|
+
fs.writeFileSync(
|
|
826
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
827
|
+
`export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [{name: 'label', type: 'string', description: 'Text.'}]};\n`,
|
|
828
|
+
);
|
|
829
|
+
fs.writeFileSync(
|
|
830
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
831
|
+
'export function AcmeBadge(props: {label: string}) { return null; }\n',
|
|
832
|
+
);
|
|
833
|
+
process.chdir(tmpDir);
|
|
834
|
+
|
|
835
|
+
await createProgram().parseAsync([
|
|
836
|
+
'node',
|
|
837
|
+
'astryx',
|
|
838
|
+
'doctor',
|
|
839
|
+
'integration',
|
|
840
|
+
'components',
|
|
841
|
+
]);
|
|
842
|
+
|
|
843
|
+
const output = logCalls.join('\n');
|
|
844
|
+
expect(output).toMatch(/^kind:\s+required_mismatch$/m);
|
|
845
|
+
expect(output).toContain(
|
|
846
|
+
'"label" is required in the code, but the doc does not mark it required.',
|
|
847
|
+
);
|
|
848
|
+
expect(output).toContain(
|
|
849
|
+
'1 prop(s) differ between the docs and the code in 1 of 1 component(s) compared (TypeScript',
|
|
850
|
+
);
|
|
851
|
+
expect(output).not.toContain('[ok] Documented prop');
|
|
852
|
+
expect(process.exitCode).toBeUndefined();
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('components compares a hidden doc and a family entry with no record, so their drift is never an [ok]', async () => {
|
|
856
|
+
writeComponentIntegration('AcmeMenu');
|
|
857
|
+
const dir = path.join(tmpDir, 'components');
|
|
858
|
+
fs.writeFileSync(
|
|
859
|
+
path.join(dir, 'AcmeMenu.doc.mjs'),
|
|
860
|
+
`export default {type: 'component', name: 'AcmeMenuRoot', displayName: 'AcmeMenuRoot', description: 'Fixture component.', hiddenComponents: ['AcmeMenuItem'], components: [{name: 'AcmeMenuItem', displayName: 'AcmeMenuItem', description: 'Fixture item.', props: [{name: 'label', type: 'string', description: 'Text.'}]}]};\n`,
|
|
861
|
+
);
|
|
862
|
+
fs.writeFileSync(
|
|
863
|
+
path.join(dir, 'AcmeMenu.tsx'),
|
|
864
|
+
'export function AcmeMenuRoot() { return null; }\nexport function AcmeMenuItem(props: {value?: string}) { return null; }\n',
|
|
865
|
+
);
|
|
866
|
+
fs.writeFileSync(
|
|
867
|
+
path.join(dir, 'AcmeSecret.doc.mjs'),
|
|
868
|
+
`export default {type: 'component', name: 'AcmeSecret', displayName: 'AcmeSecret', description: 'Fixture component.', hidden: true, props: [{name: 'tone', type: 'string', description: 'Tone.'}]};\n`,
|
|
869
|
+
);
|
|
870
|
+
fs.writeFileSync(
|
|
871
|
+
path.join(dir, 'AcmeSecret.tsx'),
|
|
872
|
+
'export function AcmeSecret(props: {size?: number}) { return null; }\n',
|
|
873
|
+
);
|
|
874
|
+
process.chdir(tmpDir);
|
|
875
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
876
|
+
logCalls.length = 0;
|
|
877
|
+
await createProgram().parseAsync([
|
|
878
|
+
'node',
|
|
879
|
+
'astryx',
|
|
880
|
+
...flags,
|
|
881
|
+
'doctor',
|
|
882
|
+
'integration',
|
|
883
|
+
'components',
|
|
884
|
+
]);
|
|
885
|
+
return logCalls.join('\n');
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
const report = JSON.parse(await run(['--json'])).data.propDrift;
|
|
889
|
+
const output = await run([]);
|
|
890
|
+
|
|
891
|
+
expect(
|
|
892
|
+
report.findings.map(f => `${f.component}.${f.prop} ${f.kind}`),
|
|
893
|
+
).toEqual([
|
|
894
|
+
'AcmeMenuItem.label not_in_code',
|
|
895
|
+
'AcmeMenuItem.value not_in_doc',
|
|
896
|
+
'AcmeSecret.size not_in_doc',
|
|
897
|
+
'AcmeSecret.tone not_in_code',
|
|
898
|
+
]);
|
|
899
|
+
expect(output).not.toContain('[ok] Documented prop');
|
|
900
|
+
expect(process.exitCode).toBeUndefined();
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('components lists props a mapped type builds from keys it cannot trace, and says the [ok] leaves them out', async () => {
|
|
904
|
+
writeComponentIntegration('AcmeChip');
|
|
905
|
+
const dir = path.join(tmpDir, 'components');
|
|
906
|
+
fs.writeFileSync(
|
|
907
|
+
path.join(dir, 'AcmeChip.doc.mjs'),
|
|
908
|
+
`export default {type: 'component', name: 'AcmeChip', displayName: 'AcmeChip', description: 'Fixture.', props: [{name: 'label', type: 'string', description: 'Text.'}]};\n`,
|
|
909
|
+
);
|
|
910
|
+
fs.writeFileSync(
|
|
911
|
+
path.join(dir, 'AcmeChip.tsx'),
|
|
912
|
+
"export function AcmeChip(props: {label?: string} & {[K in `on${Capitalize<'open'>}`]?: () => void}) { return null; }\n",
|
|
913
|
+
);
|
|
914
|
+
process.chdir(tmpDir);
|
|
915
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
916
|
+
logCalls.length = 0;
|
|
917
|
+
await createProgram().parseAsync([
|
|
918
|
+
'node',
|
|
919
|
+
'astryx',
|
|
920
|
+
...flags,
|
|
921
|
+
'doctor',
|
|
922
|
+
'integration',
|
|
923
|
+
'components',
|
|
924
|
+
]);
|
|
925
|
+
return logCalls.join('\n');
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const {propsNotChecked} = JSON.parse(await run(['--json'])).data.propDrift;
|
|
929
|
+
const output = await run([]);
|
|
930
|
+
|
|
931
|
+
expect(propsNotChecked).toEqual([
|
|
932
|
+
expect.objectContaining({component: 'AcmeChip', props: ['onOpen']}),
|
|
933
|
+
]);
|
|
934
|
+
expect(output).toContain(
|
|
935
|
+
'[info] Props not checked for a doc entry in 1 component(s):',
|
|
936
|
+
);
|
|
937
|
+
expect(output).toContain(
|
|
938
|
+
'in 1 component(s), apart from the props not checked (TypeScript',
|
|
939
|
+
);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('components compares the entries of a family doc whose own source is missing', async () => {
|
|
943
|
+
writeComponentIntegration('AcmeTabs');
|
|
944
|
+
const dir = path.join(tmpDir, 'components');
|
|
945
|
+
const entry = (/** @type {string} */ name, /** @type {string} */ prop) =>
|
|
946
|
+
`{name: '${name}', displayName: '${name}', description: 'Fixture.', props: [{name: '${prop}', type: 'string', description: 'Text.'}]}`;
|
|
947
|
+
fs.writeFileSync(
|
|
948
|
+
path.join(dir, 'AcmeTabs.doc.mjs'),
|
|
949
|
+
`export default {type: 'component', name: 'AcmeTabs', displayName: 'AcmeTabs', description: 'Fixture.', props: [], hiddenComponents: ['AcmeTabsPanel'], components: [${entry('AcmeTabsItem', 'label')}, ${entry('AcmeTabsPanel', 'ghost')}]};\n`,
|
|
950
|
+
);
|
|
951
|
+
fs.rmSync(path.join(dir, 'AcmeTabs.tsx'), {force: true});
|
|
952
|
+
// The package's entry exports the members, so discovery records the
|
|
953
|
+
// listed one, though the family's own file is missing.
|
|
954
|
+
for (const name of ['AcmeTabsItem', 'AcmeTabsPanel']) {
|
|
955
|
+
fs.writeFileSync(
|
|
956
|
+
path.join(dir, `${name}.tsx`),
|
|
957
|
+
`export function ${name}(props: {label?: string; open?: boolean}) { return null; }\n`,
|
|
958
|
+
);
|
|
959
|
+
fs.appendFileSync(
|
|
960
|
+
path.join(tmpDir, 'index.ts'),
|
|
961
|
+
`export {${name}} from './components/${name}';\n`,
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
process.chdir(tmpDir);
|
|
965
|
+
|
|
966
|
+
await createProgram().parseAsync([
|
|
967
|
+
'node',
|
|
968
|
+
'astryx',
|
|
969
|
+
'--json',
|
|
970
|
+
'doctor',
|
|
971
|
+
'integration',
|
|
972
|
+
'components',
|
|
973
|
+
]);
|
|
974
|
+
const report = JSON.parse(logCalls.join('\n')).data.propDrift;
|
|
975
|
+
|
|
976
|
+
expect(
|
|
977
|
+
report.findings.map(
|
|
978
|
+
(/** @type {any} */ f) => `${f.component}.${f.prop} ${f.kind}`,
|
|
979
|
+
),
|
|
980
|
+
).toEqual([
|
|
981
|
+
'AcmeTabsItem.open not_in_doc',
|
|
982
|
+
'AcmeTabsPanel.ghost not_in_code',
|
|
983
|
+
'AcmeTabsPanel.label not_in_doc',
|
|
984
|
+
'AcmeTabsPanel.open not_in_doc',
|
|
985
|
+
]);
|
|
986
|
+
expect(report.unchecked).toEqual([
|
|
987
|
+
{
|
|
988
|
+
component: 'AcmeTabs',
|
|
989
|
+
doc: 'components/AcmeTabs.doc.mjs',
|
|
990
|
+
reason: 'no source file for AcmeTabs',
|
|
991
|
+
},
|
|
992
|
+
]);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it('components counts two docs of one name as two components, and compares a .doc.ts beside a .doc.mjs', async () => {
|
|
996
|
+
writeComponentIntegration('AcmeTwin');
|
|
997
|
+
const dir = path.join(tmpDir, 'components');
|
|
998
|
+
const doc = (/** @type {string} */ name, /** @type {string} */ prop) =>
|
|
999
|
+
`export default {type: 'component', name: '${name}', displayName: '${name}', description: 'Fixture.', props: [{name: '${prop}', type: 'string', description: 'Text.'}]};\n`;
|
|
1000
|
+
fs.writeFileSync(
|
|
1001
|
+
path.join(dir, 'AcmeTwin.doc.mjs'),
|
|
1002
|
+
doc('AcmeTwin', 'tone'),
|
|
1003
|
+
);
|
|
1004
|
+
fs.writeFileSync(
|
|
1005
|
+
path.join(dir, 'AcmeTwin.doc.ts'),
|
|
1006
|
+
doc('AcmeTwin', 'size'),
|
|
1007
|
+
);
|
|
1008
|
+
fs.writeFileSync(
|
|
1009
|
+
path.join(dir, 'AcmeTwin.tsx'),
|
|
1010
|
+
'export function AcmeTwin(props: {label?: string}) { return null; }\n',
|
|
1011
|
+
);
|
|
1012
|
+
for (const folder of ['a', 'b']) {
|
|
1013
|
+
fs.mkdirSync(path.join(dir, folder));
|
|
1014
|
+
fs.writeFileSync(
|
|
1015
|
+
path.join(dir, folder, 'AcmeX.doc.mjs'),
|
|
1016
|
+
doc('AcmeX', folder),
|
|
1017
|
+
);
|
|
1018
|
+
fs.writeFileSync(
|
|
1019
|
+
path.join(dir, folder, 'AcmeX.tsx'),
|
|
1020
|
+
'export function AcmeX() { return null; }\n',
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
process.chdir(tmpDir);
|
|
1024
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
1025
|
+
logCalls.length = 0;
|
|
1026
|
+
await createProgram().parseAsync([
|
|
1027
|
+
'node',
|
|
1028
|
+
'astryx',
|
|
1029
|
+
...flags,
|
|
1030
|
+
'doctor',
|
|
1031
|
+
'integration',
|
|
1032
|
+
'components',
|
|
1033
|
+
]);
|
|
1034
|
+
return logCalls.join('\n');
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const report = JSON.parse(await run(['--json'])).data.propDrift;
|
|
1038
|
+
const output = await run([]);
|
|
1039
|
+
|
|
1040
|
+
expect(
|
|
1041
|
+
report.findings
|
|
1042
|
+
.filter(f => f.kind === 'not_in_code')
|
|
1043
|
+
.map(f => `${f.doc} ${f.prop}`)
|
|
1044
|
+
.sort(),
|
|
1045
|
+
).toEqual([
|
|
1046
|
+
'components/AcmeTwin.doc.mjs tone',
|
|
1047
|
+
'components/AcmeTwin.doc.ts size',
|
|
1048
|
+
'components/a/AcmeX.doc.mjs a',
|
|
1049
|
+
'components/b/AcmeX.doc.mjs b',
|
|
1050
|
+
]);
|
|
1051
|
+
expect(output).toMatch(/ in 4 of 4 component\(s\) compared /);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('components prints every field of the props comparison that JSON carries', async () => {
|
|
1055
|
+
writeComponentIntegration('AcmeBadge');
|
|
1056
|
+
fs.writeFileSync(
|
|
1057
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
1058
|
+
`export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [${[
|
|
1059
|
+
"{name: 'label', type: 'string', description: 'Text.'}",
|
|
1060
|
+
"{name: 'elevation', type: 'number', description: 'Shadow.'}",
|
|
1061
|
+
"{name: 'size', type: 'string', description: 'Size.'}",
|
|
1062
|
+
"{name: 'meta', type: '{id: string; extra?: number}', description: 'Data.'}",
|
|
1063
|
+
].join(', ')}]};\n`,
|
|
1064
|
+
);
|
|
1065
|
+
// `key` is a prop a doc never needs, though the component declares it.
|
|
1066
|
+
fs.writeFileSync(
|
|
1067
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
1068
|
+
"export function AcmeBadge(props: {label: string; tone?: 'neutral' | 'info'; size?: number; meta?: {id: string}; key?: string}) { return null; }\n",
|
|
1069
|
+
);
|
|
1070
|
+
fs.writeFileSync(
|
|
1071
|
+
path.join(tmpDir, 'components', 'AcmeLonely.doc.mjs'),
|
|
1072
|
+
`export default {type: 'component', name: 'AcmeLonely', displayName: 'AcmeLonely', description: 'Fixture component.', props: []};\n`,
|
|
1073
|
+
);
|
|
1074
|
+
fs.writeFileSync(
|
|
1075
|
+
path.join(tmpDir, 'components', 'AcmeLonely.tsx'),
|
|
1076
|
+
'export function Other() { return null; }\n',
|
|
1077
|
+
);
|
|
1078
|
+
process.chdir(tmpDir);
|
|
1079
|
+
const run = async (/** @type {string[]} */ flags) => {
|
|
1080
|
+
logCalls.length = 0;
|
|
1081
|
+
await createProgram().parseAsync([
|
|
1082
|
+
'node',
|
|
1083
|
+
'astryx',
|
|
1084
|
+
...flags,
|
|
1085
|
+
'doctor',
|
|
1086
|
+
'integration',
|
|
1087
|
+
'components',
|
|
1088
|
+
]);
|
|
1089
|
+
return logCalls.join('\n');
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
const report = JSON.parse(await run(['--json'])).data.propDrift;
|
|
1093
|
+
const output = await run([]);
|
|
1094
|
+
|
|
1095
|
+
expect(report.findings.map(f => `${f.prop} ${f.kind}`)).toEqual([
|
|
1096
|
+
'elevation not_in_code',
|
|
1097
|
+
'label required_mismatch',
|
|
1098
|
+
'size type_mismatch',
|
|
1099
|
+
'tone not_in_doc',
|
|
1100
|
+
]);
|
|
1101
|
+
expectRecordsCarry(output, report.findings, ['component', 'prop', 'kind']);
|
|
1102
|
+
expectRecordsCarry(output, report.unchecked, ['component', 'reason']);
|
|
1103
|
+
expectRecordsCarry(output, report.typesNotCompared, [
|
|
1104
|
+
'component',
|
|
1105
|
+
'reason',
|
|
1106
|
+
'props',
|
|
1107
|
+
]);
|
|
1108
|
+
expect(report.typesNotCompared).toHaveLength(1);
|
|
1109
|
+
expect(output).toContain(
|
|
1110
|
+
`Documented prop types judged: ${report.typesCompared}; not compared: 1.`,
|
|
1111
|
+
);
|
|
1112
|
+
// The response-type reference, which the README's JSON table comes from,
|
|
1113
|
+
// names every field too.
|
|
1114
|
+
const {doc: responseTypes} =
|
|
1115
|
+
await import('../../../foundation/response/response-types.doc.mjs');
|
|
1116
|
+
const member = responseTypes.members.find(
|
|
1117
|
+
m => m.value === 'integration.component-conflicts',
|
|
1118
|
+
);
|
|
1119
|
+
for (const field of Object.keys(report)) {
|
|
1120
|
+
expect(member?.description).toMatch(new RegExp(`\\b${field}\\b`));
|
|
1121
|
+
}
|
|
1122
|
+
expect(output).toContain(
|
|
1123
|
+
`in 1 of ${report.components} component(s) compared (TypeScript ${report.typescript}).`,
|
|
1124
|
+
);
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it('components says documented props match when they do', async () => {
|
|
1128
|
+
writeComponentIntegration('AcmeOnlyWidget');
|
|
1129
|
+
process.chdir(tmpDir);
|
|
1130
|
+
|
|
1131
|
+
await createProgram().parseAsync([
|
|
1132
|
+
'node',
|
|
1133
|
+
'astryx',
|
|
1134
|
+
'doctor',
|
|
1135
|
+
'integration',
|
|
1136
|
+
'components',
|
|
1137
|
+
]);
|
|
1138
|
+
|
|
1139
|
+
const output = logCalls.join('\n');
|
|
1140
|
+
expect(output).toMatch(
|
|
1141
|
+
/\[ok\] Documented prop names and required flags match the code in 1 component\(s\) \(TypeScript \d/,
|
|
1142
|
+
);
|
|
1143
|
+
expect(output).toMatch(
|
|
1144
|
+
/\[info\] Documented prop types judged: \d+; not compared: 0\./,
|
|
1145
|
+
);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it('components prints no [ok] for props it could not compare', async () => {
|
|
1149
|
+
writeComponentIntegration('AcmeLonely');
|
|
1150
|
+
fs.writeFileSync(
|
|
1151
|
+
path.join(tmpDir, 'components', 'AcmeLonely.tsx'),
|
|
1152
|
+
'export function Other() { return null; }\n',
|
|
1153
|
+
);
|
|
1154
|
+
process.chdir(tmpDir);
|
|
1155
|
+
|
|
1156
|
+
await createProgram().parseAsync([
|
|
1157
|
+
'node',
|
|
1158
|
+
'astryx',
|
|
1159
|
+
'doctor',
|
|
1160
|
+
'integration',
|
|
1161
|
+
'components',
|
|
1162
|
+
]);
|
|
1163
|
+
|
|
1164
|
+
const output = logCalls.join('\n');
|
|
1165
|
+
expect(output).toContain(
|
|
1166
|
+
'[info] Props not compared for 1 documented component(s):',
|
|
1167
|
+
);
|
|
1168
|
+
expect(output).not.toContain('[ok] Documented prop');
|
|
1169
|
+
});
|
|
1170
|
+
|
|
399
1171
|
it('components exposes conflicts as structured JSON warnings', async () => {
|
|
400
1172
|
const coreDir = findCoreDir(tmpDir);
|
|
401
1173
|
expect(coreDir).not.toBeNull();
|
|
@@ -425,6 +1197,71 @@ describe('doctor integration — command', () => {
|
|
|
425
1197
|
expect(process.exitCode).toBeUndefined();
|
|
426
1198
|
});
|
|
427
1199
|
|
|
1200
|
+
it('docs resolves a link to a component or a template, its own or Core’s, and still flags one that names no doc', async () => {
|
|
1201
|
+
const page = (await discoverCoreTemplates()).find(
|
|
1202
|
+
template => template.type === 'page',
|
|
1203
|
+
);
|
|
1204
|
+
expect(page).toBeDefined();
|
|
1205
|
+
fs.writeFileSync(
|
|
1206
|
+
path.join(tmpDir, 'package.json'),
|
|
1207
|
+
JSON.stringify({name: '@acme/widgets', version: '1.2.3'}),
|
|
1208
|
+
);
|
|
1209
|
+
fs.writeFileSync(
|
|
1210
|
+
path.join(tmpDir, 'astryx.integration.mjs'),
|
|
1211
|
+
"export default {components: './components', templates: './templates', docs: './docs'};\n",
|
|
1212
|
+
);
|
|
1213
|
+
for (const dir of ['components', 'templates', 'docs']) {
|
|
1214
|
+
fs.mkdirSync(path.join(tmpDir, dir), {recursive: true});
|
|
1215
|
+
}
|
|
1216
|
+
fs.writeFileSync(
|
|
1217
|
+
path.join(tmpDir, 'components', 'AcmeButton.doc.mjs'),
|
|
1218
|
+
"export default {type: 'component', name: 'AcmeButton', displayName: 'Acme Button', description: 'A button.', props: []};\n",
|
|
1219
|
+
);
|
|
1220
|
+
fs.writeFileSync(
|
|
1221
|
+
path.join(tmpDir, 'components', 'AcmeButton.tsx'),
|
|
1222
|
+
'export function AcmeButton() { return null; }\n',
|
|
1223
|
+
);
|
|
1224
|
+
fs.writeFileSync(
|
|
1225
|
+
path.join(tmpDir, 'templates', 'acme-card.doc.mjs'),
|
|
1226
|
+
"export default {type: 'block', name: 'Acme card', description: 'A card.'};\n",
|
|
1227
|
+
);
|
|
1228
|
+
fs.writeFileSync(
|
|
1229
|
+
path.join(tmpDir, 'templates', 'acme-card.tsx'),
|
|
1230
|
+
'export default function Fixture() { return null; }\n',
|
|
1231
|
+
);
|
|
1232
|
+
const text =
|
|
1233
|
+
'Start with {@link component:AcmeButton} or {@link block:acme-card}, ' +
|
|
1234
|
+
'built on {@link @astryxdesign/core:component:Button} and ' +
|
|
1235
|
+
`{@link @astryxdesign/core:page:${page?.dirName}}. Not {@link component:AcmeMissing}.`;
|
|
1236
|
+
fs.writeFileSync(
|
|
1237
|
+
path.join(tmpDir, 'docs', 'using-widgets.doc.mjs'),
|
|
1238
|
+
`export default ${JSON.stringify({
|
|
1239
|
+
type: 'generic',
|
|
1240
|
+
name: 'using-widgets',
|
|
1241
|
+
title: 'Using widgets',
|
|
1242
|
+
description: 'Fixture topic.',
|
|
1243
|
+
sections: [{title: 'Overview', content: [{type: 'prose', text}]}],
|
|
1244
|
+
})};\n`,
|
|
1245
|
+
);
|
|
1246
|
+
process.chdir(tmpDir);
|
|
1247
|
+
|
|
1248
|
+
await createProgram().parseAsync([
|
|
1249
|
+
'node',
|
|
1250
|
+
'astryx',
|
|
1251
|
+
'--json',
|
|
1252
|
+
'doctor',
|
|
1253
|
+
'integration',
|
|
1254
|
+
'docs',
|
|
1255
|
+
]);
|
|
1256
|
+
|
|
1257
|
+
const parsed = JSON.parse(logCalls.join('\n'));
|
|
1258
|
+
const unresolved = parsed.data.issues
|
|
1259
|
+
.filter(issue => issue.code === 'invalid_doc_graph')
|
|
1260
|
+
.map(issue => issue.message);
|
|
1261
|
+
expect(unresolved).toHaveLength(1);
|
|
1262
|
+
expect(unresolved[0]).toContain('"component:AcmeMissing" names no doc');
|
|
1263
|
+
});
|
|
1264
|
+
|
|
428
1265
|
it('docs reports an intentional replacement as info', async () => {
|
|
429
1266
|
const [coreTopic] = Object.keys(discoverBuiltinTopics());
|
|
430
1267
|
writeDocIntegration({name: 'acme-setup', replaces: coreTopic});
|