@astryxdesign/cli 0.7.2-canary.e26723d → 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 +9 -8
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +37 -0
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/integration/add-contribution.mjs +1 -1
- package/api/integration/add-contribution.test.mjs +4 -2
- 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 +19 -4
- package/api/integration/validate-integration.mjs +8 -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 +13 -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 +33 -9
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +110 -13
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/assets/docs/styling-overview.doc.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +1 -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 +51 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
- package/assets/docs/tree/checks.doc.mjs +21 -13
- package/assets/docs/tree/codemods.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +3 -5
- package/assets/docs/tree/component-imports.doc.mjs +2 -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 +1 -1
- 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 +5 -4
- 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 +12 -4
- 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 +4 -4
- 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 +6 -6
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -2
- 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 +28 -25
- package/authoring/doctypes/component/type.ts +7 -5
- 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.mjs +1 -1
- package/authoring/doctypes/template/template.doc.mjs +3 -3
- 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/integration/integration.doc.mjs +4 -3
- package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
- package/clients/cli/commands/doctor-integration-components.doc.mjs +8 -3
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +447 -2
- package/clients/cli/commands/doctor.mjs +116 -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/discovery/authoring-self-docs.mjs +1 -1
- package/foundation/discovery/doc-types.mjs +2 -2
- 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/prop-drift.d.mts +61 -3
- package/foundation/discovery/prop-drift.mjs +559 -61
- package/foundation/discovery/prop-drift.test.mjs +1032 -10
- package/foundation/doc-compiler/doc-loads.test.mjs +2 -0
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file FunctionDoc for `integrationCheck()` / `astryx integration check`.
|
|
5
|
+
* Colocated with the API function it documents; the shape source of truth stays
|
|
6
|
+
* in `integration-check.type.mjs`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
|
|
10
|
+
export const doc = {
|
|
11
|
+
type: 'function',
|
|
12
|
+
kind: 'api',
|
|
13
|
+
name: 'integrationCheck',
|
|
14
|
+
namespace: 'cli/api',
|
|
15
|
+
displayName: 'integrationCheck()',
|
|
16
|
+
summary:
|
|
17
|
+
'Run every source check against the integration package in the current folder, as its build does: validate, components, templates, and docs.',
|
|
18
|
+
description:
|
|
19
|
+
'Runs `validateIntegration`, `integrationComponentConflicts`, ' +
|
|
20
|
+
'`integrationTemplateConflicts`, and `integrationDocConflicts` against the ' +
|
|
21
|
+
'integration package in `options.cwd`, and counts each result the way that ' +
|
|
22
|
+
"check's own subcommand exits. The run fails when any of the four would fail on " +
|
|
23
|
+
'its own. With `strict` it also fails on any warning and on any check that did ' +
|
|
24
|
+
'not run: a package with no integration manifest, or a comparison a check ' +
|
|
25
|
+
"carries that was skipped, such as one that needs the project's TypeScript. " +
|
|
26
|
+
'`strict` adds no check: it reports those warnings and skipped work again, as ' +
|
|
27
|
+
"errors in the checks' own issue format. " +
|
|
28
|
+
'A new check, or a new failure code, joins only in a minor release, named in the release notes.',
|
|
29
|
+
importPath: '@astryxdesign/cli/api',
|
|
30
|
+
signature:
|
|
31
|
+
'integrationCheck(options?: IntegrationCheckOptions): Promise<IntegrationCheckCommandResponse>',
|
|
32
|
+
keywords: ['integration', 'check', 'build', 'strict', 'validate', 'doctor'],
|
|
33
|
+
params: [
|
|
34
|
+
{
|
|
35
|
+
name: 'options.cwd',
|
|
36
|
+
type: 'string',
|
|
37
|
+
default: 'process.cwd()',
|
|
38
|
+
description: 'The integration package folder to check.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'options.strict',
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
default: 'false',
|
|
44
|
+
description:
|
|
45
|
+
'Also fail on any warning and on any check that did not run, not only on errors.',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
returns: [
|
|
49
|
+
{
|
|
50
|
+
type: 'integration.check',
|
|
51
|
+
description:
|
|
52
|
+
"`data.validated` (false when no integration manifest was found, so nothing was checked), `data.name` and `data.version` of the package, `data.strict`, and `data.checks`, one entry per check in the order they run (`validate`, `components`, `templates`, `docs`), each with `check`, `errors` (what makes that check's own subcommand exit 1), `warnings`, `skipped` (why work did not run), `failed` (whether that subcommand exits 1), and `response`, the envelope the check returns on its own. The totals are `data.errors`, `data.warnings`, and `data.skipped` (each reason prefixed with its check); an issue every check starts from, such as one about the manifest, counts once, and every other finding counts on its own. `data.unchecked` counts the items the checks list as not compared, which never fail it. With `strict`, `data.failures` lists each warning and each piece of skipped work once again as an error in the checks' own issue format ({check, severity: `error`, code, message, component, prop, fix, file, line}, each where it applies); it is empty without `strict`. `data.failed` decides the exit code.",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
examples: [
|
|
56
|
+
{
|
|
57
|
+
label: 'Check the local package',
|
|
58
|
+
code: 'const r = await integrationCheck();',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: 'Fail on warnings too',
|
|
62
|
+
code: 'const {data} = await integrationCheck({strict: true});\nif (data.failed) process.exitCode = 1;',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
command: 'integration check',
|
|
66
|
+
related: [
|
|
67
|
+
'validateIntegration',
|
|
68
|
+
'integrationComponentConflicts',
|
|
69
|
+
'integrationTemplateConflicts',
|
|
70
|
+
'integrationDocConflicts',
|
|
71
|
+
'integrationPackCheck',
|
|
72
|
+
],
|
|
73
|
+
};
|
|
@@ -19,9 +19,24 @@ export const doc = {
|
|
|
19
19
|
'declares the CLI range that turns replacement on and warnings otherwise. ' +
|
|
20
20
|
"Each component doc's props are compared with the component's TypeScript props, " +
|
|
21
21
|
"read with the project's own `typescript`: a documented prop the code lacks, a " +
|
|
22
|
-
'prop the package declares that the doc omits,
|
|
23
|
-
'
|
|
24
|
-
'
|
|
22
|
+
'prop the package declares that the doc omits, a wrong `required`, or a type ' +
|
|
23
|
+
'that does not match. A type is judged only when it reads exactly: the same ' +
|
|
24
|
+
'text as the source, once whitespace, quotes and union order are set aside (exactly, if it contains `readonly`), ' +
|
|
25
|
+
'or primitives, literals, arrays and unions of those, or a name for one, on ' +
|
|
26
|
+
'both sides, a field such as `status.type` included, and for a union of ' +
|
|
27
|
+
"props types, the type of any one member. A field the prop's variants give " +
|
|
28
|
+
"different types is listed when the doc gives only some variants' types. A " +
|
|
29
|
+
'mutable array type matches a readonly one, because callers can pass it; a ' +
|
|
30
|
+
'readonly one documented for a mutable array is a mismatch. ' +
|
|
31
|
+
'`Name = …` with a union after it ' +
|
|
32
|
+
"must match in both parts; one value after it (`Size = 'md'`) is read as a " +
|
|
33
|
+
"default. A name only TypeScript's own libraries declare is not resolved, and " +
|
|
34
|
+
'`React.X` resolves through React where the prop is declared. Every ' +
|
|
35
|
+
'other type is listed as not compared, never reported, and so is a name an ' +
|
|
36
|
+
'index signature takes unless exactly one the package declares takes it; ' +
|
|
37
|
+
'with a TypeScript older than 4.8 no type is judged. ' +
|
|
38
|
+
'Those findings are warnings in `propDrift`; without TypeScript it is ' +
|
|
39
|
+
'`skipped` with a reason.',
|
|
25
40
|
importPath: '@astryxdesign/cli/api',
|
|
26
41
|
signature:
|
|
27
42
|
'integrationComponentConflicts(pkg?: string, options?: IntegrationAuthoringOptions): Promise<IntegrationComponentConflictResponse>',
|
|
@@ -42,7 +57,7 @@ export const doc = {
|
|
|
42
57
|
{
|
|
43
58
|
type: 'integration.component-conflicts',
|
|
44
59
|
description:
|
|
45
|
-
'`validated` (false when no integration manifest was found, so nothing was inspected), integration identity, structural and replaces issues, and Core component-name conflicts with package-qualified commands. An active replacement that keeps the Core name it replaces is not a conflict. When the manifest loads and declares a components root, `propDrift` holds the props comparison: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `components` compared, `findings` ({component, prop, kind: `not_in_code` | `not_in_doc` | `required_mismatch`, severity: `warning`, message, doc, source, docType?, codeType?, docRequired?, codeRequired?}), `unchecked` ({component, doc, reason}), and `propsNotChecked` when a mapped type builds props from keys the check cannot trace to the package ({component, doc, reason, props}: no warning is drawn for those props).',
|
|
60
|
+
'`validated` (false when no integration manifest was found, so nothing was inspected), integration identity, structural and replaces issues, and Core component-name conflicts with package-qualified commands. An active replacement that keeps the Core name it replaces is not a conflict. When the manifest loads and declares a components root, `propDrift` holds the props comparison: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `components` compared, `typesCompared` (documented prop types judged), `typesSkipped` (why none was, with a TypeScript older than 4.8), `findings` ({component, prop, kind: `not_in_code` | `not_in_doc` | `required_mismatch` | `type_mismatch`, severity: `warning`, message, doc, source, docType?, codeType?, docRequired?, codeRequired?}), `unchecked` ({component, doc, reason}), `typesNotCompared` ({component, doc, reason, props}: every other documented prop type, grouped by why it was not judged: not a form the check judges exactly (a doc type that does not parse, or a prop a mapped type builds from keys the check cannot trace, included), a field the prop\'s variants give different types, a standard prop such as `className`, or a prop declared outside the package), and `propsNotChecked` when a mapped type builds props from keys the check cannot trace to the package ({component, doc, reason, props}: no warning is drawn for those props).',
|
|
46
61
|
},
|
|
47
62
|
],
|
|
48
63
|
examples: [
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import * as fs from 'node:fs';
|
|
35
35
|
import * as path from 'node:path';
|
|
36
36
|
import {assertWithin} from '../../foundation/fs/path-safety.mjs';
|
|
37
|
+
import {integrationDocTypes} from '../docs/_adapter.mjs';
|
|
37
38
|
import {
|
|
38
39
|
THEME_SLUG_RE,
|
|
39
40
|
unreadThemeFolders,
|
|
@@ -461,6 +462,12 @@ export async function validateIntegration(pkg, options = {}) {
|
|
|
461
462
|
const result = pkg
|
|
462
463
|
? await validateInstalledIntegration(pkg, cwd)
|
|
463
464
|
: await validateLocalIntegration(cwd);
|
|
465
|
+
// Each doc the package ships, against the published authoring type for its
|
|
466
|
+
// kind, read with the project's own compiler. Findings are warnings only.
|
|
467
|
+
const integration = result.found ? result.integration : undefined;
|
|
468
|
+
const docTypes = integration
|
|
469
|
+
? await integrationDocTypes(integration, cwd)
|
|
470
|
+
: undefined;
|
|
464
471
|
return {
|
|
465
472
|
type: 'integration.validate',
|
|
466
473
|
data: {
|
|
@@ -472,6 +479,7 @@ export async function validateIntegration(pkg, options = {}) {
|
|
|
472
479
|
issues: result.reason === 'no_package_json'
|
|
473
480
|
? [{code: 'no_package', severity: 'error', message: 'No package.json found. Run this command inside a package directory.'}]
|
|
474
481
|
: result.issues,
|
|
482
|
+
...(docTypes ? {docTypes} : {}),
|
|
475
483
|
},
|
|
476
484
|
};
|
|
477
485
|
}
|
|
@@ -39,5 +39,13 @@ export type ValidateIntegrationResponse = {
|
|
|
39
39
|
name: string | null;
|
|
40
40
|
version: string | null;
|
|
41
41
|
issues: import("../../foundation/integrations/issue").AstryxIntegrationIssue[];
|
|
42
|
+
docTypes?: DocTypesReport;
|
|
42
43
|
};
|
|
43
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* `fixtures`: how many test fixture docs (under `__tests__` in the
|
|
47
|
+
* components or docs root, which those readers skip) were not checked
|
|
48
|
+
*/
|
|
49
|
+
export type DocTypesReport = import("../../foundation/discovery/doc-types.mjs").DocTypesReport & {
|
|
50
|
+
fixtures?: number;
|
|
51
|
+
};
|
|
@@ -38,7 +38,18 @@
|
|
|
38
38
|
* "healthy" — the two are otherwise byte-identical.
|
|
39
39
|
* @typedef {object} ValidateIntegrationResponse
|
|
40
40
|
* @property {'integration.validate'} type
|
|
41
|
-
* @property {{validated: boolean, name: string | null, version: string | null, issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[]}} data
|
|
41
|
+
* @property {{validated: boolean, name: string | null, version: string | null, issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[], docTypes?: DocTypesReport}} data
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Each doc the package ships, checked against the published authoring type
|
|
46
|
+
* for its kind. Present when the manifest loads. `status: 'skipped'`
|
|
47
|
+
* means nothing was checked (no TypeScript in the project) and `reason` says
|
|
48
|
+
* why. Every finding is a warning.
|
|
49
|
+
*
|
|
50
|
+
* @typedef {import('../../foundation/discovery/doc-types.mjs').DocTypesReport & {fixtures?: number}} DocTypesReport
|
|
51
|
+
* `fixtures`: how many test fixture docs (under `__tests__` in the
|
|
52
|
+
* components or docs root, which those readers skip) were not checked
|
|
42
53
|
*/
|
|
43
54
|
|
|
44
55
|
export {};
|
|
@@ -23,11 +23,25 @@ export const doc = {
|
|
|
23
23
|
'declared contribution root exists, and runs the codemod/template/component/' +
|
|
24
24
|
'docs/theme validators, returning every finding as an AstryxIntegrationIssue. A missing ' +
|
|
25
25
|
'local manifest is guidance (name null, no issues), not an error, so callers ' +
|
|
26
|
-
'can stay exit-0 in a non-integration directory.'
|
|
26
|
+
'can stay exit-0 in a non-integration directory. It also checks every doc the ' +
|
|
27
|
+
'package ships under its roots (component, template, docs topic, theme, and translation, served or not) against ' +
|
|
28
|
+
"the published authoring type for its kind, as `tsc` would, with the project's " +
|
|
29
|
+
'own `typescript`: an unknown field, a missing required field, or a value outside ' +
|
|
30
|
+
"a field's type is a warning in `docTypes` naming the field and a fix. A test " +
|
|
31
|
+
'fixture under `__tests__` in the components or docs root is counted, not checked. ' +
|
|
32
|
+
'Without TypeScript, `docTypes` is `skipped` with a reason.',
|
|
27
33
|
importPath: '@astryxdesign/cli/api',
|
|
28
34
|
signature:
|
|
29
35
|
'validateIntegration(pkg?: string, options?: ValidateIntegrationOptions): Promise<ValidateIntegrationResponse>',
|
|
30
|
-
keywords: [
|
|
36
|
+
keywords: [
|
|
37
|
+
'integration',
|
|
38
|
+
'validate',
|
|
39
|
+
'manifest',
|
|
40
|
+
'lint',
|
|
41
|
+
'check',
|
|
42
|
+
'types',
|
|
43
|
+
'typecheck',
|
|
44
|
+
],
|
|
31
45
|
params: [
|
|
32
46
|
{
|
|
33
47
|
name: 'pkg',
|
|
@@ -46,7 +60,7 @@ export const doc = {
|
|
|
46
60
|
{
|
|
47
61
|
type: 'integration.validate',
|
|
48
62
|
description:
|
|
49
|
-
'The result envelope: `data.validated`, false when nothing was validated — with no `package.json` at all, `data.issues` carries one `no_package` error-severity issue so the command exits 1; with a `package.json` but no integration manifest, `data.issues` is empty, which means "not looked at", not "healthy"; `data.name` and `data.version` of the validated package (both null when `data.validated` is false; `data.name` is `(local package)` when the local package.json has no readable name), plus `data.issues`, an AstryxIntegrationIssue[] of {code, severity: `warning` | `error`, message}.',
|
|
63
|
+
'The result envelope: `data.validated`, false when nothing was validated — with no `package.json` at all, `data.issues` carries one `no_package` error-severity issue so the command exits 1; with a `package.json` but no integration manifest, `data.issues` is empty, which means "not looked at", not "healthy"; `data.name` and `data.version` of the validated package (both null when `data.validated` is false; `data.name` is `(local package)` when the local package.json has no readable name), plus `data.issues`, an AstryxIntegrationIssue[] of {code, severity: `warning` | `error`, message}. When the manifest loads, `data.docTypes` holds the doc type check: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `docs` (doc values checked), `findings` ({doc, line, export, type, field, problem: `unknown_field` | `missing_field` | `wrong_type`, severity: `warning`, message, fix}), `unchecked` ({doc, reason}: a doc that could not be fully checked, or a root its reader cannot read), and `fixtures` when the components or docs root holds test fixture docs under `__tests__`: how many, none of them checked.',
|
|
50
64
|
},
|
|
51
65
|
],
|
|
52
66
|
examples: [
|
package/api/search/search.d.mts
CHANGED
|
@@ -40,19 +40,20 @@ export function tokenizeQuery(term: string): string[];
|
|
|
40
40
|
* typography/font-setup, `side panel` is "Side panels");
|
|
41
41
|
* - 1: a word of the query is the topic's name (`illustration` in a longer
|
|
42
42
|
* question is the illustrations guide);
|
|
43
|
+
* - 1: a query of two or more words is one of the topic's own declared
|
|
44
|
+
* keywords, whatever the plural (`code review` on a guide that declares it);
|
|
43
45
|
* - 1: the query holds one of the doc's titles whole (`resizable side panels`
|
|
44
46
|
* names "Side panels"; `light dark mode button` names "Light/Dark Mode");
|
|
45
47
|
* - 1: the doc matches every word of the query and one of them is a word of
|
|
46
48
|
* its title (`switch to a dark theme` and "Use a theme").
|
|
47
49
|
* A section answers to its topic's name.
|
|
48
50
|
*
|
|
49
|
-
* A doc that matched only by
|
|
51
|
+
* A doc that matched only by a heading holding the query, by one word, or by
|
|
50
52
|
* words spread through its text has no priority. Docs are split into many
|
|
51
|
-
* small topics,
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* above the component. Broad reference pages match every word of many queries
|
|
53
|
+
* small topics, each with its own headings, so a common phrase such as
|
|
54
|
+
* `font size` hits a heading exactly (170) while the component the reader is
|
|
55
|
+
* after matches one word by keyword (`Text`, 98). Ranked on text alone, every
|
|
56
|
+
* split adds another doc above the component. Broad reference pages match every word of many queries
|
|
56
57
|
* in their text the same way. The score stays the text-match strength the
|
|
57
58
|
* result reports, so callers that gate on it (`build`) see the same numbers.
|
|
58
59
|
*
|
|
@@ -213,6 +214,12 @@ export type Candidate = {
|
|
|
213
214
|
*/
|
|
214
215
|
aliases?: string[] | undefined;
|
|
215
216
|
keywords?: string[] | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* - A doc's own declared keywords: what its
|
|
219
|
+
* author lists in `keywords`, without the code terms and table cells its
|
|
220
|
+
* other keywords add.
|
|
221
|
+
*/
|
|
222
|
+
_declared?: string[] | undefined;
|
|
216
223
|
weakKeywords?: string[] | undefined;
|
|
217
224
|
description?: string | undefined;
|
|
218
225
|
prose?: string[] | undefined;
|
|
@@ -75,14 +75,75 @@ async function placedGuides(route, seen = new Set()) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
describe('search domain priority — the rule', () => {
|
|
78
|
-
it('gives a doc no priority for a
|
|
78
|
+
it('gives a doc no priority for a heading match or a one-word keyword', () => {
|
|
79
79
|
const topic = {
|
|
80
80
|
domain: 'doc',
|
|
81
81
|
name: 'typography/font-setup',
|
|
82
|
-
keywords: ['
|
|
82
|
+
keywords: ['size'],
|
|
83
83
|
titles: ['Font Setup', 'Font Sizes'],
|
|
84
84
|
};
|
|
85
85
|
expect(priority('font size', topic)).toBe(0);
|
|
86
|
+
expect(priority('size', topic)).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('gives a doc priority when the query is one of its declared keywords', () => {
|
|
90
|
+
const guide = {
|
|
91
|
+
domain: 'doc',
|
|
92
|
+
name: 'guides/quality',
|
|
93
|
+
titles: ['Is my code good?'],
|
|
94
|
+
keywords: ['code review', 'review my code'],
|
|
95
|
+
_declared: ['code review', 'review my code'],
|
|
96
|
+
};
|
|
97
|
+
expect(priority('code review', guide)).toBe(1);
|
|
98
|
+
expect(priority('Code Reviews', guide)).toBe(1);
|
|
99
|
+
expect(priority('review my code', guide)).toBe(1);
|
|
100
|
+
// A section's keywords are its headings and code terms.
|
|
101
|
+
const section = {
|
|
102
|
+
...guide,
|
|
103
|
+
name: 'before-you-ship',
|
|
104
|
+
_topic: 'guides/quality',
|
|
105
|
+
_section: 'before-you-ship',
|
|
106
|
+
};
|
|
107
|
+
expect(priority('code review', section)).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("gives a doc no priority for a code term or table cell it didn't declare", () => {
|
|
111
|
+
// A typed doc's keywords hold the first column of its tables and the code
|
|
112
|
+
// terms its text mentions; `missing_field` reads as two words.
|
|
113
|
+
const reference = {
|
|
114
|
+
domain: 'doc',
|
|
115
|
+
name: 'cli/api/functions/validate-integration',
|
|
116
|
+
titles: ['validateIntegration'],
|
|
117
|
+
keywords: ['validate-integration', 'missing_field', 'file required'],
|
|
118
|
+
_declared: [],
|
|
119
|
+
_topic: 'cli/api/functions/validate-integration',
|
|
120
|
+
};
|
|
121
|
+
expect(priority('missing field', reference)).toBe(0);
|
|
122
|
+
expect(priority('file required', reference)).toBe(0);
|
|
123
|
+
expect(
|
|
124
|
+
priority('missing field', {...reference, _declared: ['missing field']}),
|
|
125
|
+
).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('keeps a component the query names exactly above a doc that declares it', async () => {
|
|
129
|
+
const tokens = tokenizeQuery('empty state');
|
|
130
|
+
const component = {domain: 'component', name: 'EmptyState'};
|
|
131
|
+
const doc = {
|
|
132
|
+
domain: 'doc',
|
|
133
|
+
name: 'illustrations',
|
|
134
|
+
keywords: ['empty state'],
|
|
135
|
+
_declared: ['empty state'],
|
|
136
|
+
};
|
|
137
|
+
const c = scoreQuery('empty state', tokens, component);
|
|
138
|
+
const d = scoreQuery('empty state', tokens, doc);
|
|
139
|
+
expect(
|
|
140
|
+
domainPriority('empty state', tokens, /** @type {any} */ (component), c),
|
|
141
|
+
).toBe(1);
|
|
142
|
+
expect(
|
|
143
|
+
domainPriority('empty state', tokens, /** @type {any} */ (doc), d),
|
|
144
|
+
).toBe(1);
|
|
145
|
+
// Both have priority, so the stronger match wins: the exact name.
|
|
146
|
+
expect(c?.score).toBeGreaterThan(d?.score ?? 0);
|
|
86
147
|
});
|
|
87
148
|
|
|
88
149
|
it('gives a doc priority when the query names it', () => {
|
|
@@ -190,32 +251,46 @@ describe('search domain priority — the rule', () => {
|
|
|
190
251
|
|
|
191
252
|
describe('search domain priority — real docs and components', () => {
|
|
192
253
|
it(
|
|
193
|
-
'finds
|
|
254
|
+
'finds a guide that declares the whole phrase above a component that matches one word',
|
|
194
255
|
async () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
r => r.domain === '
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
256
|
+
const results = (await search('font size', {cwd})).data.results;
|
|
257
|
+
expect(results[0]).toMatchObject({domain: 'doc'});
|
|
258
|
+
expect(results[0].name).toMatch(/^typography(?:\/|$)/);
|
|
259
|
+
// The component the phrase points to stays near the top.
|
|
260
|
+
expect(
|
|
261
|
+
results
|
|
262
|
+
.slice(0, 3)
|
|
263
|
+
.some(r => r.domain === 'component' && r.name === 'Text'),
|
|
264
|
+
).toBe(true);
|
|
265
|
+
},
|
|
266
|
+
SLOW,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
it(
|
|
270
|
+
'keeps a component above a reference doc whose table or code terms hold the phrase',
|
|
271
|
+
async () => {
|
|
272
|
+
for (const q of ['missing field', 'file required']) {
|
|
273
|
+
const [top] = (await search(q, {cwd})).data.results;
|
|
274
|
+
expect(top, q).toMatchObject({domain: 'component', name: 'Field'});
|
|
206
275
|
}
|
|
207
276
|
},
|
|
208
277
|
SLOW,
|
|
209
278
|
);
|
|
210
279
|
|
|
211
280
|
it(
|
|
212
|
-
'
|
|
281
|
+
'keeps a component above docs that match only by a heading, with scores unchanged',
|
|
213
282
|
async () => {
|
|
214
283
|
const results = (await search('font size', {cwd})).data.results;
|
|
215
|
-
const
|
|
216
|
-
r => r.domain === '
|
|
284
|
+
const text = results.findIndex(
|
|
285
|
+
r => r.domain === 'component' && r.name === 'Text',
|
|
217
286
|
);
|
|
218
|
-
expect(
|
|
287
|
+
expect(text).toBeGreaterThanOrEqual(0);
|
|
288
|
+
// A doc below Text outscores it on text alone: priority moved it, not the score.
|
|
289
|
+
expect(
|
|
290
|
+
results
|
|
291
|
+
.slice(text + 1)
|
|
292
|
+
.some(r => r.domain === 'doc' && r.score > results[text].score),
|
|
293
|
+
).toBe(true);
|
|
219
294
|
},
|
|
220
295
|
SLOW,
|
|
221
296
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A reader looking for the check a build runs finds
|
|
5
|
+
* `astryx integration check` first.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {describe, expect, it} from 'vitest';
|
|
9
|
+
import {search} from './search.mjs';
|
|
10
|
+
|
|
11
|
+
describe('search finds integration check for CI', () => {
|
|
12
|
+
it.each([
|
|
13
|
+
'ci',
|
|
14
|
+
'continuous integration',
|
|
15
|
+
'typecheck integration',
|
|
16
|
+
'integration check',
|
|
17
|
+
])('`%s` ranks it first', async query => {
|
|
18
|
+
const {results} = (await search(query)).data;
|
|
19
|
+
expect(results[0], JSON.stringify(results.slice(0, 3))).toMatchObject({
|
|
20
|
+
domain: 'doc',
|
|
21
|
+
name: 'cli/commands/integration-check',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
package/api/search/search.mjs
CHANGED
|
@@ -57,9 +57,10 @@
|
|
|
57
57
|
* Between domains, ranking adds one rule ahead of the score: domain priority
|
|
58
58
|
* (see {@link domainPriority}). A component, hook, template, or theme whose
|
|
59
59
|
* name or keyword a query word hits ranks ahead of every doc the reader did not
|
|
60
|
-
* ask for by name or
|
|
61
|
-
*
|
|
62
|
-
* query holds,
|
|
60
|
+
* ask for by name, title, or a declared phrase, so `font size` finds `Text`
|
|
61
|
+
* before a token page whose heading holds the phrase. A doc the query names,
|
|
62
|
+
* whose title the query holds, or that declares the whole query as a keyword
|
|
63
|
+
* keeps its place.
|
|
63
64
|
*
|
|
64
65
|
* Description and guidance are separate tiers on purpose. A component's own
|
|
65
66
|
* one-line description saying "notification" is a claim about what it IS; the
|
|
@@ -138,6 +139,9 @@ import {setResultCoverage} from './coverage.mjs';
|
|
|
138
139
|
* @property {string[]} [aliases] - Other names the candidate answers to, scored
|
|
139
140
|
* with the same name signals: a theme's display name.
|
|
140
141
|
* @property {string[]} [keywords]
|
|
142
|
+
* @property {string[]} [_declared] - A doc's own declared keywords: what its
|
|
143
|
+
* author lists in `keywords`, without the code terms and table cells its
|
|
144
|
+
* other keywords add.
|
|
141
145
|
* @property {string[]} [weakKeywords]
|
|
142
146
|
* @property {string} [description]
|
|
143
147
|
* @property {string[]} [prose]
|
|
@@ -513,19 +517,20 @@ const STRONG_TOKEN_SCORE = 70;
|
|
|
513
517
|
* typography/font-setup, `side panel` is "Side panels");
|
|
514
518
|
* - 1: a word of the query is the topic's name (`illustration` in a longer
|
|
515
519
|
* question is the illustrations guide);
|
|
520
|
+
* - 1: a query of two or more words is one of the topic's own declared
|
|
521
|
+
* keywords, whatever the plural (`code review` on a guide that declares it);
|
|
516
522
|
* - 1: the query holds one of the doc's titles whole (`resizable side panels`
|
|
517
523
|
* names "Side panels"; `light dark mode button` names "Light/Dark Mode");
|
|
518
524
|
* - 1: the doc matches every word of the query and one of them is a word of
|
|
519
525
|
* its title (`switch to a dark theme` and "Use a theme").
|
|
520
526
|
* A section answers to its topic's name.
|
|
521
527
|
*
|
|
522
|
-
* A doc that matched only by
|
|
528
|
+
* A doc that matched only by a heading holding the query, by one word, or by
|
|
523
529
|
* words spread through its text has no priority. Docs are split into many
|
|
524
|
-
* small topics,
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
* above the component. Broad reference pages match every word of many queries
|
|
530
|
+
* small topics, each with its own headings, so a common phrase such as
|
|
531
|
+
* `font size` hits a heading exactly (170) while the component the reader is
|
|
532
|
+
* after matches one word by keyword (`Text`, 98). Ranked on text alone, every
|
|
533
|
+
* split adds another doc above the component. Broad reference pages match every word of many queries
|
|
529
534
|
* in their text the same way. The score stays the text-match strength the
|
|
530
535
|
* result reports, so callers that gate on it (`build`) see the same numbers.
|
|
531
536
|
*
|
|
@@ -551,6 +556,20 @@ export function domainPriority(term, tokens, candidate, hit) {
|
|
|
551
556
|
if (named.some(n => samePhrase(term, n))) {
|
|
552
557
|
return 2;
|
|
553
558
|
}
|
|
559
|
+
// A query of two or more words that is one of the doc's own declared
|
|
560
|
+
// keywords, read as words whatever the plural, is the author naming the
|
|
561
|
+
// doc for that phrase: `code review` on a guide that declares it. Only
|
|
562
|
+
// what the author declared counts: the code terms and table cells a
|
|
563
|
+
// doc's other keywords add (`missing_field`) name what it mentions, not
|
|
564
|
+
// what it is for. One word is left out too, and a section declares
|
|
565
|
+
// nothing.
|
|
566
|
+
if (
|
|
567
|
+
phraseWords(term).length >= 2 &&
|
|
568
|
+
candidate._section == null &&
|
|
569
|
+
(candidate._declared ?? []).some(k => samePhrase(term, String(k)))
|
|
570
|
+
) {
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
554
573
|
if (
|
|
555
574
|
words.some(
|
|
556
575
|
w =>
|
|
@@ -1508,6 +1527,10 @@ async function gatherDocs(cwd) {
|
|
|
1508
1527
|
...defined,
|
|
1509
1528
|
...codeTerms({content}),
|
|
1510
1529
|
],
|
|
1530
|
+
_declared: [
|
|
1531
|
+
...(Array.isArray(selfDoc?.keywords) ? selfDoc.keywords : []),
|
|
1532
|
+
...(Array.isArray(node.keywords) ? node.keywords : []),
|
|
1533
|
+
],
|
|
1511
1534
|
description: node.summary || '',
|
|
1512
1535
|
prose: sectionProse({title: node.title, content}),
|
|
1513
1536
|
titles: [node.title],
|
|
@@ -1644,6 +1667,7 @@ function topicCandidates(
|
|
|
1644
1667
|
...(doc?.title || title ? [doc?.title || title] : []),
|
|
1645
1668
|
...(Array.isArray(doc?.keywords) ? doc.keywords : []),
|
|
1646
1669
|
],
|
|
1670
|
+
_declared: Array.isArray(doc?.keywords) ? doc.keywords : [],
|
|
1647
1671
|
description: doc?.description || summary,
|
|
1648
1672
|
prose: split
|
|
1649
1673
|
? sections.map(section => section.title).filter(Boolean)
|
|
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
|
|
|
13
13
|
import {interceptCore} from './core-interception.mjs';
|
|
14
14
|
import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
|
|
15
15
|
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
16
17
|
vi.setConfig({testTimeout: 120_000});
|
|
17
18
|
const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
18
19
|
const FAMILY_KEY = 'ocean-family';
|
|
@@ -252,22 +253,69 @@ describe('themeBuildFamily()', () => {
|
|
|
252
253
|
expect(fs.existsSync(dtsPath)).toBe(false);
|
|
253
254
|
});
|
|
254
255
|
|
|
255
|
-
it('refuses family outputs outside cwd
|
|
256
|
+
it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
|
|
256
257
|
const dir = makeDir();
|
|
257
258
|
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
258
259
|
const app = path.join(dir, 'app');
|
|
259
260
|
fs.mkdirSync(app);
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
await expect(
|
|
263
|
+
themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
|
|
264
|
+
).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
265
|
+
const checked = await themeBuildFamily(
|
|
266
|
+
files,
|
|
267
|
+
{familyKey: FAMILY_KEY, check: true},
|
|
268
|
+
{cwd: app},
|
|
269
|
+
);
|
|
270
|
+
expect(checked?.type).toBe('theme.build.check');
|
|
271
|
+
expect(checked?.data.upToDate).toBe(false);
|
|
272
|
+
// The `.css.d.ts` import stub may be absent in check mode.
|
|
273
|
+
expect(checked?.data.stale).toEqual(
|
|
274
|
+
OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
|
|
275
|
+
path: `../themes/${file}`,
|
|
276
|
+
reason: 'missing',
|
|
277
|
+
})),
|
|
278
|
+
);
|
|
266
279
|
for (const output of OUTPUTS) {
|
|
267
280
|
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
|
|
268
281
|
}
|
|
269
282
|
});
|
|
270
283
|
|
|
284
|
+
it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
|
|
285
|
+
const dir = makeDir();
|
|
286
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
287
|
+
const app = path.join(dir, 'app');
|
|
288
|
+
fs.mkdirSync(app);
|
|
289
|
+
const family = ['--family', ...files, '--family-key', FAMILY_KEY];
|
|
290
|
+
|
|
291
|
+
const stale = await runCli(['theme', 'build', ...family, '--check'], app);
|
|
292
|
+
expect(stale.code).toBe(1);
|
|
293
|
+
// The old remedy ran the build from cwd, which refuses these outputs.
|
|
294
|
+
expect(stale.stderr).not.toContain('Rebuild with:');
|
|
295
|
+
|
|
296
|
+
const lines = stale.stderr.split('\n').map(line => line.trim());
|
|
297
|
+
const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
|
|
298
|
+
expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
|
|
299
|
+
const stem = `${getCliInvocation(runDir)} theme build `;
|
|
300
|
+
const [buildArgs, checkArgs] = lines
|
|
301
|
+
.filter(line => line.startsWith(stem))
|
|
302
|
+
.map(line => line.slice(stem.length).split(' '));
|
|
303
|
+
expect(checkArgs).toEqual([...buildArgs, '--check']);
|
|
304
|
+
|
|
305
|
+
// Following the remedy builds, and the check from there passes.
|
|
306
|
+
const built = await runCli(['theme', 'build', ...buildArgs], runDir);
|
|
307
|
+
expect(built.code).toBe(0);
|
|
308
|
+
for (const output of OUTPUTS) {
|
|
309
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
const checked = await runCli(
|
|
312
|
+
['--json', 'theme', 'build', ...checkArgs],
|
|
313
|
+
runDir,
|
|
314
|
+
);
|
|
315
|
+
expect(checked.code).toBe(0);
|
|
316
|
+
expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
271
319
|
it('fails without publishing a mixed trio when staging a write fails', async () => {
|
|
272
320
|
const dir = makeDir();
|
|
273
321
|
const files = writeFamily(dir);
|