@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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
AUTHORING_SELF_DOCS,
|
|
8
|
+
auditAuthoringSelfDocs,
|
|
9
|
+
buildAuthoringTopic,
|
|
10
|
+
discoverAuthoringSelfDocSources,
|
|
11
|
+
} from './authoring-self-docs.mjs';
|
|
12
|
+
import {
|
|
13
|
+
GRAPH_BLOCK_TYPES,
|
|
14
|
+
GRAPH_ONLY_FIELDS,
|
|
15
|
+
problemsInTopic,
|
|
16
|
+
} from './docs-discovery.mjs';
|
|
17
|
+
import {doc as graphFieldsDoc} from '../../authoring/doctypes/base/graph-fields.doc.mjs';
|
|
18
|
+
import {doc as namespaceDoc} from '../../authoring/doctypes/namespace/namespace.doc.mjs';
|
|
19
|
+
import {doc as referenceDoc} from '../../authoring/doctypes/reference/reference.doc.mjs';
|
|
20
|
+
import {docs} from '../../api/docs/docs.mjs';
|
|
21
|
+
|
|
22
|
+
const SLOW = 60_000;
|
|
23
|
+
|
|
24
|
+
describe('authoring self-docs', () => {
|
|
25
|
+
it('lists every self-doc on disk exactly once', () => {
|
|
26
|
+
expect([...AUTHORING_SELF_DOCS].sort()).toEqual(
|
|
27
|
+
discoverAuthoringSelfDocSources(),
|
|
28
|
+
);
|
|
29
|
+
expect(new Set(AUTHORING_SELF_DOCS).size).toBe(AUTHORING_SELF_DOCS.length);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('audits clean: every self-doc loads, is reachable, and fits one read', async () => {
|
|
33
|
+
expect(await auditAuthoringSelfDocs()).toEqual({
|
|
34
|
+
sections: AUTHORING_SELF_DOCS.length,
|
|
35
|
+
unreachable: [],
|
|
36
|
+
failed: [],
|
|
37
|
+
oversized: [],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('builds a valid topic with one section per self-doc', async () => {
|
|
42
|
+
const topic = await buildAuthoringTopic();
|
|
43
|
+
expect(problemsInTopic(topic)).toEqual([]);
|
|
44
|
+
expect(topic.sections).toHaveLength(AUTHORING_SELF_DOCS.length);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it(
|
|
48
|
+
'is readable progressively through the docs API',
|
|
49
|
+
async () => {
|
|
50
|
+
const index = await docs('authoring', undefined, {index: true});
|
|
51
|
+
expect(index.type).toBe('docs.index');
|
|
52
|
+
expect(index.data.sections.map(s => s.id)).toContain('integration');
|
|
53
|
+
const section = await docs('authoring', 'integration');
|
|
54
|
+
expect(section.data.title).toBe('Astryx Integration');
|
|
55
|
+
expect(section.data.content.some(block => block.type === 'table')).toBe(
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
SLOW,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('what the authoring docs say about the unbuilt docs graph', () => {
|
|
64
|
+
it('marks exactly the fields topic loading rejects as not read yet', () => {
|
|
65
|
+
const notReadYet = graphFieldsDoc.fields
|
|
66
|
+
.filter(field => /Not read yet/.test(field.description))
|
|
67
|
+
.map(field => field.name);
|
|
68
|
+
expect(notReadYet.sort()).toEqual([...GRAPH_ONLY_FIELDS].sort());
|
|
69
|
+
for (const field of graphFieldsDoc.fields) {
|
|
70
|
+
if (notReadYet.includes(field.name)) {
|
|
71
|
+
expect(field.description).toMatch(/fails to load/);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
expect(graphFieldsDoc.description).toMatch(/not built yet/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('says a topic using a graph block fails to load', () => {
|
|
78
|
+
const content = referenceDoc.fields
|
|
79
|
+
.flatMap(field => [field, ...(field.fields ?? [])])
|
|
80
|
+
.find(field => field.name === 'sections[].content');
|
|
81
|
+
for (const type of GRAPH_BLOCK_TYPES) {
|
|
82
|
+
expect(content.description).toContain(type);
|
|
83
|
+
}
|
|
84
|
+
expect(content.description).toMatch(/fails to load/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('says namespace docs are not loaded yet', () => {
|
|
88
|
+
expect(namespaceDoc.description).toMatch(/Not loaded yet/);
|
|
89
|
+
expect(
|
|
90
|
+
problemsInTopic({
|
|
91
|
+
...namespaceDoc.examples?.[0],
|
|
92
|
+
type: 'namespace',
|
|
93
|
+
name: 'x',
|
|
94
|
+
}),
|
|
95
|
+
).toEqual([
|
|
96
|
+
'"x" is a namespace doc. Only the docs graph reads namespace docs, and it is not built yet; remove this file from the docs directory.',
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('auditAuthoringSelfDocs', () => {
|
|
102
|
+
let root;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
root = fs.mkdtempSync(path.join(process.cwd(), '.astryx-self-docs-'));
|
|
106
|
+
fs.mkdirSync(path.join(root, 'kept'));
|
|
107
|
+
fs.writeFileSync(
|
|
108
|
+
path.join(root, 'kept', 'kept.doc.mjs'),
|
|
109
|
+
"export const doc = {type: 'schema', name: 'kept', displayName: 'Kept', description: 'Listed.', fields: []};\n",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
fs.rmSync(root, {recursive: true, force: true});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('names a self-doc on disk that the list leaves out', async () => {
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
path.join(root, 'kept', 'forgotten.doc.mjs'),
|
|
120
|
+
"export const doc = {name: 'forgotten', description: 'Not listed.'};\n",
|
|
121
|
+
);
|
|
122
|
+
const audit = await auditAuthoringSelfDocs({
|
|
123
|
+
root,
|
|
124
|
+
sources: ['kept/kept.doc.mjs'],
|
|
125
|
+
});
|
|
126
|
+
expect(audit.unreachable).toEqual(['kept/forgotten.doc.mjs']);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('reports a self-doc that fails to load instead of throwing', async () => {
|
|
130
|
+
fs.writeFileSync(
|
|
131
|
+
path.join(root, 'kept', 'broken.doc.mjs'),
|
|
132
|
+
'export const doc = {;\n',
|
|
133
|
+
);
|
|
134
|
+
const audit = await auditAuthoringSelfDocs({
|
|
135
|
+
root,
|
|
136
|
+
sources: ['kept/kept.doc.mjs', 'kept/broken.doc.mjs'],
|
|
137
|
+
});
|
|
138
|
+
expect(audit.failed.map(entry => entry.source)).toEqual([
|
|
139
|
+
'kept/broken.doc.mjs',
|
|
140
|
+
]);
|
|
141
|
+
expect(audit.sections).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('names a section over the budget', async () => {
|
|
145
|
+
const audit = await auditAuthoringSelfDocs({
|
|
146
|
+
root,
|
|
147
|
+
sources: ['kept/kept.doc.mjs'],
|
|
148
|
+
budget: 10,
|
|
149
|
+
});
|
|
150
|
+
expect(audit.oversized).toEqual([
|
|
151
|
+
expect.objectContaining({key: 'kept', title: 'Kept'}),
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -175,4 +175,4 @@ export function discoverOwnedComponents(coreDir: string, loadedIntegrations?: Ar
|
|
|
175
175
|
issuesUrl: string | undefined;
|
|
176
176
|
}>;
|
|
177
177
|
/** The owner package name for built-in (core) components. */
|
|
178
|
-
export const CORE_PACKAGE: "
|
|
178
|
+
export const CORE_PACKAGE: import("../../authoring/index.js").ProviderId;
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import {existsCaseExact} from '../fs/paths.mjs';
|
|
10
|
+
import {CORE_PROVIDER_ID} from '../identity/providers.mjs';
|
|
10
11
|
|
|
11
12
|
const SKIP_DIRS = new Set(['hooks', 'utils', '__tests__', 'node_modules']);
|
|
12
13
|
|
|
13
14
|
/** The owner package name for built-in (core) components. */
|
|
14
|
-
export const CORE_PACKAGE =
|
|
15
|
+
export const CORE_PACKAGE = CORE_PROVIDER_ID;
|
|
15
16
|
|
|
16
17
|
/** Conventional doc-file suffixes for integration components (same-stem). */
|
|
17
18
|
const INTEGRATION_DOC_SUFFIXES = ['.doc.ts', '.doc.mjs', '.doc.js'];
|
|
@@ -72,9 +72,10 @@ export function discoverIntegrationDocs(integration: {
|
|
|
72
72
|
errors: Error[];
|
|
73
73
|
}>;
|
|
74
74
|
/**
|
|
75
|
-
* Merge an extension onto a base topic: a section
|
|
76
|
-
* the base
|
|
77
|
-
* title
|
|
75
|
+
* Merge an extension onto a base topic: a section with a stable `id` replaces
|
|
76
|
+
* the base section with the same `id`; legacy sections without IDs fall back to
|
|
77
|
+
* title matching. A section with no match is appended, and title/description
|
|
78
|
+
* are taken from the extension when it states them.
|
|
78
79
|
*
|
|
79
80
|
* Keyed by section TITLE rather than by position, the way the localization
|
|
80
81
|
* overlays are — position keying grafts an overlay onto whichever section
|
|
@@ -86,12 +87,17 @@ export function discoverIntegrationDocs(integration: {
|
|
|
86
87
|
* @returns {any} a new doc; neither input is mutated
|
|
87
88
|
*/
|
|
88
89
|
export function mergeTopic(base: any, overlay: any): any;
|
|
90
|
+
export { withSourceTitle };
|
|
89
91
|
/**
|
|
90
92
|
* Owner package recorded for the built-in topics. They ship inside the CLI
|
|
91
93
|
* (assets/docs), not in @astryxdesign/core, so this is the CLI's own name —
|
|
92
94
|
* unlike component discovery, whose built-ins belong to core.
|
|
93
95
|
*/
|
|
94
|
-
export const BUILTIN_DOCS_PACKAGE: "
|
|
96
|
+
export const BUILTIN_DOCS_PACKAGE: import("../../authoring/index.js").ProviderId;
|
|
97
|
+
/** Blocks that are valid authoring but require the compiled graph renderer. */
|
|
98
|
+
export const GRAPH_BLOCK_TYPES: Set<string>;
|
|
99
|
+
/** Doc fields only the docs graph reads; a topic that sets one fails to load. */
|
|
100
|
+
export const GRAPH_ONLY_FIELDS: string[];
|
|
95
101
|
/**
|
|
96
102
|
* Every topic a project can read, and the relationships between them.
|
|
97
103
|
*
|
|
@@ -183,3 +189,4 @@ export type DocsTopicEntry = {
|
|
|
183
189
|
path: string;
|
|
184
190
|
}>;
|
|
185
191
|
};
|
|
192
|
+
import { withSourceTitle } from './docs-section-key.mjs';
|
|
@@ -33,7 +33,16 @@ import * as fs from 'node:fs';
|
|
|
33
33
|
import * as path from 'node:path';
|
|
34
34
|
import {CLI_ROOT} from '../fs/paths.mjs';
|
|
35
35
|
import {importUserModule} from '../fs/module-loader.mjs';
|
|
36
|
+
import {CLI_PROVIDER_ID} from '../identity/providers.mjs';
|
|
36
37
|
import {parseDoc} from '../../authoring/doctypes/parse.mjs';
|
|
38
|
+
import {
|
|
39
|
+
sectionKey,
|
|
40
|
+
sectionKeyProblems,
|
|
41
|
+
sourceTitle,
|
|
42
|
+
withSourceTitle,
|
|
43
|
+
} from './docs-section-key.mjs';
|
|
44
|
+
|
|
45
|
+
export {withSourceTitle};
|
|
37
46
|
|
|
38
47
|
/** Where the CLI's own topics live. */
|
|
39
48
|
const BUILTIN_DOCS_DIR = path.join(CLI_ROOT, 'assets', 'docs');
|
|
@@ -43,7 +52,7 @@ const BUILTIN_DOCS_DIR = path.join(CLI_ROOT, 'assets', 'docs');
|
|
|
43
52
|
* (assets/docs), not in @astryxdesign/core, so this is the CLI's own name —
|
|
44
53
|
* unlike component discovery, whose built-ins belong to core.
|
|
45
54
|
*/
|
|
46
|
-
export const BUILTIN_DOCS_PACKAGE =
|
|
55
|
+
export const BUILTIN_DOCS_PACKAGE = CLI_PROVIDER_ID;
|
|
47
56
|
|
|
48
57
|
/**
|
|
49
58
|
* A built-in topic file: `{topic}.doc.mjs`. Anchored at both ends so a
|
|
@@ -128,12 +137,25 @@ const BLOCK_FIELDS = {
|
|
|
128
137
|
'token-ref': ['topic', 'section'],
|
|
129
138
|
};
|
|
130
139
|
|
|
140
|
+
/** Blocks that are valid authoring but require the compiled graph renderer. */
|
|
141
|
+
export const GRAPH_BLOCK_TYPES = new Set([
|
|
142
|
+
'workflow',
|
|
143
|
+
'collection',
|
|
144
|
+
'reference',
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
/** Doc fields only the docs graph reads; a topic that sets one fails to load. */
|
|
148
|
+
export const GRAPH_ONLY_FIELDS = ['placement', 'aliases', 'audience'];
|
|
149
|
+
|
|
131
150
|
/**
|
|
132
151
|
* Fields a block kind may carry but does not need. Kept per kind rather than
|
|
133
152
|
* globally: only a code block renders a `label`, so allowing it everywhere
|
|
134
153
|
* would wave through the misspellings this check exists to catch.
|
|
135
154
|
*/
|
|
136
|
-
|
|
155
|
+
/** @type {Record<string, string[]>} */
|
|
156
|
+
const OPTIONAL_BLOCK_FIELDS = {
|
|
157
|
+
code: ['label'],
|
|
158
|
+
};
|
|
137
159
|
|
|
138
160
|
/**
|
|
139
161
|
* Fields whose value has to be one of a set, because the renderer indexes on
|
|
@@ -147,7 +169,7 @@ const BLOCK_FIELD_VALUES = {
|
|
|
147
169
|
};
|
|
148
170
|
|
|
149
171
|
/** Keys a section may carry. */
|
|
150
|
-
const SECTION_FIELDS = ['title', 'category', 'content', 'previewType'];
|
|
172
|
+
const SECTION_FIELDS = ['id', 'title', 'category', 'content', 'previewType'];
|
|
151
173
|
|
|
152
174
|
/**
|
|
153
175
|
* Check the fields the docs surfaces actually read. `parseDoc` is the outer
|
|
@@ -161,6 +183,13 @@ const SECTION_FIELDS = ['title', 'category', 'content', 'previewType'];
|
|
|
161
183
|
* @returns {string[]} problems, each already pointed at a place in the doc
|
|
162
184
|
*/
|
|
163
185
|
export function problemsInTopic(doc) {
|
|
186
|
+
// A namespace doc is valid authoring that only the docs graph reads. Said
|
|
187
|
+
// plainly, instead of as the topic fields it does not have.
|
|
188
|
+
if (doc?.type === 'namespace') {
|
|
189
|
+
return [
|
|
190
|
+
`"${doc.name}" is a namespace doc. Only the docs graph reads namespace docs, and it is not built yet; remove this file from the docs directory.`,
|
|
191
|
+
];
|
|
192
|
+
}
|
|
164
193
|
/** @type {string[]} */
|
|
165
194
|
const problems = [];
|
|
166
195
|
for (const field of ['name', 'title', 'description']) {
|
|
@@ -173,83 +202,119 @@ export function problemsInTopic(doc) {
|
|
|
173
202
|
`name: "${doc.name}" is not URL-safe. A topic name is its CLI argument and its docsite path, so it may hold only letters, digits, "_" and "-".`,
|
|
174
203
|
);
|
|
175
204
|
}
|
|
205
|
+
for (const field of GRAPH_ONLY_FIELDS) {
|
|
206
|
+
if (doc?.[field] != null) {
|
|
207
|
+
problems.push(
|
|
208
|
+
`${field}: requires the compiled graph reader and is not supported by legacy topic readers`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
176
212
|
if (!Array.isArray(doc?.sections) || doc.sections.length === 0) {
|
|
177
213
|
problems.push('sections: expected at least one section');
|
|
178
214
|
return problems;
|
|
179
215
|
}
|
|
180
216
|
|
|
181
|
-
doc.sections.forEach(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
for (const key of Object.keys(section ?? {})) {
|
|
187
|
-
if (!SECTION_FIELDS.includes(key)) {
|
|
188
|
-
problems.push(`${at}.${key}: not a field of a section`);
|
|
217
|
+
doc.sections.forEach(
|
|
218
|
+
(/** @type {any} */ section, /** @type {number} */ s) => {
|
|
219
|
+
const at = `sections[${s}]`;
|
|
220
|
+
if (typeof section?.title !== 'string' || section.title === '') {
|
|
221
|
+
problems.push(`${at}.title: expected a non-empty string`);
|
|
189
222
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
section.content.forEach((/** @type {any} */ block, /** @type {number} */ b) => {
|
|
196
|
-
const blockAt = `${at}.content[${b}]`;
|
|
197
|
-
const fields = /** @type {Record<string, string[]>} */ (BLOCK_FIELDS)[block?.type];
|
|
198
|
-
if (fields == null) {
|
|
199
|
-
problems.push(
|
|
200
|
-
`${blockAt}.type: ${JSON.stringify(block?.type)} is not one of ${Object.keys(BLOCK_FIELDS).join(', ')}`,
|
|
201
|
-
);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
for (const field of fields) {
|
|
205
|
-
const value = block[field];
|
|
206
|
-
// Empty counts as missing, the way it does for the doc's own title: a
|
|
207
|
-
// block whose text is '' passes every other check and renders as a gap.
|
|
208
|
-
if (value == null) {
|
|
209
|
-
problems.push(`${blockAt}.${field}: required for a ${block.type} block`);
|
|
210
|
-
} else if (typeof value === 'string' && value.trim() === '') {
|
|
211
|
-
problems.push(`${blockAt}.${field}: expected a non-empty string`);
|
|
212
|
-
} else if (Array.isArray(value) && value.length === 0) {
|
|
213
|
-
problems.push(`${blockAt}.${field}: expected a non-empty array`);
|
|
223
|
+
for (const key of Object.keys(section ?? {})) {
|
|
224
|
+
if (!SECTION_FIELDS.includes(key)) {
|
|
225
|
+
problems.push(`${at}.${key}: not a field of a section`);
|
|
214
226
|
}
|
|
215
227
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const value = block[field];
|
|
220
|
-
if (value != null && !values.includes(value)) {
|
|
221
|
-
problems.push(
|
|
222
|
-
`${blockAt}.${field}: ${JSON.stringify(value)} is not one of ${values.join(', ')}`,
|
|
223
|
-
);
|
|
224
|
-
}
|
|
228
|
+
if (!Array.isArray(section?.content)) {
|
|
229
|
+
problems.push(`${at}.content: expected an array of blocks`);
|
|
230
|
+
return;
|
|
225
231
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
section.content.forEach(
|
|
233
|
+
(/** @type {any} */ block, /** @type {number} */ b) => {
|
|
234
|
+
const blockAt = `${at}.content[${b}]`;
|
|
235
|
+
const fields = /** @type {Record<string, string[]>} */ (BLOCK_FIELDS)[
|
|
236
|
+
block?.type
|
|
237
|
+
];
|
|
238
|
+
if (fields == null) {
|
|
239
|
+
if (GRAPH_BLOCK_TYPES.has(block?.type)) {
|
|
240
|
+
problems.push(
|
|
241
|
+
`${blockAt}.type: ${JSON.stringify(block.type)} requires the compiled graph renderer and is not supported by legacy topic readers`,
|
|
242
|
+
);
|
|
243
|
+
} else {
|
|
244
|
+
problems.push(
|
|
245
|
+
`${blockAt}.type: ${JSON.stringify(block?.type)} is not one of ${Object.keys(BLOCK_FIELDS).join(', ')}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
for (const field of fields) {
|
|
251
|
+
const value = block[field];
|
|
252
|
+
// Empty counts as missing, the way it does for the doc's own title: a
|
|
253
|
+
// block whose text is '' passes every other check and renders as a gap.
|
|
254
|
+
if (value == null) {
|
|
255
|
+
problems.push(
|
|
256
|
+
`${blockAt}.${field}: required for a ${block.type} block`,
|
|
257
|
+
);
|
|
258
|
+
} else if (typeof value === 'string' && value.trim() === '') {
|
|
259
|
+
problems.push(`${blockAt}.${field}: expected a non-empty string`);
|
|
260
|
+
} else if (Array.isArray(value) && value.length === 0) {
|
|
261
|
+
problems.push(`${blockAt}.${field}: expected a non-empty array`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const allowedValues =
|
|
265
|
+
/** @type {Record<string, Record<string, unknown[]>>} */ (
|
|
266
|
+
BLOCK_FIELD_VALUES
|
|
267
|
+
)[block.type] ?? {};
|
|
268
|
+
for (const [field, values] of Object.entries(allowedValues)) {
|
|
269
|
+
const value = block[field];
|
|
270
|
+
if (value != null && !values.includes(value)) {
|
|
271
|
+
problems.push(
|
|
272
|
+
`${blockAt}.${field}: ${JSON.stringify(value)} is not one of ${values.join(', ')}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// A table's cells are read by column index, so a short row renders blank
|
|
277
|
+
// cells and a long one drops its tail — both silently.
|
|
278
|
+
if (
|
|
279
|
+
block.type === 'table' &&
|
|
280
|
+
Array.isArray(block.headers) &&
|
|
281
|
+
Array.isArray(block.rows)
|
|
282
|
+
) {
|
|
283
|
+
block.rows.forEach(
|
|
284
|
+
(/** @type {any} */ row, /** @type {number} */ r) => {
|
|
285
|
+
if (!Array.isArray(row)) {
|
|
286
|
+
problems.push(
|
|
287
|
+
`${blockAt}.rows[${r}]: expected an array of cells`,
|
|
288
|
+
);
|
|
289
|
+
} else if (row.length !== block.headers.length) {
|
|
290
|
+
problems.push(
|
|
291
|
+
`${blockAt}.rows[${r}]: has ${row.length} cells but the table has ${block.headers.length} headers`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
235
295
|
);
|
|
236
296
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
297
|
+
// An unknown key is almost always a misspelled required one, and it
|
|
298
|
+
// would otherwise reach a reader as a block that renders nothing.
|
|
299
|
+
const allowed = [
|
|
300
|
+
'type',
|
|
301
|
+
...fields,
|
|
302
|
+
...(OPTIONAL_BLOCK_FIELDS[block.type] ?? []),
|
|
303
|
+
];
|
|
304
|
+
for (const key of Object.keys(block)) {
|
|
305
|
+
if (!allowed.includes(key)) {
|
|
306
|
+
problems.push(
|
|
307
|
+
`${blockAt}.${key}: not a field of a ${block.type} block`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
// Readers address a section by its key, so two sections sharing one would
|
|
316
|
+
// make one of them unreachable.
|
|
317
|
+
problems.push(...sectionKeyProblems(doc.sections));
|
|
253
318
|
return problems;
|
|
254
319
|
}
|
|
255
320
|
|
|
@@ -283,7 +348,9 @@ export async function discoverIntegrationDocs(integration) {
|
|
|
283
348
|
const full = path.join(dirPath, entry.name);
|
|
284
349
|
if (entry.isDirectory()) {
|
|
285
350
|
scanDir(full);
|
|
286
|
-
} else if (
|
|
351
|
+
} else if (
|
|
352
|
+
INTEGRATION_DOC_SUFFIXES.some(suffix => entry.name.endsWith(suffix))
|
|
353
|
+
) {
|
|
287
354
|
files.push(full);
|
|
288
355
|
}
|
|
289
356
|
}
|
|
@@ -298,7 +365,11 @@ export async function discoverIntegrationDocs(integration) {
|
|
|
298
365
|
try {
|
|
299
366
|
doc = parseDoc(await loadTopicModule(file), path.basename(file));
|
|
300
367
|
} catch (err) {
|
|
301
|
-
errors.push(
|
|
368
|
+
errors.push(
|
|
369
|
+
new Error(
|
|
370
|
+
`${path.relative(docsDir, file)}: ${/** @type {any} */ (err).message}`,
|
|
371
|
+
),
|
|
372
|
+
);
|
|
302
373
|
continue;
|
|
303
374
|
}
|
|
304
375
|
const problems = problemsInTopic(doc);
|
|
@@ -315,7 +386,8 @@ export async function discoverIntegrationDocs(integration) {
|
|
|
315
386
|
const parsed = /** @type {any} */ (doc);
|
|
316
387
|
// Two files claiming one name would collapse into a single entry, and the
|
|
317
388
|
// one that lost would never be reachable. Named here, where both files are.
|
|
318
|
-
const
|
|
389
|
+
const topicKey = parsed.name.toLowerCase();
|
|
390
|
+
const previous = seen.get(topicKey);
|
|
319
391
|
if (previous) {
|
|
320
392
|
errors.push(
|
|
321
393
|
new Error(
|
|
@@ -324,7 +396,7 @@ export async function discoverIntegrationDocs(integration) {
|
|
|
324
396
|
);
|
|
325
397
|
continue;
|
|
326
398
|
}
|
|
327
|
-
seen.set(
|
|
399
|
+
seen.set(topicKey, path.relative(docsDir, file));
|
|
328
400
|
if (parsed.replaces != null && parsed.extends != null) {
|
|
329
401
|
errors.push(
|
|
330
402
|
new Error(
|
|
@@ -349,9 +421,10 @@ export async function discoverIntegrationDocs(integration) {
|
|
|
349
421
|
}
|
|
350
422
|
|
|
351
423
|
/**
|
|
352
|
-
* Merge an extension onto a base topic: a section
|
|
353
|
-
* the base
|
|
354
|
-
* title
|
|
424
|
+
* Merge an extension onto a base topic: a section with a stable `id` replaces
|
|
425
|
+
* the base section with the same `id`; legacy sections without IDs fall back to
|
|
426
|
+
* title matching. A section with no match is appended, and title/description
|
|
427
|
+
* are taken from the extension when it states them.
|
|
355
428
|
*
|
|
356
429
|
* Keyed by section TITLE rather than by position, the way the localization
|
|
357
430
|
* overlays are — position keying grafts an overlay onto whichever section
|
|
@@ -365,9 +438,18 @@ export async function discoverIntegrationDocs(integration) {
|
|
|
365
438
|
export function mergeTopic(base, overlay) {
|
|
366
439
|
const sections = [...(base.sections ?? [])];
|
|
367
440
|
for (const section of overlay.sections ?? []) {
|
|
368
|
-
const at = sections
|
|
369
|
-
if (at === -1)
|
|
370
|
-
|
|
441
|
+
const at = findMergeTarget(sections, section);
|
|
442
|
+
if (at === -1) {
|
|
443
|
+
sections.push(section);
|
|
444
|
+
} else {
|
|
445
|
+
// A legacy extension that replaces a section which has since gained a
|
|
446
|
+
// stable ID keeps that ID, so readers addressing it keep working.
|
|
447
|
+
const replaced = sections[at];
|
|
448
|
+
sections[at] =
|
|
449
|
+
section.id == null && replaced.id != null
|
|
450
|
+
? withSourceTitle({...section, id: replaced.id}, sourceTitle(section))
|
|
451
|
+
: section;
|
|
452
|
+
}
|
|
371
453
|
}
|
|
372
454
|
return {
|
|
373
455
|
...base,
|
|
@@ -377,6 +459,41 @@ export function mergeTopic(base, overlay) {
|
|
|
377
459
|
};
|
|
378
460
|
}
|
|
379
461
|
|
|
462
|
+
/**
|
|
463
|
+
* The base section an extension section replaces. A stable ID matches first.
|
|
464
|
+
* Otherwise the exact title matches when at least one side has no ID: the
|
|
465
|
+
* migration window in which the base or the extension adopts stable IDs
|
|
466
|
+
* before the other does. Two different authored IDs stay distinct even under
|
|
467
|
+
* one title.
|
|
468
|
+
*
|
|
469
|
+
* @param {any[]} sections
|
|
470
|
+
* @param {any} section
|
|
471
|
+
* @returns {number}
|
|
472
|
+
*/
|
|
473
|
+
function findMergeTarget(sections, section) {
|
|
474
|
+
const title = sourceTitle(section);
|
|
475
|
+
const key = sectionKey(section);
|
|
476
|
+
// A section is addressed by its key: an authored id, or the key its title
|
|
477
|
+
// derives, which is the key the topic's index shows. Matching on it means an
|
|
478
|
+
// extension never appends a second section under a key already in use.
|
|
479
|
+
const byKey = () =>
|
|
480
|
+
sections.findIndex(candidate => sectionKey(candidate) === key);
|
|
481
|
+
const legacyTitleMatch = () =>
|
|
482
|
+
sections.findIndex(
|
|
483
|
+
candidate => candidate.id == null && sourceTitle(candidate) === title,
|
|
484
|
+
);
|
|
485
|
+
if (section.id != null) {
|
|
486
|
+
const byId = byKey();
|
|
487
|
+
return byId === -1 ? legacyTitleMatch() : byId;
|
|
488
|
+
}
|
|
489
|
+
const legacy = legacyTitleMatch();
|
|
490
|
+
if (legacy !== -1) return legacy;
|
|
491
|
+
const sameTitle = sections.findIndex(
|
|
492
|
+
candidate => sourceTitle(candidate) === title,
|
|
493
|
+
);
|
|
494
|
+
return sameTitle === -1 ? byKey() : sameTitle;
|
|
495
|
+
}
|
|
496
|
+
|
|
380
497
|
/**
|
|
381
498
|
* Every topic a project can read, and the relationships between them.
|
|
382
499
|
*
|
|
@@ -399,7 +516,7 @@ export class DocsCatalog {
|
|
|
399
516
|
static fromBuiltins(builtins = discoverBuiltinTopics()) {
|
|
400
517
|
const catalog = new DocsCatalog();
|
|
401
518
|
for (const [name, file] of Object.entries(builtins)) {
|
|
402
|
-
catalog.#topics.set(name, {
|
|
519
|
+
catalog.#topics.set(name.toLowerCase(), {
|
|
403
520
|
name,
|
|
404
521
|
package: BUILTIN_DOCS_PACKAGE,
|
|
405
522
|
path: file,
|
|
@@ -455,7 +572,9 @@ export class DocsCatalog {
|
|
|
455
572
|
// The replacement takes the base topic's slot, so a reader that opens
|
|
456
573
|
// the first topic (or the nth) sees the same one it did before.
|
|
457
574
|
const replaced = target.name;
|
|
458
|
-
|
|
575
|
+
const replacedKey = replaced.toLowerCase();
|
|
576
|
+
const replacementKey = record.name.toLowerCase();
|
|
577
|
+
this.#replaceAt(replacedKey, {
|
|
459
578
|
name: record.name,
|
|
460
579
|
package: record.package,
|
|
461
580
|
path: record.path,
|
|
@@ -466,17 +585,18 @@ export class DocsCatalog {
|
|
|
466
585
|
// Extensions were authored against the content that just went away.
|
|
467
586
|
extensions: [],
|
|
468
587
|
});
|
|
469
|
-
if (
|
|
470
|
-
this.#aliases.set(
|
|
588
|
+
if (replacementKey !== replacedKey) {
|
|
589
|
+
this.#aliases.set(replacedKey, replacementKey);
|
|
471
590
|
// A topic renamed twice keeps every name it has ever answered to.
|
|
472
591
|
for (const [from, to] of this.#aliases) {
|
|
473
|
-
if (to ===
|
|
592
|
+
if (to === replacedKey) this.#aliases.set(from, replacementKey);
|
|
474
593
|
}
|
|
475
594
|
}
|
|
476
595
|
return warning;
|
|
477
596
|
}
|
|
478
597
|
|
|
479
|
-
const
|
|
598
|
+
const topicKey = record.name.toLowerCase();
|
|
599
|
+
const existing = this.#topics.get(topicKey);
|
|
480
600
|
if (existing) {
|
|
481
601
|
return {
|
|
482
602
|
code: 'invalid_doc',
|
|
@@ -484,7 +604,7 @@ export class DocsCatalog {
|
|
|
484
604
|
message: `Topic "${record.name}" is already provided by ${existing.package}. Give it another name, or declare \`replaces: '${record.name}'\` to take its place.`,
|
|
485
605
|
};
|
|
486
606
|
}
|
|
487
|
-
this.#topics.set(
|
|
607
|
+
this.#topics.set(topicKey, {
|
|
488
608
|
name: record.name,
|
|
489
609
|
package: record.package,
|
|
490
610
|
path: record.path,
|
|
@@ -519,7 +639,7 @@ export class DocsCatalog {
|
|
|
519
639
|
|
|
520
640
|
/** @returns {string[]} every topic name, in read order */
|
|
521
641
|
names() {
|
|
522
|
-
return [...this.#topics.
|
|
642
|
+
return [...this.#topics.values()].map(entry => entry.name);
|
|
523
643
|
}
|
|
524
644
|
|
|
525
645
|
/** @returns {DocsTopicEntry[]} every topic, in read order */
|
|
@@ -536,7 +656,7 @@ export class DocsCatalog {
|
|
|
536
656
|
/** @type {Map<string, DocsTopicEntry>} */
|
|
537
657
|
const next = new Map();
|
|
538
658
|
for (const [key, value] of this.#topics) {
|
|
539
|
-
if (key === name) next.set(entry.name, entry);
|
|
659
|
+
if (key === name.toLowerCase()) next.set(entry.name.toLowerCase(), entry);
|
|
540
660
|
else next.set(key, value);
|
|
541
661
|
}
|
|
542
662
|
this.#topics = next;
|