@astryxdesign/cli 0.7.12-canary.9a690d5 → 0.7.12-canary.c73b9e4

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.
Files changed (47) hide show
  1. package/api/component/__fixtures__/resolutions/modules.txt +2 -2
  2. package/api/docs/_adapter.d.mts +22 -0
  3. package/api/docs/_adapter.mjs +81 -3
  4. package/api/docs/docs.doc.mjs +5 -5
  5. package/api/docs/docs.type.d.mts +21 -0
  6. package/api/docs/docs.type.mjs +11 -0
  7. package/api/docs/list/list.d.mts +5 -1
  8. package/api/docs/list/list.mjs +72 -6
  9. package/api/docs/node/node.mjs +2 -40
  10. package/api/index.d.mts +1 -0
  11. package/api/index.mjs +1 -0
  12. package/api/render/_adapter.mjs +9 -0
  13. package/api/render/render.doc.mjs +2 -2
  14. package/api/search/_adapter.d.mts +22 -1
  15. package/api/search/_adapter.mjs +37 -20
  16. package/api/search/exportSearchIndex.doc.d.mts +10 -0
  17. package/api/search/exportSearchIndex.doc.mjs +58 -0
  18. package/api/search/rankSearchIndex.doc.d.mts +10 -0
  19. package/api/search/rankSearchIndex.doc.mjs +79 -0
  20. package/api/search/search-index.d.mts +44 -0
  21. package/api/search/search-index.mjs +270 -0
  22. package/api/search/search.type.d.mts +28 -0
  23. package/api/search/search.type.mjs +17 -0
  24. package/api/swizzle/_moduleGraph.d.mts +7 -0
  25. package/api/swizzle/_moduleGraph.mjs +191 -83
  26. package/api/swizzle/copy/copy.mjs +28 -6
  27. package/assets/docs/tree/render-pages.doc.mjs +2 -2
  28. package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.doc.mjs +14 -0
  29. package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.tsx +146 -0
  30. package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.doc.mjs +14 -0
  31. package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.tsx +173 -0
  32. package/assets/templates/themes/butter/butterTheme.ts +1 -1
  33. package/assets/templates/themes/stone/stoneTheme.ts +1 -1
  34. package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
  35. package/clients/cli/commands/docs.doc.mjs +5 -1
  36. package/clients/cli/commands/docs.mjs +30 -2
  37. package/foundation/render/charts.d.mts +59 -0
  38. package/foundation/render/charts.mjs +216 -0
  39. package/foundation/render/embedded-imports.d.mts +7 -7
  40. package/foundation/render/embedded-imports.mjs +63 -54
  41. package/foundation/render/page-worker.mjs +339 -52
  42. package/foundation/render/render-page.mjs +26 -10
  43. package/foundation/render/ts-paths.d.mts +63 -0
  44. package/foundation/render/ts-paths.mjs +138 -0
  45. package/foundation/response/error-codes.doc.mjs +2 -2
  46. package/foundation/response/error-codes.mjs +3 -2
  47. package/package.json +7 -6
@@ -247,9 +247,9 @@ useAutoMediaMode useAutoMediaMode hooks/useAnimatedLayout.doc.mjs hooks/useAutoM
247
247
  useBackdropPress useBackdropPress Layer/Layer.doc.mjs Layer/useBackdropPress.ts
248
248
  useBaseTablePlugins useBaseTablePlugins Table/Table.doc.mjs Table/useBaseTablePlugins.ts
249
249
  useBusyAnnouncement useBusyAnnouncement hooks/useAnimatedLayout.doc.mjs hooks/useBusyAnnouncement.ts
250
- useCalendarConstraints useCalendarConstraints Calendar/Calendar.doc.mjs Calendar/hooks/useCalendarConstraints.ts
250
+ useCalendarConstraints useCalendarConstraints Calendar/useCalendarConstraints.doc.mjs Calendar/hooks/useCalendarConstraints.ts
251
251
  useCalendarDays useCalendarDays Calendar/useCalendarDays.doc.mjs Calendar/hooks/useCalendarDays.ts
252
- useCalendarNavigation useCalendarNavigation Calendar/Calendar.doc.mjs Calendar/hooks/useCalendarNavigation.ts
252
+ useCalendarNavigation useCalendarNavigation Calendar/useCalendarNavigation.doc.mjs Calendar/hooks/useCalendarNavigation.ts
253
253
  useChatComposerTokens useChatComposerTokens Chat/Chat.doc.mjs Chat/useChatComposerTokens.ts
254
254
  useChatDictation useChatDictation Chat/Chat.doc.mjs Chat/useChatDictation.ts
255
255
  useChatNewMessages useChatNewMessages Chat/Chat.doc.mjs Chat/useChatNewMessages.ts
@@ -319,6 +319,28 @@ export function nameOwner(tree: DocsTree, catalog: DocsCatalog, name: string): {
319
319
  kind: "node";
320
320
  node: TreeNode;
321
321
  } | null;
