@astryxdesign/cli 0.6.3-canary.98a2e4e → 0.6.3-canary.b333c39

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.
@@ -4,14 +4,15 @@
4
4
  * @file docs.detail.section leaf — load a single section of a topic.
5
5
  *
6
6
  * @input A topic name, a section query, and optional {lang, zh, dense}. Resolves
7
- * and loads the topic via the shared adapter, then finds the section by its
8
- * stable key, then by exact title, then by a title that contains the query.
7
+ * the topic via the shared adapter, finds the section in its lowered compiled
8
+ * node by its stable key, then by exact title, then by a title that contains
9
+ * the query, and links only that section.
9
10
  * @output { type: 'docs.detail.section', data: ReferenceSection } with any
10
11
  * token-ref blocks inlined — matching `astryx --json docs <topic> <section>`.
11
12
  * Throws ERR_UNKNOWN_SECTION when nothing matches, or when the query matches
12
13
  * more than one section (the candidates come back as suggestions).
13
- * @position Leaf nested under api/docs/detail. Shares discovery/loading/
14
- * topic-resolution with the detail leaf via _adapter.mjs.
14
+ * @position Leaf nested under api/docs/detail. Shares topic resolution with the
15
+ * detail leaf via _adapter.mjs and reads through the compiler's lenses.
15
16
  */
16
17
 
17
18
  import {AstryxError} from '../../../error.mjs';
@@ -20,8 +21,12 @@ import {
20
21
  findDocSection,
21
22
  sectionKey,
22
23
  } from '../../../../foundation/discovery/docs-section-key.mjs';
23
- import {resolveTopicDocs} from '../../_adapter.mjs';
24
- import {resolveTokenRefs} from '../detail.mjs';
24
+ import {linkReferenceSection} from '../../../../foundation/doc-compiler/compile.mjs';
25
+ import {
26
+ readerSections,
27
+ sectionView,
28
+ } from '../../../../foundation/doc-compiler/lenses.mjs';
29
+ import {referenceTargets, resolveTopicDocs} from '../../_adapter.mjs';
25
30
 
26
31
  /**
27
32
  * @param {string} topic
@@ -44,31 +49,30 @@ export async function section(topic, sectionName, options = {}) {
44
49
  ERROR_CODES.ERR_UNKNOWN_SECTION,
45
50
  );
46
51
  }
47
- const {catalog, docsData, lang} = await resolveTopicDocs(topic, options);
48
52
 
49
- const {section: match, candidates} = findDocSection(
50
- docsData.sections,
51
- sectionName,
52
- );
53
+ const {catalog, node, lang} = await resolveTopicDocs(topic, options);
54
+ const sections = readerSections(node);
55
+ const {section: match, candidates} = findDocSection(sections, sectionName);
53
56
  if (!match) {
54
57
  const ambiguous = candidates.length > 1;
55
58
  throw new AstryxError(
56
59
  ambiguous
57
60
  ? `Section "${sectionName}" matches ${candidates.length} sections in "${topic}". Read one by its key.`
58
61
  : `Section "${sectionName}" not found in "${topic}"`,
59
- (ambiguous ? candidates : docsData.sections).map(s => ({
62
+ (ambiguous ? candidates : sections).map(s => ({
60
63
  name: sectionKey(s),
61
64
  reason: s.title,
62
65
  })),
63
66
  ERROR_CODES.ERR_UNKNOWN_SECTION,
64
67
  );
65
68
  }
69
+
66
70
  // A section read on its own inlines its token refs, as the whole topic does;
67
- // otherwise a section that is only a token-ref prints blank.
68
- const {sections} = await resolveTokenRefs(
69
- {...docsData, sections: [match]},
70
- catalog,
71
- {lang},
71
+ // otherwise a section that is only a token-ref prints blank. Only this
72
+ // section is linked, so a broken reference elsewhere cannot fail the read.
73
+ const linked = await linkReferenceSection(
74
+ match,
75
+ referenceTargets(catalog, lang),
72
76
  );
73
- return {type: 'docs.detail.section', data: sections[0]};
77
+ return {type: 'docs.detail.section', data: sectionView(node, linked)};
74
78
  }
@@ -10,7 +10,7 @@
10
10
  import {describe, it, expect} from 'vitest';
11
11
  import {section} from './section.mjs';
12
12
  import {AstryxError} from '../../../error.mjs';
13
- import {loadDocsCatalog, loadTopicDoc} from '../../_adapter.mjs';
13
+ import {loadDocsCatalog, lowerTopic} from '../../_adapter.mjs';
14
14
 
15
15
  const SLOW = 30_000;
16
16
 
@@ -42,7 +42,8 @@ describe('docs.detail.section leaf', () => {
42
42
  }, SLOW);
43
43
 
44
44
  it('reads a section by its stable key', async () => {
45
- const doc = await loadTopicDoc((await loadDocsCatalog()).resolve('theme'));
45
+ const catalog = await loadDocsCatalog();
46
+ const {doc} = await lowerTopic(catalog, catalog.resolve('theme'));
46
47
  const target = doc.sections[doc.sections.length - 1];
47
48
  const res = await section('theme', target.id);
48
49
  expect(res.data.title).toBe(target.title);
@@ -63,7 +64,7 @@ describe('docs.detail.section leaf', () => {
63
64
  const catalog = await loadDocsCatalog();
64
65
  let checked = 0;
65
66
  for (const entry of catalog.entries()) {
66
- const doc = await loadTopicDoc(entry);
67
+ const {doc} = await lowerTopic(catalog, entry);
67
68
  for (const own of doc.sections) {
68
69
  if (!own.content.some(block => block.type === 'token-ref')) continue;
69
70
  const res = await section(entry.name, own.id, lang ? {lang} : {});
@@ -3,8 +3,9 @@
3
3
  /**
4
4
  * @file docs.index leaf — the section index of one topic.
5
5
  *
6
- * @input A topic name plus optional {lang, zh, dense, cwd}. Resolves and loads
7
- * the topic via the shared adapter.
6
+ * @input A topic name plus optional {lang, zh, dense, cwd}. Resolves the topic
7
+ * via the shared adapter and reads its lowered compiled node; nothing is
8
+ * linked, since the index never inlines a token reference.
8
9
  * @output { type: 'docs.index', data: DocsIndex } — the topic's name, title and
9
10
  * description and, for each section, the key it is read by, its title, and a
10
11
  * one-line summary. Matches `astryx --json docs <topic> --index`.
@@ -13,7 +14,7 @@
13
14
  * read, is the detail leaf.
14
15
  */