322
+ /**
323
+ * `stability: 'experimental'` for a node whose doc is marked experimental, or
324
+ * that sits under an experimental namespace (spec:AST-017 FR27), the way help
325
+ * marks an experimental command; nothing for a stable one, so stable entries
326
+ * read as they always have.
327
+ * @param {DocsTree} tree
328
+ * @param {TreeNode} node
329
+ * @returns {{stability?: 'experimental'}}
330
+ */
331
+ export function stabilityOf(tree: DocsTree, node: TreeNode): {
332
+ stability?: "experimental";
333
+ };
334
+ /**
335
+ * The fields every child carries: what it is, and the route that opens it.
336
+ * Shared, so a child reads the same whether a namespace read or the topic
337
+ * list names it — including the package, which is the integration's for a
338
+ * guide an integration replaced, not the package whose route it took.
339
+ * @param {DocsTree} tree
340
+ * @param {TreeNode} child
341
+ * @returns {import('./docs.type.mjs').DocsNodeChild}
342
+ */
343
+ export function childIdentity(tree: DocsTree, child: TreeNode): import("./docs.type.mjs").DocsNodeChild;
322
344
  /**
323
345
  * Whether a topic answers to its own name: what the topic list and search
324
346
  * offer must open that topic.
@@ -1765,9 +1765,87 @@ export function nameOwner(tree, catalog, name) {
1765
1765
  return entry == null ? null : {kind: 'topic', entry};
1766
1766
  }
1767
1767
  const entry = catalog.resolve(name);
1768
- if (entry == null) return null;
1769
- const owner = routeOwner(tree, entry);
1770
- return owner == null ? {kind: 'topic', entry} : {kind: 'node', node: owner};
1768
+ if (entry != null) {
1769
+ const owner = routeOwner(tree, entry);
1770
+ return owner == null ? {kind: 'topic', entry} : {kind: 'node', node: owner};
1771
+ }
1772
+ return childOwner(tree, name);
1773
+ }
1774
+
1775
+ /**
1776
+ * The child a reader names without its namespace, or with a route that leaves
1777
+ * out the prefix the child's own name repeats (spec:AST-046 FR5). A namespace
1778
+ * whose children repeat its name makes its own routes stutter: `tokens` holds
1779
+ * `tokens-color`, so the route is `tokens/tokens-color`, while the namespace
1780
+ * listing shows the bare `tokens-color`. Both spellings name one child, so
1781
+ * that child answers. Two or more children answering is ambiguous, and the
1782
+ * read falls through to the suggestion list, unchanged. An experimental doc
1783
+ * never answers to a bare name: its project reaches it only by a marked route
1784
+ * (spec:AST-017 FR27).
1785
+ * @param {DocsTree} tree
1786
+ * @param {string} name
1787
+ * @returns {{kind: 'node', node: TreeNode} | null}
1788
+ */
1789
+ function childOwner(tree, name) {
1790
+ const wanted = name.toLowerCase();
1791
+ const slash = wanted.lastIndexOf('/');
1792
+ const parent = slash > 0 ? wanted.slice(0, slash) : null;
1793
+ const unstuttered =
1794
+ parent == null
1795
+ ? null
1796
+ : `${parent}/${parent.slice(parent.lastIndexOf('/') + 1)}-${wanted.slice(slash + 1)}`;
1797
+ const matches = [...tree.nodes.values()].filter(
1798
+ node =>
1799
+ node.ref?.flatTopic == null &&
1800
+ node.route.includes('/') &&
1801
+ // An experimental doc is reachable only by a route its project marked
1802
+ // as experimental (spec:AST-017 FR27): a bare name never opens one.
1803
+ !isExperimentalTreeNode(tree, node) &&
1804
+ (node.name?.toLowerCase() === wanted ||
1805
+ (unstuttered != null && node.route.toLowerCase() === unstuttered)),
1806
+ );
1807
+ return matches.length === 1 ? {kind: 'node', node: matches[0]} : null;
1808
+ }
1809
+
1810
+ /**
1811
+ * `stability: 'experimental'` for a node whose doc is marked experimental, or
1812
+ * that sits under an experimental namespace (spec:AST-017 FR27), the way help
1813
+ * marks an experimental command; nothing for a stable one, so stable entries
1814
+ * read as they always have.
1815
+ * @param {DocsTree} tree
1816
+ * @param {TreeNode} node
1817
+ * @returns {{stability?: 'experimental'}}
1818
+ */
1819
+ export function stabilityOf(tree, node) {
1820
+ return isExperimentalTreeNode(tree, node) ? {stability: 'experimental'} : {};
1821
+ }
1822
+
1823
+ /**
1824
+ * The fields every child carries: what it is, and the route that opens it.
1825
+ * Shared, so a child reads the same whether a namespace read or the topic
1826
+ * list names it — including the package, which is the integration's for a
1827
+ * guide an integration replaced, not the package whose route it took.
1828
+ * @param {DocsTree} tree
1829
+ * @param {TreeNode} child
1830
+ * @returns {import('./docs.type.mjs').DocsNodeChild}
1831
+ */
1832
+ export function childIdentity(tree, child) {
1833
+ return {
1834
+ route: child.route,
1835
+ name: child.route.slice(child.route.lastIndexOf('/') + 1),
1836
+ package: child.guideOverlay?.replacement?.package ?? child.provider,
1837
+ // A guide an integration's doc took the place of names whose it was.
1838
+ ...(child.guideOverlay?.replacement
1839
+ ? {
1840
+ replaces: child.route,
1841
+ replacedPackage: child.guideOverlay.replacedPackage ?? child.provider,
1842
+ }
1843
+ : {}),
1844
+ kind: child.kind,
1845
+ title: child.title,
1846
+ summary: child.summary,
1847
+ ...stabilityOf(tree, child),
1848
+ };
1771
1849
  }
1772
1850
 
1773
1851
  /**
@@ -54,7 +54,7 @@ export const doc = {
54
54
  name: 'topic',
55
55
  type: 'string',
56
56
  description:
57
- "Doc topic to load (e.g. 'principles'), or a docs-tree route (e.g. 'cli/api/functions/search'). Omit to list all topics.",
57
+ "Doc topic to load (e.g. 'principles'), a docs-tree route (e.g. 'cli/api/functions/search'), or the name of one node of the docs tree: its own name (e.g. 'tokens-color'), or its route without the prefix its name repeats (e.g. 'tokens/color'), where exactly one node answers to it. Omit to list all topics.",
58
58
  },
59
59
  {
60
60
  name: 'section',
@@ -88,13 +88,13 @@ export const doc = {
88
88
  name: 'options.depth',
89
89
  type: "number | 'all'",
90
90
  description:
91
- "How many levels below a docs-tree namespace to read: 0 for the namespace alone, 1 for its children (the default), 'all' for every level. Where a read stops, a child with docs below it carries childCount. A doc with nothing below it reads the same at any depth.",
91
+ "How many levels below a docs-tree namespace to read: 0 for the namespace alone, 1 for its children (the default), 'all' for every level. Where a read stops, a child with docs below it carries childCount. A doc with nothing below it reads the same at any depth. On the topic list, where each namespace otherwise lists alone, it names the docs below each one, as deep as asked; without it the list is unchanged.",
92
92
  },
93
93
  {
94
94
  name: 'options.detail',
95
95
  type: "'brief' | 'compact' | 'full'",
96
96
  description:
97
- "How much of each doc below the named one a depth read returns: brief (the default) is its identity; compact and full add its text (a guide's sections, a namespace's or typed doc's content). Given alone, it reads one level down.",
97
+ "How much of each doc below the named one a depth read returns: brief (the default) is its identity; compact and full add its text (a guide's sections, a namespace's or typed doc's content). Given alone, it reads one level down. The topic list names each doc below a namespace by its identity whatever the detail.",
98
98
  },
99
99
  {
100
100
  name: 'options.cwd',
@@ -107,7 +107,7 @@ export const doc = {
107
107
  {
108
108
  type: 'docs.list',
109
109
  description:
110
- "Every reference-doc topic in read order, as DocsListEntry[] ({topic, description, package, replaces?, replacedPackage?}), each readable with docs(topic). meta.namespaces lists the docs tree's top-level namespaces ({topic, description, package}), and meta.notLoaded each package whose docs did not load ({package, message}).",
110
+ "Every reference-doc topic in read order, as DocsListEntry[] ({topic, description, package, replaces?, replacedPackage?}), each readable with docs(topic). meta.namespaces lists the docs tree's top-level namespaces ({topic, description, package}); with depth, each also carries children, the docs below it as a namespace read names them ({topic, description, route, name, package, kind, title, summary, replaces?, replacedPackage?, stability?}), nested the same way to the depth asked for, and childCount where the depth stops on a doc with docs below it. meta.notLoaded names each package whose docs did not load ({package, message}).",
111
111
  },
112
112
  {
113
113
  type: 'docs.detail',
@@ -141,7 +141,7 @@ export const doc = {
141
141
  },
142
142
  {
143
143
  code: 'ERR_UNKNOWN_TOPIC',
144
- when: 'the topic is not a string, or matches no topic and no docs-tree route',
144
+ when: 'the topic is not a string, or matches no topic, no docs-tree route, and no one node of the docs tree by its own name or by its route without the prefix its name repeats; a name more than one node answers to opens none of them, and the suggestions list them',
145
145
  },
146
146
  {
147
147
  code: 'ERR_UNKNOWN_SECTION',
@@ -56,6 +56,27 @@ export type DocsListNamespace = {
56
56
  * the package that owns it
57
57
  */