15
16
 
16
- import {buildDocsIndexData} from '../../../foundation/discovery/docs-section-key.mjs';
17
+ import {indexView} from '../../../foundation/doc-compiler/lenses.mjs';
17
18
  import {resolveTopicDocs} from '../_adapter.mjs';
18
19
 
19
20
  /**
@@ -26,6 +27,6 @@ import {resolveTopicDocs} from '../_adapter.mjs';
26
27
  * @returns {Promise<import('../docs.type.mjs').DocsIndexResponse>}
27
28
  */
28
29
  export async function index(topic, options = {}) {
29
- const {docsData} = await resolveTopicDocs(topic, options);
30
- return {type: 'docs.index', data: buildDocsIndexData(docsData)};
30
+ const {node} = await resolveTopicDocs(topic, options);
31
+ return {type: 'docs.index', data: indexView(node)};
31
32
  }
@@ -34,8 +34,8 @@ import {
34
34
  docsIndexBytes,
35
35
  oversizedDocSections,
36
36
  } from '../../foundation/discovery/docs-output-budget.mjs';
37
- import {loadTopicDoc, overlayLanguages} from '../docs/_adapter.mjs';
38
- import {resolveTokenRefs} from '../docs/detail/detail.mjs';
37
+ import {compileTopic, overlayLanguages} from '../docs/_adapter.mjs';
38
+ import {detailView} from '../../foundation/doc-compiler/lenses.mjs';
39
39
  import {semverCompare, isValidSemver, satisfiesRange} from '../../foundation/env/semver.mjs';
40
40
 
41
41
  /**
@@ -798,11 +798,7 @@ export async function checkDocsProgressiveDisclosure(ctx) {
798
798
  for (const lang of [null, ...overlayLanguages(entry)]) {
799
799
  const where = lang ? `${entry.name} [${lang}]` : entry.name;
800
800
  try {
801
- const doc = await resolveTokenRefs(
802
- await loadTopicDoc(entry, {lang}),
803
- catalog,
804
- {lang},
805
- );
801
+ const doc = detailView(await compileTopic(catalog, entry, lang));
806
802
  if (lang == null) topics += 1;
807
803
  const indexBytes = docsIndexBytes(buildDocsIndexData(doc));
808
804
  if (indexBytes > DOC_OUTPUT_BUDGET_BYTES) {
@@ -66,7 +66,7 @@ import {
66
66
  import {loadIntegrationsSafely} from '../component/_adapter.mjs';
67
67
  import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
68
68
  import {discoverTemplates, extractComponents} from '../template/template.mjs';
69
- import {loadDocsCatalog, loadTopicDoc} from '../docs/_adapter.mjs';
69
+ import {loadDocsCatalog, lowerTopic} from '../docs/_adapter.mjs';
70
70
  import {AstryxError} from '../error.mjs';
71
71
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
72
72
  import {setResultCoverage} from './coverage.mjs';
@@ -732,16 +732,16 @@ async function gatherHooks(coreDir) {
732
732
  async function gatherDocs(cwd) {
733
733
  /** @type {Candidate[]} */