58
58
  package: string;
59
+ /**
60
+ * the docs below it, present only for
61
+ * a read that passed `depth` and only where the namespace has children
62
+ */
63
+ children?: DocsListChild[] | undefined;
64
+ /**
65
+ * how many docs sit right below it, where a
66
+ * depth read stops here and it has docs below it
67
+ */
68
+ childCount?: number | undefined;
69
+ };
70
+ /**
71
+ * One doc below a namespace on the topic list, as `astryx docs --depth` names
72
+ * it: everything a namespace read gives a child, plus the two fields every
73
+ * list row carries, so one row reads the same way wherever it came from.
74
+ */
75
+ export type DocsListChild = DocsNodeChild & {
76
+ topic: string;
77
+ description: string;
78
+ children?: DocsListChild[];
79
+ childCount?: number;
59
80
  };
60
81
  /**
61
82
  * astryx --json docs <topic> --index
@@ -45,6 +45,17 @@
45
45
  * @property {string} topic its route, the argument that opens it
46
46
  * @property {string} description
47
47
  * @property {string} package the package that owns it
48
+ * @property {DocsListChild[]} [children] the docs below it, present only for
49
+ * a read that passed `depth` and only where the namespace has children
50
+ * @property {number} [childCount] how many docs sit right below it, where a
51
+ * depth read stops here and it has docs below it
52
+ */
53
+
54
+ /**
55
+ * One doc below a namespace on the topic list, as `astryx docs --depth` names
56
+ * it: everything a namespace read gives a child, plus the two fields every
57
+ * list row carries, so one row reads the same way wherever it came from.
58
+ * @typedef {DocsNodeChild & {topic: string, description: string, children?: DocsListChild[], childCount?: number}} DocsListChild
48
59
  */
49
60
 
50
61
  /**
@@ -4,8 +4,12 @@
4
4
  /**
5
5
  * @param {object} [options]
6
6
  * @param {string} [options.cwd]
7
+ * @param {number | 'all'} [options.depth] how many levels of the docs tree to
8
+ * list below each namespace; without it the namespaces list alone, as they
9
+ * always have
7
10
  * @returns {Promise<import('../docs.type.mjs').DocsListResponse>}
8
11
  */
9
- export function list({ cwd }?: {
12
+ export function list({ cwd, depth }?: {
10
13
  cwd?: string | undefined;
14
+ depth?: number | "all" | undefined;
11
15
  }): Promise<import("../docs.type.mjs").DocsListResponse>;
@@ -16,6 +16,7 @@
16
16
 
17
17
  import {loadTopicFile} from '../../../foundation/doc-compiler/read.mjs';
18
18
  import {
19
+ childIdentity,
19
20
  holdsOwnName,
20
21
  linkDocText,
21
22
  loadDocsCatalog,
@@ -25,9 +26,12 @@ import {
25
26
  /**
26
27
  * @param {object} [options]
27
28
  * @param {string} [options.cwd]
29
+ * @param {number | 'all'} [options.depth] how many levels of the docs tree to
30
+ * list below each namespace; without it the namespaces list alone, as they
31
+ * always have
28
32
  * @returns {Promise<import('../docs.type.mjs').DocsListResponse>}
29
33
  */
30
- export async function list({cwd} = {}) {
34
+ export async function list({cwd, depth} = {}) {
31
35
  const catalog = await loadDocsCatalog(cwd);
32
36
  const tree = await projectTree(catalog);
33
37
  /** @type {Array<import('../docs.type.mjs').DocsListEntry>} */
@@ -64,11 +68,11 @@ export async function list({cwd} = {}) {
64
68
  // The docs tree's top-level namespaces, the CLI's and each integration's,
65
69
  // each the way into a whole branch (spec:AST-046 FR7). They go in `meta`,
66
70
  // so `data` stays the topic list 0.6 returned: every entry reads as a topic.
67
- const namespaces = tree.roots().map(root => ({
68
- topic: root.route,
69
- description: root.summary,
70
- package: root.provider,
71
- }));
71
+ // With `depth`, each one also carries the branch below it, so one read
72
+ // learns every route: without it a reader knows only that the namespace is
73
+ // there, and the children it must name are invisible until it opens each one.
74
+ const levels = depthLevels(depth);
75
+ const namespaces = tree.roots().map(root => branch(tree, root, levels));
72
76
  /** @type {NonNullable<import('../docs.type.mjs').DocsListResponse['meta']>} */
73
77
  const meta = {};
74
78
  if (namespaces.length > 0) meta.namespaces = namespaces;
@@ -79,3 +83,65 @@ export async function list({cwd} = {}) {
79
83
  ? {type: 'docs.list', data: entries}
80
84
  : {type: 'docs.list', data: entries, meta};
81
85
  }
86
+
87
+ /**
88
+ * How many levels below each namespace the list carries: none without
89
+ * `depth`, every level for `'all'`. `docs <namespace> --depth` reads the same
90
+ * way, so one flag means one thing everywhere.
91
+ * @param {number | 'all'} [depth]
92
+ * @returns {number | null} null for a list read with no depth, which carries
93
+ * the namespaces alone
94
+ */
95
+ function depthLevels(depth) {
96
+ if (depth == null) return null;
97
+ return depth === 'all' ? Infinity : depth;
98
+ }
99
+
100
+ /**
101
+ * One namespace of the list, with the branch below it to `levels`. A child
102
+ * reads exactly as a namespace read gives it (`childIdentity`): the route
103
+ * that opens it, and the package that owns it — the integration's, for a
104
+ * guide an integration replaced, with the package whose route it took beside
105
+ * it. Every line of the list is then an argument a reader can pass straight
106
+ * back to `astryx docs`, attributed the way the read attributes it. Where the
107
+ * depth stops on a doc with docs below it, the row says how many, as a
108
+ * namespace read's child does; `levels` of null is a read with no depth,
109
+ * which says nothing about what is below.
110
+ * @param {import('../../../foundation/doc-compiler/tree.mjs').DocsTree} tree
111
+ * @param {import('../../../foundation/doc-compiler/tree.mjs').TreeNode} node
112
+ * @param {number | null} levels
113
+ * @returns {import('../docs.type.mjs').DocsListNamespace}
114
+ */
115
+ function branch(tree, node, levels) {
116
+ /** @type {import('../docs.type.mjs').DocsListNamespace} */
117
+ const listed = {
118
+ topic: node.route,
119
+ description: node.summary,
120
+ package: node.provider,
121
+ };
122
+ if (levels == null) return listed;
123
+ const below = node.slots.flatMap(slot => slot.children);
124
+ if (levels <= 0) {
125
+ if (below.length > 0) listed.childCount = below.length;
126
+ return listed;
127
+ }
128
+ const children = below
129
+ .map(route => tree.get(route))
130
+ .filter(child => child != null)
131
+ .map(child => {
132
+ const node = /** @type {import('../../../foundation/doc-compiler/tree.mjs').TreeNode} */ (
133
+ child
134
+ );
135
+ const {topic: _route, ...rest} = branch(tree, node, levels - 1);
136
+ const identity = childIdentity(tree, node);
137
+ return /** @type {import('../docs.type.mjs').DocsListChild} */ ({
138
+ ...identity,
139
+ topic: identity.route,
140
+ description: identity.summary,
141
+ ...(rest.children ? {children: rest.children} : {}),
142
+ ...(rest.childCount ? {childCount: rest.childCount} : {}),
143
+ });
144
+ });
145
+ if (children.length > 0) listed.children = children;
146
+ return listed;
147
+ }
@@ -16,13 +16,14 @@
16
16
  */
17
17
 
18
18
  import {detailView} from '../../../foundation/doc-compiler/lenses.mjs';
19
- import {isExperimentalTreeNode} from '../../../foundation/doc-compiler/tree.mjs';
20
19
  import {
20
+ childIdentity,
21
21
  compileTopic,
22
22
  guideEntry,
23
23
  nodeContent,
24
24
  placeLinks,
25
25
  resolveDocsArgument,
26
+ stabilityOf,
26
27
  unknownTopicError,
27
28
  } from '../_adapter.mjs';
28
29
 
@@ -87,45 +88,6 @@ export async function nodeView(catalog, tree, node, read) {
87
88
  return view;
88
89
  }
89
90
 
90
- /**
91
- * `stability: 'experimental'` for a node whose doc is marked experimental, or
92
- * that sits under an experimental namespace (spec:AST-017 FR27), the way help
93
- * marks an experimental command; nothing for a stable one, so stable entries
94
- * read as they always have.
95
- * @param {DocsTree} tree
96
- * @param {TreeNode} node
97
- * @returns {{stability?: 'experimental'}}
98
- */
99
- function stabilityOf(tree, node) {
100
- return isExperimentalTreeNode(tree, node) ? {stability: 'experimental'} : {};
101
- }
102
-
103
- /**
104
- * The fields every child carries: what it is, and the route that opens it.
105
- * @param {DocsTree} tree
106
- * @param {TreeNode} child
107
- * @returns {import('../docs.type.mjs').DocsNodeChild}
108
- */
109
- function childIdentity(tree, child) {
110
- return {
111
- route: child.route,
112
- name: child.route.slice(child.route.lastIndexOf('/') + 1),
113
- package: child.guideOverlay?.replacement?.package ?? child.provider,
114
- // A guide an integration's doc took the place of names whose it was.
115
- ...(child.guideOverlay?.replacement
116
- ? {
117
- replaces: child.route,
118
- replacedPackage:
119
- child.guideOverlay.replacedPackage ?? child.provider,
120
- }
121
- : {}),
122
- kind: child.kind,
123
- title: child.title,
124
- summary: child.summary,
125
- ...stabilityOf(tree, child),
126
- };
127
- }
128
-
129
91
  /**
130
92
  * @param {TreeNode['slots']} slots
131
93
  * @returns {number}
package/api/index.d.mts CHANGED
@@ -47,6 +47,7 @@ export * from "./integration/authoring-checks.type.mjs";
47
47
  export * from "./integration/integration-check.type.mjs";
48
48
  export type Logger = import("./logger.mjs").Logger;
49
49
  export { themeBuild, themeAdd, themeRemove, themeUse, themeEject, themeTemplate, themeList, themeListAvailable, themeListCopySources, themeTargets, themePaletteGenerate, generateTonalPalette, listThemes } from "./theme/theme.mjs";
50
+ export { exportSearchIndex, rankSearchIndex } from "./search/search-index.mjs";
50
51
  export { integrationAdd, integrationAddAgentDoc, integrationAddCodemod, integrationAddComponent, integrationAddDoc, integrationAddTemplate } from "./integration/add-contribution.mjs";
51
52
  export { validateIntegration, summarizeIssues } from "./integration/validate-integration.mjs";
52
53
  export { integrationTemplateConflicts, integrationComponentConflicts, integrationDocConflicts } from "./integration/authoring-checks.mjs";
package/api/index.mjs CHANGED
@@ -48,6 +48,7 @@ export {
48
48
  } from './theme/theme.mjs';
49
49
  export {hook} from './hook/hook.mjs';
50
50
  export {search} from './search/search.mjs';
51
+ export {exportSearchIndex, rankSearchIndex} from './search/search-index.mjs';
51
52
  export {build} from './build/build.mjs';
52
53
  export {swizzle} from './swizzle/swizzle.mjs';
53
54
  export {gapReport} from './gap-report/gap-report.mjs';
@@ -548,6 +548,15 @@ export function reverseToFile({cwd, input, output, overwrite}) {
548
548
  ...(missing.length > 0 ? {missing} : {}),
549
549
  };
550
550
  }
551
+ // A file a render wrote, whose source block was taken out on the way
552
+ // (sanitizers and editors strip <script> elements), says so.
553
+ if (!isAstryxMarkup(html) && /<meta name="generator" content="Astryx[ "]/.test(html)) {
554
+ throw new AstryxError(
555
+ `${shown(cwd, input)} was rendered by Astryx, but its embedded source was removed (a sanitizer or an editor strips <script> blocks) and it has no Astryx components to rebuild from; nothing was written. Render the page again from its source and send the new file as is.`,
556
+ [],
557
+ ERROR_CODES.ERR_RENDER_SOURCE_DAMAGED,
558
+ );
559
+ }
551
560
  if (!isAstryxMarkup(html)) {
552
561
  throw new AstryxError(
553
562
  `${shown(cwd, input)} is not an Astryx page: it has no embedded source and no Astryx components to rebuild from. To start an Astryx page like it, run astryx build "<what the page is>".`,
@@ -99,9 +99,9 @@ export const doc = {
99
99
  {code: 'ERR_RENDER_FAILED', when: 'the page throws while rendering for any other reason'},
100
100
  {
101
101
  code: 'ERR_RENDER_NOT_ASTRYX',
102
- when: 'reversing a file with no embedded source and no Astryx markup; the suggestion is `astryx build "<what the page is>"`',
102
+ when: 'reversing a file a render did not write, with no embedded source and no Astryx markup; the suggestion is `astryx build "<what the page is>"`',
103
103
  },
104
- {code: 'ERR_RENDER_SOURCE_DAMAGED', when: 'reversing a file whose embedded source is damaged, lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds (a dotfile or dot-folder, `node_modules`, a package manifest, lockfile or tool configuration, or any kind but a page module, stylesheet, JSON or image)'},
104
+ {code: 'ERR_RENDER_SOURCE_DAMAGED', when: 'reversing a file a render wrote whose embedded source was removed (and that has no Astryx components to rebuild from), a file whose embedded source is damaged, lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds (a dotfile or dot-folder, `node_modules`, a package manifest, lockfile or tool configuration, or any kind but a page module, stylesheet, JSON or image)'},
105
105
  {code: 'ERR_PATH_TRAVERSAL', when: 'a file would be written outside the current folder (symlinks followed), or through a symbolic link: `out`, or with `reverse` the page or an embedded file; or, with `reverse`, a file other than the page would go into a folder that already exists'},
106
106
  ],
107
107
  examples: [
@@ -16,7 +16,7 @@ export function requireSearchCoreDir(cwd: string): string;
16
16
  * in the CLI's searches. A caller that runs its own operation shares it.
17
17
  *
18
18
  * @param {{cwd?: string, type?: string}} options
19
- * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null}>}
19
+ * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null, coreSkippedCode: string | null}>}
20
20
  */
21
21
  export function gatherCandidates(options: {
22
22
  cwd?: string;
@@ -27,7 +27,28 @@ export function gatherCandidates(options: {
27
27
  searchedComponents: boolean;
28
28
  outside: boolean;
29
29
  coreSkipped: string | null;
30
+ coreSkippedCode: string | null;
30
31
  }>;
32
+ /**
33
+ * The Core a search of `type` reads, and the domains it gathers. `astryx docs`
34
+ * reads docs without @astryxdesign/core, and `astryx theme list` reads themes
35
+ * without it, so a docs or themes search never looks for Core. Every other
36
+ * domain reads Core: the project's, else the answering release's own,
37
+ * installed into the per-user cache on first need (spec AST-081 FR3). A Core
38
+ * domain asked for by name throws the lookup's coded error. An open search
39
+ * without Core gathers the docs and themes alone and records why in
40
+ * `coreSkipped`; it never fails silently.
41
+ * @param {string | undefined} type
42
+ * @param {(cwd: string) => string} lookup finds the Core folder, or throws
43
+ * @param {string} cwd
44
+ * @returns {{coreDir: string | null, coreSkipped: string | null, coreSkippedCode: string | null, wants: (domain: string) => boolean}}
45
+ */
46
+ export function resolveSearchCore(type: string | undefined, lookup: (cwd: string) => string, cwd: string): {
47
+ coreDir: string | null;
48
+ coreSkipped: string | null;
49
+ coreSkippedCode: string | null;
50
+ wants: (domain: string) => boolean;
51
+ };
31
52
  /**
32
53
  * gatherCandidates, reused within one process: a later search in the same
33
54
  * folder, for the same `type`, ranks the candidates an earlier search
@@ -43,46 +43,62 @@ export function requireSearchCoreDir(cwd) {
43
43
  * in the CLI's searches. A caller that runs its own operation shares it.
44
44
  *
45
45
  * @param {{cwd?: string, type?: string}} options
46
- * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null}>}
46
+ * @returns {Promise<{all: Candidate[], components: Candidate[], searchedComponents: boolean, outside: boolean, coreSkipped: string | null, coreSkippedCode: string | null}>}
47
47
  */
48
48
  export function gatherCandidates(options) {
49
49
  return withListingCache(() => gatherDomains(options));
50
50
  }
51
51
 
52
52
  /**
53
- * @param {{cwd?: string, type?: string}} options
53
+ * The Core a search of `type` reads, and the domains it gathers. `astryx docs`
54
+ * reads docs without @astryxdesign/core, and `astryx theme list` reads themes
55
+ * without it, so a docs or themes search never looks for Core. Every other
56
+ * domain reads Core: the project's, else the answering release's own,
57
+ * installed into the per-user cache on first need (spec AST-081 FR3). A Core
58
+ * domain asked for by name throws the lookup's coded error. An open search
59
+ * without Core gathers the docs and themes alone and records why in
60
+ * `coreSkipped`; it never fails silently.
61
+ * @param {string | undefined} type
62
+ * @param {(cwd: string) => string} lookup finds the Core folder, or throws
63
+ * @param {string} cwd
64
+ * @returns {{coreDir: string | null, coreSkipped: string | null, coreSkippedCode: string | null, wants: (domain: string) => boolean}}
54
65
  */
55
- async function gatherDomains(options) {
56
- const {cwd = process.cwd(), type} = options;
57
- // `astryx docs` reads docs without @astryxdesign/core, and `astryx theme
58
- // list` reads themes without it (bundled themes need no project), so a
59
- // search of either must too. Every other domain reads core: asked for by
60
- // name, it is an error without core; an open search then covers the docs
61
- // and themes alone.
66
+ export function resolveSearchCore(type, lookup, cwd) {
62
67
  const needsCore = !type || !CORELESS_DOMAINS.includes(type);
63
- // A domain that reads Core gets it as `component` does: the project's, else
64
- // the answering release's own, installed into the per-user cache on first
65
- // need (spec AST-081 FR3). Asked for by name, a Core that cannot be had is
66
- // the error; an open search answers from the rest and says what it skipped
67
- // and why, never silently.
68
68
  /** @type {string | null} */
69
69
  let coreDir = null;
70
70
  /** @type {string | null} */
71
71
  let coreSkipped = null;
72
+ /** @type {string | null} */
73
+ let coreSkippedCode = null;
72
74
  if (needsCore && type) {
73
- coreDir = coreDirOrThrow(requireSearchCoreDir, cwd);
75
+ coreDir = coreDirOrThrow(lookup, cwd);
74
76
  } else if (needsCore) {
75
77
  try {
76
- coreDir = requireSearchCoreDir(cwd);
78
+ coreDir = lookup(cwd);
77
79
  } catch (error) {
78
80
  coreSkipped = /** @type {Error} */ (error).message;
81
+ const code = /** @type {any} */ (error)?.code;
82
+ coreSkippedCode = typeof code === 'string' ? code : null;
79
83
  }
80
84
  }
85
+ /** @param {string} domain */
86
+ const wants = domain =>
87
+ (!type && (coreDir != null || CORELESS_DOMAINS.includes(domain))) ||
88
+ type === domain;
89
+ return {coreDir, coreSkipped, coreSkippedCode, wants};
90
+ }
81
91
 
82
- // Gather candidates from each requested domain in parallel.
83
- /** @param {string} d */
84
- const wants = d =>
85
- (!type && (coreDir != null || CORELESS_DOMAINS.includes(d))) || type === d;
92
+ /**
93
+ * @param {{cwd?: string, type?: string}} options
94
+ */
95
+ async function gatherDomains(options) {
96
+ const {cwd = process.cwd(), type} = options;
97
+ const {coreDir, coreSkipped, coreSkippedCode, wants} = resolveSearchCore(
98
+ type,
99
+ requireSearchCoreDir,
100
+ cwd,
101
+ );
86
102
  const [components, hooks, docTopics, templates, themes] = await Promise.all([
87
103
  wants('component')
88
104
  ? gatherComponents(/** @type {string} */ (coreDir), cwd)
@@ -103,6 +119,7 @@ async function gatherDomains(options) {
103
119
  searchedComponents: wants('component'),
104
120
  outside,
105
121
  coreSkipped,
122
+ coreSkippedCode,
106
123
  };
107
124
  }
108
125
 
@@ -0,0 +1,10 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in api/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * @file FunctionDoc for `exportSearchIndex()`. Colocated with the API function it
6
+ * documents; the shape source of truth stays in `search.type.mjs`.
7
+ * @position packages/cli/api/search — function documentation
8
+ */
9
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
10
+ export const doc: import("@astryxdesign/cli/authoring").FunctionDoc;
@@ -0,0 +1,58 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file FunctionDoc for `exportSearchIndex()`. Colocated with the API function it
5
+ * documents; the shape source of truth stays in `search.type.mjs`.
6
+ * @position packages/cli/api/search — function documentation
7
+ */
8
+
9
+ /** @type {import('@astryxdesign/cli/authoring').FunctionDoc} */
10
+ export const doc = {
11
+ type: 'function',
12
+ kind: 'api',
13
+ name: 'exportSearchIndex',
14
+ namespace: 'cli/api',
15
+ stability: 'experimental',
16
+ displayName: 'exportSearchIndex()',
17
+ summary:
18
+ 'The candidates search ranks in a folder, as plain JSON to store and rank later with rankSearchIndex().',
19
+ description:
20
+ "Gathers what `search()` would rank in `cwd` and returns it as `{version, candidates}`. Gathering reads each installed package's docs and loads its integration manifest, which runs that package's code, so a catalog that lists packages it must not load makes the index where loading is safe (a folder with those packages installed), stores it, and ranks it with {@link function:rankSearchIndex}. With `package`, the index keeps only that package's candidates, for a catalog that stores one index per package. A component description's inline doc tokens are resolved where the index is made, as `search` prints them; when Core could not be had there, the index says why in `coreSkipped`. Fields that point into the folder are left out, so an index reads the same wherever it was made. The candidates are the ranker's input, not a stable shape: an index is ranked only by the CLI version that made it. Experimental: it may change or be removed in a patch release.",
21
+ importPath: '@astryxdesign/cli/api',
22
+ signature: 'exportSearchIndex(options?: {cwd?: string, package?: string}): Promise<SearchIndex>',
23
+ keywords: ['search index', 'stored search index'],
24
+ params: [
25
+ {
26
+ name: 'options.cwd',
27
+ type: 'string',
28
+ description: 'The folder to gather from, read as `search()` reads it.',
29
+ default: 'process.cwd()',
30
+ },
31
+ {
32
+ name: 'options.package',
33
+ type: 'string',
34
+ description:
35
+ "Keep only the candidates this npm package owns: its components, templates, themes, and doc parts. Without it the index holds everything the folder's search ranks, Core's entries and the CLI's docs included.",
36
+ },
37
+ ],
38
+ returns: [
39
+ {
40
+ type: 'SearchIndex',
41
+ description:
42
+ '`version`, the CLI version that made it, and `candidates`, the gathered entries as plain JSON.',
43
+ },
44
+ ],
45
+ throws: [
46
+ {
47
+ code: 'ERR_INVALID_ARGUMENT',
48
+ when: '`package` is given and is not a non-empty string',
49
+ },
50
+ ],
51
+ examples: [
52
+ {
53
+ label: 'Index one installed integration',
54
+ code: "const index = await exportSearchIndex({cwd: '/tmp/harness', package: '@acme/ui'});",
55
+ },
56
+ ],
57
+ related: ['search', 'rankSearchIndex'],
58
+ };