734
734
  const candidates = [];
735
- let entries;
735
+ let catalog;
736
736
  try {
737
- entries = (await loadDocsCatalog(cwd)).entries();
737
+ catalog = await loadDocsCatalog(cwd);
738
738
  } catch {
739
739
  return candidates;
740
740
  }
741
- for (const entry of entries) {
741
+ for (const entry of catalog.entries()) {
742
742
  let doc = null;
743
743
  try {
744
- doc = await loadTopicDoc(entry);
744
+ doc = (await lowerTopic(catalog, entry)).doc;
745
745
  } catch {
746
746
  // A topic that cannot be loaded is reported by the commands that own
747
747
  // integration issues; search just cannot index it.
@@ -3,7 +3,8 @@
3
3
  import {describe, it, expect} from 'vitest';
4
4
 
5
5
  async function applyTransform(source, path = 'test.ts') {
6
- const {default: transform} = await import('../unwrap-authoring-factories.mjs');
6
+ const {default: transform} =
7
+ await import('../unwrap-authoring-factories.mjs');
7
8
  const jscodeshift = (await import('jscodeshift')).default;
8
9
  const j = jscodeshift.withParser('tsx');
9
10
  const api = {jscodeshift: j, stats: () => {}, report: () => {}};
@@ -18,7 +19,7 @@ export default createConfig({integrations: ['@acme/widgets']});
18
19
  `;
19
20
  const output = await applyTransform(input);
20
21
  expect(output).not.toContain('createConfig');
21
- expect(output).toContain("export default {");
22
+ expect(output).toContain('export default {');
22
23
  expect(output).toContain("integrations: ['@acme/widgets']");
23
24
  expect(output).not.toContain('type:');
24
25
  });
@@ -132,13 +133,34 @@ export default createComponentDoc();
132
133
  expect(output).toContain("type: 'component'");
133
134
  });
134
135
 
136
+ it('leaves same-named factories from unrelated packages unchanged', async () => {
137
+ const input = `import {createConfig} from '@acme/eslint';
138
+ export default createConfig({strict: true});
139
+ `;
140
+ expect(await applyTransform(input)).toBe(input);
141
+ });
142
+
143
+ it('keeps an unrelated same-named import when another factory is migrated', async () => {
144
+ const input = `import {createConfig as createLintConfig} from '@acme/eslint';
145
+ import {createDoc} from '@astryxdesign/cli/doc';
146
+ export const lintConfig = createLintConfig({strict: true});
147
+ export const doc = createDoc({name: 'Theming', description: 'How theming works.'});
148
+ `;
149
+ const output = await applyTransform(input);
150
+ expect(output).toContain(
151
+ "import {createConfig as createLintConfig} from '@acme/eslint'",
152
+ );
153
+ expect(output).toContain('createLintConfig({strict: true})');
154
+ expect(output).not.toContain('createDoc');
155
+ expect(output).toContain("type: 'generic'");
156
+ });
157
+
135
158
  it('is a no-op when no authoring factory is imported', async () => {
136
159
  const input = `import {Button} from '@astryxdesign/core';
137
160
  export default Button;
138
161
  `;
139
- const {default: transform} = await import(
140
- '../unwrap-authoring-factories.mjs'
141
- );
162
+ const {default: transform} =
163
+ await import('../unwrap-authoring-factories.mjs');
142
164
  const jscodeshift = (await import('jscodeshift')).default;
143
165
  const j = jscodeshift.withParser('tsx');
144
166
  const api = {jscodeshift: j, stats: () => {}, report: () => {}};
@@ -5,8 +5,9 @@
5
5
  *
6
6
  * v0.3.0 removes the authoring factories. Authoring is now types + parsers: an
7
7
  * author writes a plain object and stamps its `type` directly. This transform
8
- * rewrites every factory call to the plain object the factory used to return,
9
- * then drops the now-dead factory imports:
8
+ * rewrites factory calls imported from the retired Astryx authoring entrypoints
9
+ * to the plain object the factory used to return, then drops the now-dead
10
+ * factory imports:
10
11
  *
11
12
  * createConfig(o) / createIntegration(o) -> o (no discriminant)
12
13
  * createComponentDoc(o) -> { ...o, type: 'component' }
@@ -25,8 +26,9 @@
25
26
  *
26
27
  * Import aliases are followed (`import {createDoc as mk}` → calls to `mk`), and
27
28
  * the factory specifiers are removed afterward (the whole import statement goes
28
- * if nothing else was imported from it). Run this BEFORE
29
- * `migrate-authoring-imports`, which repoints the surviving type imports.
29
+ * if nothing else was imported from it). Same-named imports from other packages
30
+ * remain untouched. Run this BEFORE `migrate-authoring-imports`, which repoints
31
+ * the surviving type imports.
30
32
  */
31
33
 
32
34
  export const meta = {
@@ -35,13 +37,24 @@ export const meta = {
35
37
  'Rewrites createConfig/createIntegration/createComponentDoc/' +
36
38
  'createFunctionDoc/createDoc/createPageTemplate/createBlockTemplate/' +
37
39
  'createCodemod/createConfigCodemod calls to the plain object they returned ' +
38
- "(stamping the doc/template/codemod `type` discriminant), and removes the " +
40
+ '(stamping the doc/template/codemod `type` discriminant), and removes the ' +
39
41
  'now-dead factory imports. Authoring is types + parsers in v0.3.0 — there ' +
40
42
  'are no factories.',
41
43
  pr: '#4612',
42
44
  fileExtensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'],
43
45
  };
44
46
 
47
+ /** Legacy Astryx authoring entrypoints that exported the removed factories. */
48
+ const AUTHORING_SOURCES = new Set([
49
+ '@astryxdesign/cli/config',
50
+ '@astryxdesign/cli/doc',
51
+ '@astryxdesign/cli/integration',
52
+ '@astryxdesign/cli/template',
53
+ '@astryxdesign/cli/codemod',
54
+ '@astryxdesign/core/authoring',
55
+ '@astryxdesign/core/config',
56
+ ]);
57
+
45
58
  /**
46
59
  * Factory name → the `type` discriminant it stamped, or `null` for the config /
47
60
  * integration factories, which were pure typed-identity (no discriminant).
@@ -108,6 +121,7 @@ export default function transformer(file, api) {
108
121
  /** @type {Map<string, string>} */
109
122
  const localToFactory = new Map();
110
123
  root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
124
+ if (!AUTHORING_SOURCES.has(path.node.source.value)) return;
111
125
  for (const spec of path.node.specifiers ?? []) {
112
126
  if (spec.type !== 'ImportSpecifier') continue;
113
127
  const importedName = spec.imported?.name;
@@ -163,6 +177,7 @@ export default function transformer(file, api) {
163
177
  // Drop the now-dead factory import specifiers; remove any import statement
164
178
  // left empty.
165
179
  root.find(j.ImportDeclaration).forEach((/** @type {any} */ path) => {
180
+ if (!AUTHORING_SOURCES.has(path.node.source.value)) return;
166
181
  const specs = path.node.specifiers ?? [];
167
182
  const kept = specs.filter(
168
183
  (/** @type {any} */ spec) =>
@@ -0,0 +1,162 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * One authored file, as discovery loaded it.
6
+ * @typedef {object} AuthoredFile
7
+ * @property {string} file the file's name, for messages only
8
+ * @property {any} [doc] the parsed doc, when it loaded
9
+ * @property {unknown} [error] why it did not load or parse
10
+ * @property {any} [overlay] the language overlay's export, when one applies
11
+ * @property {unknown} [overlayError] why the overlay did not load
12
+ */
13
+ /**
14
+ * @typedef {object} ReferenceTopicInput
15
+ * @property {string} id the topic's name in the catalog
16
+ * @property {string} provider the package that owns the topic
17
+ * @property {string | null} replaces the topic it took the place of
18
+ * @property {string | null} lang the overlay language, or null for authored text
19
+ * @property {AuthoredFile} base
20
+ * @property {Array<AuthoredFile & {provider: string}>} extensions in merge order
21
+ */
22
+ /**
23
+ * A token reference after linking: the target section's content, or why it
24
+ * has none.
25
+ * @typedef {{status: 'resolved', topic: string, section: string, previewType?: string, content: any[]}
26
+ * | {status: 'unknown-topic'}
27
+ * | {status: 'unknown-section'}} TokenRefResolution
28
+ */
29
+ /**
30
+ * @typedef {object} CompiledReferenceNode
31
+ * @property {number} schemaVersion
32
+ * @property {'reference'} kind
33
+ * @property {'lowered' | 'linked'} stage `linked` once every token reference
34
+ * carries its resolution; a lowered node carries none
35
+ * @property {string} id the topic's name in the catalog
36
+ * @property {string | null} lang
37
+ * @property {{provider: string, replaces: string | null, extensions: string[]}} provenance
38
+ * @property {Record<string, string>} sourceTitles section key -> authored title
39
+ * @property {any} doc the topic: authored fields in authored order, every
40
+ * section keyed; a linked node's token references carry `resolved`
41
+ */
42
+ /**
43
+ * Lower one topic: overlay each file, merge the extensions in order, and stamp
44
+ * every section with its key.
45
+ * @param {ReferenceTopicInput} input
46
+ * @returns {CompiledReferenceNode}
47
+ */
48
+ export function lowerReferenceTopic(input: ReferenceTopicInput): CompiledReferenceNode;
49
+ /**
50
+ * Link every section of a lowered node.
51
+ * @param {CompiledReferenceNode} node
52
+ * @param {(topic: string) => Promise<CompiledReferenceNode | null>} lowerTarget
53
+ * the lowered node a reference names, or null when no topic has that name
54
+ * @returns {Promise<CompiledReferenceNode>}
55
+ */
56
+ export function linkReferenceTopic(node: CompiledReferenceNode, lowerTarget: (topic: string) => Promise<CompiledReferenceNode | null>): Promise<CompiledReferenceNode>;
57
+ /**
58
+ * Resolve the token references in one section. A section with none comes back
59
+ * as it went in.
60
+ * @template {{content: any[]}} S
61
+ * @param {S} section
62
+ * @param {(topic: string) => Promise<CompiledReferenceNode | null>} lowerTarget
63
+ * @returns {Promise<S>}
64
+ */
65
+ export function linkReferenceSection<S extends {
66
+ content: any[];
67
+ }>(section: S, lowerTarget: (topic: string) => Promise<CompiledReferenceNode | null>): Promise<S>;
68
+ /** Bumped whenever the shape of a compiled node changes. */
69
+ export const COMPILED_DOC_SCHEMA_VERSION: 1;
70
+ /**
71
+ * One authored file, as discovery loaded it.
72
+ */
73
+ export type AuthoredFile = {
74
+ /**
75
+ * the file's name, for messages only
76
+ */
77
+ file: string;
78
+ /**
79
+ * the parsed doc, when it loaded
80
+ */
81
+ doc?: any;
82
+ /**
83
+ * why it did not load or parse
84
+ */
85
+ error?: unknown;
86
+ /**
87
+ * the language overlay's export, when one applies
88
+ */
89
+ overlay?: any;
90
+ /**
91
+ * why the overlay did not load
92
+ */
93
+ overlayError?: unknown;
94
+ };
95
+ export type ReferenceTopicInput = {
96
+ /**
97
+ * the topic's name in the catalog
98
+ */
99
+ id: string;
100
+ /**
101
+ * the package that owns the topic
102
+ */
103
+ provider: string;
104
+ /**
105
+ * the topic it took the place of
106
+ */
107
+ replaces: string | null;
108
+ /**
109
+ * the overlay language, or null for authored text
110
+ */
111
+ lang: string | null;
112
+ base: AuthoredFile;
113
+ /**
114
+ * in merge order
115
+ */
116
+ extensions: Array<AuthoredFile & {
117
+ provider: string;
118
+ }>;
119
+ };
120
+ /**
121
+ * A token reference after linking: the target section's content, or why it
122
+ * has none.
123
+ */
124
+ export type TokenRefResolution = {
125
+ status: "resolved";
126
+ topic: string;
127
+ section: string;
128
+ previewType?: string;
129
+ content: any[];
130
+ } | {
131
+ status: "unknown-topic";
132
+ } | {
133
+ status: "unknown-section";
134
+ };
135
+ export type CompiledReferenceNode = {
136
+ schemaVersion: number;
137
+ kind: "reference";
138
+ /**
139
+ * `linked` once every token reference
140
+ * carries its resolution; a lowered node carries none
141
+ */
142
+ stage: "lowered" | "linked";
143
+ /**
144
+ * the topic's name in the catalog
145
+ */
146
+ id: string;
147
+ lang: string | null;
148
+ provenance: {
149
+ provider: string;
150
+ replaces: string | null;
151
+ extensions: string[];
152
+ };
153
+ /**
154
+ * section key -> authored title
155
+ */
156
+ sourceTitles: Record<string, string>;
157
+ /**
158
+ * the topic: authored fields in authored order, every
159
+ * section keyed; a linked node's token references carry `resolved`
160
+ */
161
+ doc: any;
162
+ };