@forgebook/react 0.1.8 → 0.2.1
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 +5 -1
- package/dist/src/StoryErrorBoundary.d.ts +0 -1
- package/dist/src/StoryErrorBoundary.js +0 -1
- package/dist/src/clock.d.ts +0 -1
- package/dist/src/clock.js +0 -1
- package/dist/src/mountFrame.d.ts +0 -1
- package/dist/src/mountFrame.js +2 -3
- package/dist/src/reactFrameRenderer.d.ts +16 -0
- package/dist/src/reactFrameRenderer.js +43 -0
- package/dist/src/reactRenderer.d.ts +5 -7
- package/dist/src/reactRenderer.js +15 -71
- package/dist/src/stories.d.ts +0 -1
- package/dist/src/stories.js +0 -1
- package/dist/src/storyElement.d.ts +17 -2
- package/dist/src/storyElement.js +64 -7
- package/dist/src/storyJsx.d.ts +1 -16
- package/dist/src/storyJsx.js +5 -41
- package/dist/src/storyProviders.d.ts +15 -0
- package/dist/src/storyProviders.js +11 -0
- package/dist/src/storybookHooks.d.ts +74 -0
- package/dist/src/storybookHooks.js +82 -0
- package/dist/src/texts.d.ts +0 -1
- package/dist/src/texts.js +0 -1
- package/dist/src/vite/extractDocs.d.ts +13 -18
- package/dist/src/vite/extractDocs.js +6 -496
- package/dist/src/vite/react.d.ts +6 -3
- package/dist/src/vite/react.js +15 -7
- package/dist/src/vite/refreshAtOnce.d.ts +25 -0
- package/dist/src/vite/refreshAtOnce.js +40 -0
- package/dist/src/vite/refreshThroughCasts.d.ts +19 -0
- package/dist/src/vite/refreshThroughCasts.js +26 -0
- package/dist/src/vite/storybookPreviewApi.d.ts +14 -0
- package/dist/src/vite/storybookPreviewApi.js +94 -0
- package/package.json +13 -5
- package/dist/src/StoryErrorBoundary.d.ts.map +0 -1
- package/dist/src/StoryErrorBoundary.js.map +0 -1
- package/dist/src/clock.d.ts.map +0 -1
- package/dist/src/clock.js.map +0 -1
- package/dist/src/mountFrame.d.ts.map +0 -1
- package/dist/src/mountFrame.js.map +0 -1
- package/dist/src/reactRenderer.d.ts.map +0 -1
- package/dist/src/reactRenderer.js.map +0 -1
- package/dist/src/stories.d.ts.map +0 -1
- package/dist/src/stories.js.map +0 -1
- package/dist/src/storyElement.d.ts.map +0 -1
- package/dist/src/storyElement.js.map +0 -1
- package/dist/src/storyJsx.d.ts.map +0 -1
- package/dist/src/storyJsx.js.map +0 -1
- package/dist/src/texts.d.ts.map +0 -1
- package/dist/src/texts.js.map +0 -1
- package/dist/src/vite/extractDocs.d.ts.map +0 -1
- package/dist/src/vite/extractDocs.js.map +0 -1
- package/dist/src/vite/extractSignature.d.ts +0 -19
- package/dist/src/vite/extractSignature.d.ts.map +0 -1
- package/dist/src/vite/extractSignature.js +0 -59
- package/dist/src/vite/extractSignature.js.map +0 -1
- package/dist/src/vite/react.d.ts.map +0 -1
- package/dist/src/vite/react.js.map +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
/** Where the context of the decorator or render being called is kept: one place, however many copies of the module. */
|
|
3
|
+
const CURRENT = Symbol.for("forgebook.react.storybook-hooks");
|
|
4
|
+
/** Whether a value is the context `withStoryHooks` keeps. */
|
|
5
|
+
const isStoryHooksContext = (value) => typeof value === "object" &&
|
|
6
|
+
value !== null &&
|
|
7
|
+
typeof Reflect.get(value, "id") === "string" &&
|
|
8
|
+
typeof Reflect.get(value, "changeArgs") === "function";
|
|
9
|
+
/**
|
|
10
|
+
* Calls a story's decorator or render with the context Storybook's hooks read while it runs.
|
|
11
|
+
*
|
|
12
|
+
* @param context - what the story is drawn with.
|
|
13
|
+
* @param call - the decorator's or render's call.
|
|
14
|
+
* @returns what it returns.
|
|
15
|
+
*/
|
|
16
|
+
export const withStoryHooks = (context, call) => {
|
|
17
|
+
const outer = Reflect.get(globalThis, CURRENT);
|
|
18
|
+
Reflect.set(globalThis, CURRENT, context);
|
|
19
|
+
try {
|
|
20
|
+
return call();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
Reflect.set(globalThis, CURRENT, outer);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The context of the decorator or render being called.
|
|
28
|
+
*
|
|
29
|
+
* @throws outside one, with the message Storybook gives.
|
|
30
|
+
*/
|
|
31
|
+
const currentContext = () => {
|
|
32
|
+
const current = Reflect.get(globalThis, CURRENT);
|
|
33
|
+
if (!isStoryHooksContext(current))
|
|
34
|
+
throw new Error("Storybook preview hooks can only be called inside decorators and story functions.");
|
|
35
|
+
return current;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The story's args, and how to change them, as the panel would: `updateArgs` sets values, `resetArgs` puts the named
|
|
39
|
+
* ones (every one, with none named) back to the story's own.
|
|
40
|
+
*
|
|
41
|
+
* @returns the args, the update and the reset.
|
|
42
|
+
*/
|
|
43
|
+
export const useArgs = () => {
|
|
44
|
+
const { args, changeArgs } = currentContext();
|
|
45
|
+
const updateArgs = useCallback((set) => changeArgs({ reset: [], set }), [changeArgs]);
|
|
46
|
+
const resetArgs = useCallback((names) => changeArgs({ reset: names ?? "all", set: {} }), [changeArgs]);
|
|
47
|
+
return [args, updateArgs, resetArgs];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The values of the catalogue's switches, and Storybook's way to change them, which throws: they are set from the
|
|
51
|
+
* catalogue's toolbar.
|
|
52
|
+
*
|
|
53
|
+
* @returns the values and the change.
|
|
54
|
+
*/
|
|
55
|
+
export const useGlobals = () => {
|
|
56
|
+
const { globals } = currentContext();
|
|
57
|
+
const updateGlobals = useCallback(() => {
|
|
58
|
+
throw new Error("A story cannot change Forgebook's switches: they are set from the catalogue's toolbar");
|
|
59
|
+
}, []);
|
|
60
|
+
return [globals, updateGlobals];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* One of the story's parameters.
|
|
64
|
+
*
|
|
65
|
+
* @param key - the parameter's name.
|
|
66
|
+
* @param defaultValue - what to give when the story has none.
|
|
67
|
+
*/
|
|
68
|
+
export const useParameter = (key, defaultValue) => currentContext().parameters[key] ?? defaultValue;
|
|
69
|
+
/**
|
|
70
|
+
* The context the decorator or render was called with.
|
|
71
|
+
*
|
|
72
|
+
* @returns the context.
|
|
73
|
+
*/
|
|
74
|
+
export const useStoryContext = () => currentContext().context;
|
|
75
|
+
/**
|
|
76
|
+
* Storybook's channel to its addons, which Forgebook does not have.
|
|
77
|
+
*
|
|
78
|
+
* @throws always, saying so.
|
|
79
|
+
*/
|
|
80
|
+
export const useChannel = () => {
|
|
81
|
+
throw new Error("Storybook's channel does not exist in Forgebook: a story cannot talk to Storybook's addons");
|
|
82
|
+
};
|
package/dist/src/texts.d.ts
CHANGED
package/dist/src/texts.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs module of `@forgebook/react`: the plugin's worker and `forgebook-docs` load it through the adapter
|
|
3
|
+
* (`forgebook/vite/adapter`). A React component is a function of its props, read as every such framework's is
|
|
4
|
+
* (`forgebook/vite/function-component-docs`): its props from the first parameter's type, events as `on*` props, its
|
|
5
|
+
* parts from its `classNames`, `styles` and `refs` maps.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
1
9
|
import type { ComponentDocs, NoComponentDocs } from "forgebook/component-docs";
|
|
2
|
-
import type {
|
|
10
|
+
import type { ExtractFileComponent, ExtractStoryComponent } from "forgebook/vite/adapter";
|
|
3
11
|
/** The module a React story file imports its types from (`Meta`, `Story`), for the stories `forgebook new` writes. */
|
|
4
12
|
export declare const storyTypesModule = "@forgebook/react";
|
|
13
|
+
/** Reads a story file's component with the file that declares it. */
|
|
14
|
+
export declare const extractStoryComponent: ExtractStoryComponent;
|
|
15
|
+
/** Reads a component a file exports, for writing its first stories (`forgebook new`). */
|
|
16
|
+
export declare const extractFileComponent: ExtractFileComponent;
|
|
5
17
|
/**
|
|
6
18
|
* The docs of a story file's component.
|
|
7
19
|
*
|
|
@@ -10,20 +22,3 @@ export declare const storyTypesModule = "@forgebook/react";
|
|
|
10
22
|
* cannot be read.
|
|
11
23
|
*/
|
|
12
24
|
export declare const extractComponentDocs: (storyFile: string) => ComponentDocs | NoComponentDocs | string;
|
|
13
|
-
/**
|
|
14
|
-
* A story file's component with the file that declares it.
|
|
15
|
-
*
|
|
16
|
-
* @param storyFile - the story file's absolute path.
|
|
17
|
-
* @returns the component, `{ docs: { name: null } }` when the meta names none, or a message saying why the file
|
|
18
|
-
* cannot be read.
|
|
19
|
-
*/
|
|
20
|
-
export declare const extractStoryComponent: (storyFile: string) => StoryComponentResult;
|
|
21
|
-
/**
|
|
22
|
-
* A component a file exports, read from its types, for writing its first stories (`forgebook new`).
|
|
23
|
-
*
|
|
24
|
-
* @param file - the component's file, absolute.
|
|
25
|
-
* @param exportName - the export to read; the file's only component when none.
|
|
26
|
-
* @returns the component, or a message saying why it cannot be read (no component, several, a name not exported).
|
|
27
|
-
*/
|
|
28
|
-
export declare const extractFileComponent: (file: string, exportName?: string) => StoryComponentResult;
|
|
29
|
-
//# sourceMappingURL=extractDocs.d.ts.map
|
|
@@ -1,335 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
* The component of a React story file, read from its types and TSDoc: its props with their kind (the panel's group
|
|
3
|
-
* and control), its events' signatures and its parts. The docs module of `@forgebook/react`: the plugin's worker and
|
|
4
|
-
* `forgebook-docs` load it through the adapter (`forgebook/vite/adapter`).
|
|
5
|
-
*
|
|
6
|
-
* @remarks
|
|
7
|
-
* The component is the meta's `component`. Its props are the first parameter's properties declared in the package:
|
|
8
|
-
* what it passes on to the DOM (`ComponentProps<"button">`) comes from `node_modules` and is left out. Parts are the
|
|
9
|
-
* keys of the `classNames`, `styles` and `refs` maps (the reach-every-part rule).
|
|
10
|
-
*
|
|
11
|
-
* @module
|
|
12
|
-
*/
|
|
13
|
-
import { dirname } from "node:path";
|
|
14
|
-
import { moduleSummaryOf, programFor, summaryOf, tagOf } from "forgebook/vite/docs-program";
|
|
15
|
-
import { metaComponent, metaField } from "forgebook/vite/story-meta";
|
|
16
|
-
import ts from "typescript-6";
|
|
17
|
-
import { signatureDocs } from "./extractSignature.js";
|
|
1
|
+
import { functionComponentDocs, functionFileComponent, functionStoryComponent } from "forgebook/vite/function-component-docs";
|
|
18
2
|
/** The module a React story file imports its types from (`Meta`, `Story`), for the stories `forgebook new` writes. */
|
|
19
3
|
export const storyTypesModule = "@forgebook/react";
|
|
20
|
-
/**
|
|
21
|
-
const
|
|
22
|
-
/**
|
|
23
|
-
const
|
|
24
|
-
/**
|
|
25
|
-
* The subcomponents the meta names, in the order it writes them: `subcomponents: { ChipList, Items: MenuItem }`.
|
|
26
|
-
*
|
|
27
|
-
* @param source - the story file, parsed.
|
|
28
|
-
* @param checker - the program's checker.
|
|
29
|
-
*/
|
|
30
|
-
const metaSubcomponents = (source, checker) => {
|
|
31
|
-
const field = metaField(source, "subcomponents");
|
|
32
|
-
const object = field === undefined ? undefined : unwrapped(field);
|
|
33
|
-
if (object === undefined || !ts.isObjectLiteralExpression(object))
|
|
34
|
-
return [];
|
|
35
|
-
return object.properties.flatMap((property) => {
|
|
36
|
-
if (ts.isShorthandPropertyAssignment(property)) {
|
|
37
|
-
const symbol = checker.getShorthandAssignmentValueSymbol(property);
|
|
38
|
-
return [{ name: property.name.text, expression: property.name, symbol }];
|
|
39
|
-
}
|
|
40
|
-
if (!ts.isPropertyAssignment(property))
|
|
41
|
-
return [];
|
|
42
|
-
const name = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
|
|
43
|
-
? property.name.text
|
|
44
|
-
: property.name.getText(source);
|
|
45
|
-
const symbol = checker.getSymbolAtLocation(property.initializer);
|
|
46
|
-
return [{ name, expression: property.initializer, symbol }];
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
/** Whether a type is a union of string or number literals (a choice). */
|
|
50
|
-
const literalValues = (type) => {
|
|
51
|
-
if (!type.isUnion())
|
|
52
|
-
return undefined;
|
|
53
|
-
const values = type.types.map((member) => member.isStringLiteral() || member.isNumberLiteral() ? member.value : undefined);
|
|
54
|
-
return values.every((value) => value !== undefined)
|
|
55
|
-
? values.filter((value) => value !== undefined)
|
|
56
|
-
: undefined;
|
|
57
|
-
};
|
|
58
|
-
/** A literal value an expression writes (`"md"`, `3`, `-1`), or none for anything else. */
|
|
59
|
-
const literalOfExpression = (expression) => {
|
|
60
|
-
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression))
|
|
61
|
-
return expression.text;
|
|
62
|
-
if (ts.isNumericLiteral(expression))
|
|
63
|
-
return Number(expression.text);
|
|
64
|
-
if (ts.isPrefixUnaryExpression(expression) &&
|
|
65
|
-
expression.operator === ts.SyntaxKind.MinusToken &&
|
|
66
|
-
ts.isNumericLiteral(expression.operand)) {
|
|
67
|
-
return -Number(expression.operand.text);
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
};
|
|
71
|
-
/** An expression without the wrappers around a literal list (`as const`, `satisfies`, parentheses). */
|
|
72
|
-
const unwrapped = (expression) => {
|
|
73
|
-
let inner = expression;
|
|
74
|
-
while (ts.isAsExpression(inner) || ts.isSatisfiesExpression(inner) || ts.isParenthesizedExpression(inner)) {
|
|
75
|
-
inner = inner.expression;
|
|
76
|
-
}
|
|
77
|
-
return inner;
|
|
78
|
-
};
|
|
79
|
-
/** A symbol, through an import to what it names. */
|
|
80
|
-
const declaredSymbol = (symbol, checker) => symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
|
|
81
|
-
/** The value a literal type node writes (`"md"`, `3`, `-1`), or none for another literal (`true`, `null`). */
|
|
82
|
-
const literalTypeValue = (node) => {
|
|
83
|
-
const literal = node.literal;
|
|
84
|
-
const value = ts.isLiteralExpression(literal) || ts.isPrefixUnaryExpression(literal)
|
|
85
|
-
? literalOfExpression(literal)
|
|
86
|
-
: undefined;
|
|
87
|
-
return value === undefined ? [] : [value];
|
|
88
|
-
};
|
|
89
|
-
/** The values a tuple query writes (`(typeof SIZES)[number]` with `SIZES = ["md", "sm"] as const`), in its order. */
|
|
90
|
-
const tupleValues = (node, checker) => {
|
|
91
|
-
let object = node.objectType;
|
|
92
|
-
while (ts.isParenthesizedTypeNode(object))
|
|
93
|
-
object = object.type;
|
|
94
|
-
if (!ts.isTypeQueryNode(object))
|
|
95
|
-
return [];
|
|
96
|
-
const variable = declaredSymbol(checker.getSymbolAtLocation(object.exprName), checker)?.valueDeclaration;
|
|
97
|
-
if (variable === undefined || !ts.isVariableDeclaration(variable) || variable.initializer === undefined)
|
|
98
|
-
return [];
|
|
99
|
-
const list = unwrapped(variable.initializer);
|
|
100
|
-
if (!ts.isArrayLiteralExpression(list))
|
|
101
|
-
return [];
|
|
102
|
-
return list.elements.flatMap((element) => {
|
|
103
|
-
const value = literalOfExpression(element);
|
|
104
|
-
return value === undefined ? [] : [value];
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
/** The keys an object query writes (`keyof typeof TEXT_STYLES`), in the order the object writes them. */
|
|
108
|
-
const keyValues = (node, checker) => {
|
|
109
|
-
if (node.operator !== ts.SyntaxKind.KeyOfKeyword || !ts.isTypeQueryNode(node.type))
|
|
110
|
-
return [];
|
|
111
|
-
const variable = declaredSymbol(checker.getSymbolAtLocation(node.type.exprName), checker)?.valueDeclaration;
|
|
112
|
-
if (variable === undefined || !ts.isVariableDeclaration(variable) || variable.initializer === undefined)
|
|
113
|
-
return [];
|
|
114
|
-
const object = unwrapped(variable.initializer);
|
|
115
|
-
if (!ts.isObjectLiteralExpression(object))
|
|
116
|
-
return [];
|
|
117
|
-
return object.properties.flatMap((property) => {
|
|
118
|
-
const name = property.name;
|
|
119
|
-
return name !== undefined && (ts.isIdentifier(name) || ts.isStringLiteral(name)) ? [name.text] : [];
|
|
120
|
-
});
|
|
121
|
-
};
|
|
122
|
-
/**
|
|
123
|
-
* The values an indexed type writes: a tuple's members (`(typeof SIZES)[number]`), or another type's prop as that
|
|
124
|
-
* type declares it (`PopoverProps["side"]`).
|
|
125
|
-
*
|
|
126
|
-
* @param node - the indexed type.
|
|
127
|
-
* @param checker - the type checker.
|
|
128
|
-
* @param seen - the nodes already walked.
|
|
129
|
-
*/
|
|
130
|
-
const accessedValues = (node, checker, seen) => {
|
|
131
|
-
const index = node.indexType;
|
|
132
|
-
if (!ts.isLiteralTypeNode(index) || !ts.isStringLiteral(index.literal))
|
|
133
|
-
return tupleValues(node, checker);
|
|
134
|
-
const prop = checker.getPropertyOfType(checker.getTypeFromTypeNode(node.objectType), index.literal.text);
|
|
135
|
-
return (prop?.declarations ?? []).flatMap((declaration) => ts.isPropertySignature(declaration) && declaration.type !== undefined
|
|
136
|
-
? writtenLiterals(declaration.type, checker, seen)
|
|
137
|
-
: []);
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* The literal values a type node writes, in the order it writes them: through unions, aliases and a tuple's members.
|
|
141
|
-
*
|
|
142
|
-
* @param node - the type node.
|
|
143
|
-
* @param checker - the type checker, to follow a name to its declaration.
|
|
144
|
-
* @param seen - the nodes already walked, so a recursive alias ends.
|
|
145
|
-
*/
|
|
146
|
-
const writtenLiterals = (node, checker, seen) => {
|
|
147
|
-
if (seen.has(node))
|
|
148
|
-
return [];
|
|
149
|
-
seen.add(node);
|
|
150
|
-
if (ts.isParenthesizedTypeNode(node))
|
|
151
|
-
return writtenLiterals(node.type, checker, seen);
|
|
152
|
-
if (ts.isUnionTypeNode(node))
|
|
153
|
-
return node.types.flatMap((member) => writtenLiterals(member, checker, seen));
|
|
154
|
-
if (ts.isLiteralTypeNode(node))
|
|
155
|
-
return literalTypeValue(node);
|
|
156
|
-
if (ts.isIndexedAccessTypeNode(node))
|
|
157
|
-
return accessedValues(node, checker, seen);
|
|
158
|
-
if (ts.isTypeOperatorNode(node))
|
|
159
|
-
return keyValues(node, checker);
|
|
160
|
-
if (!ts.isTypeReferenceNode(node))
|
|
161
|
-
return [];
|
|
162
|
-
// a generic's arguments first (`Exclude<TableVariant, "changes">`): the values it keeps come from them
|
|
163
|
-
const fromArguments = (node.typeArguments ?? []).flatMap((argument) => writtenLiterals(argument, checker, seen));
|
|
164
|
-
const alias = declaredSymbol(checker.getSymbolAtLocation(node.typeName), checker)?.declarations?.find(ts.isTypeAliasDeclaration);
|
|
165
|
-
return [...fromArguments, ...(alias === undefined ? [] : writtenLiterals(alias.type, checker, seen))];
|
|
166
|
-
};
|
|
167
|
-
/**
|
|
168
|
-
* A choice's values in the order the source writes them: the checker's order follows the order it happened to meet
|
|
169
|
-
* the types in, which a program kept between reads changes from one read to the next.
|
|
170
|
-
*
|
|
171
|
-
* @param values - the values, in the checker's order.
|
|
172
|
-
* @param symbol - the prop.
|
|
173
|
-
* @param checker - the type checker.
|
|
174
|
-
* @returns the values, those the source writes in its order first, any other after them in the checker's order.
|
|
175
|
-
*/
|
|
176
|
-
const writtenOrder = (values, symbol, checker) => {
|
|
177
|
-
const seen = new Set();
|
|
178
|
-
const written = declaredTypesOf(symbol).flatMap((node) => ts.isMethodSignature(node) ? [] : writtenLiterals(node, checker, seen));
|
|
179
|
-
const rank = (value) => {
|
|
180
|
-
const index = written.indexOf(value);
|
|
181
|
-
return index === -1 ? written.length : index;
|
|
182
|
-
};
|
|
183
|
-
return [...values].sort((left, right) => rank(left) - rank(right));
|
|
184
|
-
};
|
|
185
|
-
/** A choice's type as TypeScript writes it, its members in the given order (`"md" | "sm"`). */
|
|
186
|
-
const choiceTypeText = (values) => values.map((value) => (typeof value === "string" ? JSON.stringify(value) : String(value))).join(" | ");
|
|
187
|
-
/**
|
|
188
|
-
* The props in the order the source declares them: those declared in the component's own folder first, then the
|
|
189
|
-
* ones it takes from elsewhere, each by file and position. The checker's order follows the types it met first (an
|
|
190
|
-
* `Omit`'s keys are a union), so a program kept between reads changed it from one read to the next.
|
|
191
|
-
*
|
|
192
|
-
* @param props - the props.
|
|
193
|
-
* @param componentFolder - the folder of the file that declares the component.
|
|
194
|
-
*/
|
|
195
|
-
const declarationOrder = (props, componentFolder) => {
|
|
196
|
-
const keyOf = (prop) => {
|
|
197
|
-
const [declaration] = prop.declarations ?? [];
|
|
198
|
-
if (declaration === undefined)
|
|
199
|
-
return [2, "", 0];
|
|
200
|
-
const file = declaration.getSourceFile().fileName;
|
|
201
|
-
return [dirname(file) === componentFolder ? 0 : 1, file, declaration.getStart()];
|
|
202
|
-
};
|
|
203
|
-
return [...props].sort((left, right) => {
|
|
204
|
-
const [leftOwn, leftFile, leftAt] = keyOf(left);
|
|
205
|
-
const [rightOwn, rightFile, rightAt] = keyOf(right);
|
|
206
|
-
return leftOwn - rightOwn || leftFile.localeCompare(rightFile) || leftAt - rightAt;
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
/** A type node's text as the source writes it, on one line. */
|
|
210
|
-
const nodeText = (node) => node.getText().replace(/\s+/g, " ").trim();
|
|
211
|
-
/**
|
|
212
|
-
* Whether a type node adds nothing a prop can take: `undefined` and `null` (left out as the checker's `NonNullable`
|
|
213
|
-
* did) and `never` (a branch of the props where the prop is not allowed).
|
|
214
|
-
*/
|
|
215
|
-
const isNothing = (node) => node.kind === ts.SyntaxKind.UndefinedKeyword ||
|
|
216
|
-
node.kind === ts.SyntaxKind.NeverKeyword ||
|
|
217
|
-
(ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword);
|
|
218
|
-
/** A type node without its parentheses. */
|
|
219
|
-
const bareType = (node) => ts.isParenthesizedTypeNode(node) ? bareType(node.type) : node;
|
|
220
|
-
/**
|
|
221
|
-
* A prop's declared type without what adds nothing (`undefined`, `null`, `never`): the members of a written union, or
|
|
222
|
-
* the node itself.
|
|
223
|
-
*
|
|
224
|
-
* @param node - the prop's declared type.
|
|
225
|
-
*/
|
|
226
|
-
const declaredMembers = (node) => {
|
|
227
|
-
const bare = bareType(node);
|
|
228
|
-
return (ts.isUnionTypeNode(bare) ? bare.types : [bare]).filter((member) => !isNothing(member));
|
|
229
|
-
};
|
|
230
|
-
/**
|
|
231
|
-
* A prop's declared type nodes: a property's type or a method's whole signature, once per declaration. A prop of
|
|
232
|
-
* props written as alternatives (`{ indicator?: never } | { indicator?: "dot" | "spinner" }`) has one per branch.
|
|
233
|
-
*
|
|
234
|
-
* @param symbol - the prop.
|
|
235
|
-
*/
|
|
236
|
-
const declaredTypesOf = (symbol) => (symbol.declarations ?? []).flatMap((declaration) => {
|
|
237
|
-
if (ts.isMethodSignature(declaration))
|
|
238
|
-
return [declaration];
|
|
239
|
-
return ts.isPropertySignature(declaration) && declaration.type !== undefined ? [declaration.type] : [];
|
|
240
|
-
});
|
|
241
|
-
/**
|
|
242
|
-
* A prop's type as the source writes it (`ReactNode`, `Ref<HTMLElement>`), without `undefined` and `null`: the
|
|
243
|
-
* checker's own text expands it and orders a union's members by the order it met the types in, which a program kept
|
|
244
|
-
* between reads changes from one read to the next.
|
|
245
|
-
*
|
|
246
|
-
* @param symbol - the prop.
|
|
247
|
-
* @returns the text, or none when the prop writes no type (the caller falls back to the checker's).
|
|
248
|
-
*/
|
|
249
|
-
const writtenTypeText = (symbol) => {
|
|
250
|
-
const texts = declaredTypesOf(symbol).flatMap((declared) => {
|
|
251
|
-
if (!ts.isMethodSignature(declared))
|
|
252
|
-
return declaredMembers(declared).map(nodeText);
|
|
253
|
-
const parameters = declared.parameters.map(nodeText).join(", ");
|
|
254
|
-
return [`(${parameters}) => ${declared.type === undefined ? "void" : nodeText(declared.type)}`];
|
|
255
|
-
});
|
|
256
|
-
// each branch's members once, in the order the branches write them
|
|
257
|
-
const unique = [...new Set(texts)];
|
|
258
|
-
return unique.length === 0 ? undefined : unique.join(" | ");
|
|
259
|
-
};
|
|
260
|
-
/**
|
|
261
|
-
* An event's signature as the source writes it: a function type among the prop's declared members, or a method.
|
|
262
|
-
*
|
|
263
|
-
* @param symbol - the event prop.
|
|
264
|
-
* @returns the signature, or none when the source writes none in place (a named handler type).
|
|
265
|
-
*/
|
|
266
|
-
const writtenSignature = (symbol) => {
|
|
267
|
-
const nodes = declaredTypesOf(symbol).flatMap((declared) => ts.isMethodSignature(declared) ? [declared] : declaredMembers(declared));
|
|
268
|
-
const fn = nodes.find((node) => ts.isMethodSignature(node) || ts.isFunctionTypeNode(node));
|
|
269
|
-
if (fn === undefined)
|
|
270
|
-
return undefined;
|
|
271
|
-
return {
|
|
272
|
-
parameters: fn.parameters.map((parameter) => ({
|
|
273
|
-
name: nodeText(parameter.name),
|
|
274
|
-
type: parameter.type === undefined ? "unknown" : nodeText(parameter.type)
|
|
275
|
-
})),
|
|
276
|
-
returns: fn.type === undefined ? "void" : nodeText(fn.type)
|
|
277
|
-
};
|
|
278
|
-
};
|
|
279
|
-
/** An event's signature. */
|
|
280
|
-
const signatureOf = (type, checker, node) => {
|
|
281
|
-
const [signature] = type.getCallSignatures();
|
|
282
|
-
if (signature === undefined)
|
|
283
|
-
return undefined;
|
|
284
|
-
return {
|
|
285
|
-
parameters: signature.parameters.map((parameter) => ({
|
|
286
|
-
name: parameter.getName(),
|
|
287
|
-
type: checker.typeToString(checker.getTypeOfSymbolAtLocation(parameter, node))
|
|
288
|
-
})),
|
|
289
|
-
returns: checker.typeToString(signature.getReturnType())
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
|
-
/** A prop's kind, from its name and its type without `undefined`. */
|
|
293
|
-
const kindOf = (name, type) => {
|
|
294
|
-
if (EVENT_NAME.test(name) && type.getCallSignatures().length > 0)
|
|
295
|
-
return "event";
|
|
296
|
-
if (type.flags & ts.TypeFlags.Boolean || type.flags & ts.TypeFlags.BooleanLiteral)
|
|
297
|
-
return "toggle";
|
|
298
|
-
if (literalValues(type) !== undefined)
|
|
299
|
-
return "choice";
|
|
300
|
-
if (type.flags & ts.TypeFlags.String)
|
|
301
|
-
return "text";
|
|
302
|
-
if (type.flags & ts.TypeFlags.Number)
|
|
303
|
-
return "number";
|
|
304
|
-
return "other";
|
|
305
|
-
};
|
|
306
|
-
/** Whether a symbol is declared outside the package (a DOM attribute from React's types). */
|
|
307
|
-
const isForeign = (symbol) => (symbol.declarations ?? []).every((declaration) => declaration.getSourceFile().fileName.includes("/node_modules/"));
|
|
308
|
-
/** One prop's docs. */
|
|
309
|
-
const propDocs = (symbol, checker, node) => {
|
|
310
|
-
const type = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(symbol, node));
|
|
311
|
-
const name = symbol.getName();
|
|
312
|
-
const kind = kindOf(name, type);
|
|
313
|
-
const defaultValue = tagOf(symbol, checker, "defaultValue");
|
|
314
|
-
const checkerChoices = kind === "choice" ? literalValues(type) : undefined;
|
|
315
|
-
const choices = checkerChoices === undefined ? undefined : writtenOrder(checkerChoices, symbol, checker);
|
|
316
|
-
const signature = kind === "event" ? (writtenSignature(symbol) ?? signatureOf(type, checker, node)) : undefined;
|
|
317
|
-
// a node takes a text: the panel can set it as one, which covers labels, titles and children
|
|
318
|
-
const canTakeText = kind === "other" && checker.isTypeAssignableTo(checker.getStringType(), type);
|
|
319
|
-
return {
|
|
320
|
-
name,
|
|
321
|
-
kind,
|
|
322
|
-
type: choices === undefined
|
|
323
|
-
? (writtenTypeText(symbol) ?? checker.typeToString(type))
|
|
324
|
-
: choiceTypeText(choices),
|
|
325
|
-
description: summaryOf(symbol, checker),
|
|
326
|
-
isRequired: (symbol.flags & ts.SymbolFlags.Optional) === 0,
|
|
327
|
-
...(defaultValue === undefined ? {} : { defaultValue }),
|
|
328
|
-
...(choices === undefined ? {} : { choices }),
|
|
329
|
-
...(signature === undefined ? {} : { signature }),
|
|
330
|
-
...(canTakeText ? { canTakeText } : {})
|
|
331
|
-
};
|
|
332
|
-
};
|
|
4
|
+
/** Reads a story file's component with the file that declares it. */
|
|
5
|
+
export const extractStoryComponent = functionStoryComponent;
|
|
6
|
+
/** Reads a component a file exports, for writing its first stories (`forgebook new`). */
|
|
7
|
+
export const extractFileComponent = functionFileComponent;
|
|
333
8
|
/**
|
|
334
9
|
* The docs of a story file's component.
|
|
335
10
|
*
|
|
@@ -337,169 +12,4 @@ const propDocs = (symbol, checker, node) => {
|
|
|
337
12
|
* @returns the component's docs, `{ name: null }` when the meta names no component, or a message saying why the file
|
|
338
13
|
* cannot be read.
|
|
339
14
|
*/
|
|
340
|
-
export const extractComponentDocs = (storyFile) =>
|
|
341
|
-
const component = extractStoryComponent(storyFile);
|
|
342
|
-
return typeof component === "string" ? component : component.docs;
|
|
343
|
-
};
|
|
344
|
-
/**
|
|
345
|
-
* A component's props, read at the expression that names it.
|
|
346
|
-
*
|
|
347
|
-
* @param symbol - the component's symbol, through its import.
|
|
348
|
-
* @param expression - where the code names it.
|
|
349
|
-
* @param checker - the program's checker.
|
|
350
|
-
* @param storyFile - the story file, the folder when the component has no declaration.
|
|
351
|
-
*/
|
|
352
|
-
const componentProps = (symbol, expression, checker, storyFile) => {
|
|
353
|
-
const [signature] = checker.getTypeOfSymbolAtLocation(symbol, expression).getCallSignatures();
|
|
354
|
-
const propsSymbol = signature?.parameters[0];
|
|
355
|
-
const propsType = propsSymbol === undefined ? undefined : checker.getTypeOfSymbolAtLocation(propsSymbol, expression);
|
|
356
|
-
const ownProps = declarationOrder((propsType?.getProperties() ?? []).filter((prop) => !isForeign(prop)), dirname(symbol.declarations?.[0]?.getSourceFile().fileName ?? storyFile));
|
|
357
|
-
return { propsType, ownProps };
|
|
358
|
-
};
|
|
359
|
-
/**
|
|
360
|
-
* The files a component's docs come from: the one declaring it and those declaring its own props.
|
|
361
|
-
*
|
|
362
|
-
* @param symbol - the component's symbol.
|
|
363
|
-
* @param ownProps - its own props.
|
|
364
|
-
* @param storyFile - the story file, when the component has no declaration.
|
|
365
|
-
*/
|
|
366
|
-
const docsSources = (symbol, ownProps, storyFile) => [
|
|
367
|
-
symbol.declarations?.[0]?.getSourceFile().fileName ?? storyFile,
|
|
368
|
-
...ownProps.flatMap((prop) => (prop.declarations ?? []).map((declaration) => declaration.getSourceFile().fileName))
|
|
369
|
-
];
|
|
370
|
-
/**
|
|
371
|
-
* The docs of the subcomponents a story file's meta names, with the files they come from.
|
|
372
|
-
*
|
|
373
|
-
* @param source - the story file, parsed.
|
|
374
|
-
* @param checker - the program's checker.
|
|
375
|
-
* @returns the docs and their files, or a message naming the subcomponent that cannot be resolved.
|
|
376
|
-
*/
|
|
377
|
-
const subcomponentsOf = (source, checker) => {
|
|
378
|
-
const docs = [];
|
|
379
|
-
const sources = [];
|
|
380
|
-
for (const subcomponent of metaSubcomponents(source, checker)) {
|
|
381
|
-
const symbol = declaredSymbol(subcomponent.symbol, checker);
|
|
382
|
-
if (symbol === undefined) {
|
|
383
|
-
return `${source.fileName}: the meta's subcomponent ${subcomponent.name} cannot be resolved`;
|
|
384
|
-
}
|
|
385
|
-
const { ownProps } = componentProps(symbol, subcomponent.expression, checker, source.fileName);
|
|
386
|
-
docs.push({
|
|
387
|
-
name: subcomponent.name,
|
|
388
|
-
description: summaryOf(symbol, checker),
|
|
389
|
-
props: ownProps.map((prop) => propDocs(prop, checker, subcomponent.expression))
|
|
390
|
-
});
|
|
391
|
-
sources.push(...docsSources(symbol, ownProps, source.fileName));
|
|
392
|
-
}
|
|
393
|
-
return { docs, sources };
|
|
394
|
-
};
|
|
395
|
-
/**
|
|
396
|
-
* A component's docs and the files they come from, read at the expression that names it.
|
|
397
|
-
*
|
|
398
|
-
* @param symbol - the component's declared symbol.
|
|
399
|
-
* @param expression - where the code names it (the meta's `component`, or the export's own name).
|
|
400
|
-
* @param checker - the program's checker.
|
|
401
|
-
* @param file - the file read, the folder when the component has no declaration.
|
|
402
|
-
* @param subcomponents - the subcomponents' docs and files; none outside a story file.
|
|
403
|
-
*/
|
|
404
|
-
const componentOf = (symbol, expression, checker, file, subcomponents) => {
|
|
405
|
-
const { propsType, ownProps } = componentProps(symbol, expression, checker, file);
|
|
406
|
-
const props = ownProps.map((prop) => propDocs(prop, checker, expression));
|
|
407
|
-
const parts = [
|
|
408
|
-
...new Set((propsType?.getProperties() ?? [])
|
|
409
|
-
.filter((prop) => PART_MAPS.includes(prop.getName()))
|
|
410
|
-
.flatMap((prop) => checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(prop, expression)).getProperties())
|
|
411
|
-
.map((part) => part.getName()))
|
|
412
|
-
];
|
|
413
|
-
const declarationFile = symbol.declarations?.[0]?.getSourceFile();
|
|
414
|
-
const sources = docsSources(symbol, ownProps, file);
|
|
415
|
-
const signature = signatureDocs(symbol, expression, checker);
|
|
416
|
-
return {
|
|
417
|
-
docs: {
|
|
418
|
-
name: symbol.getName(),
|
|
419
|
-
description: summaryOf(symbol, checker),
|
|
420
|
-
summary: declarationFile === undefined ? "" : moduleSummaryOf(declarationFile),
|
|
421
|
-
props,
|
|
422
|
-
parts,
|
|
423
|
-
subcomponents: subcomponents.docs,
|
|
424
|
-
...(signature === undefined ? {} : { signature })
|
|
425
|
-
},
|
|
426
|
-
file: sources[0] ?? file,
|
|
427
|
-
sources: [...new Set([...sources, ...subcomponents.sources])]
|
|
428
|
-
};
|
|
429
|
-
};
|
|
430
|
-
/**
|
|
431
|
-
* A story file's component with the file that declares it.
|
|
432
|
-
*
|
|
433
|
-
* @param storyFile - the story file's absolute path.
|
|
434
|
-
* @returns the component, `{ docs: { name: null } }` when the meta names none, or a message saying why the file
|
|
435
|
-
* cannot be read.
|
|
436
|
-
*/
|
|
437
|
-
export const extractStoryComponent = (storyFile) => {
|
|
438
|
-
const program = programFor(storyFile);
|
|
439
|
-
if (typeof program === "string")
|
|
440
|
-
return program;
|
|
441
|
-
const { checker } = program;
|
|
442
|
-
const source = program.program.getSourceFile(storyFile);
|
|
443
|
-
if (source === undefined)
|
|
444
|
-
return `${storyFile} is not in its program`;
|
|
445
|
-
const expression = metaComponent(source);
|
|
446
|
-
if (expression === undefined)
|
|
447
|
-
return { docs: { name: null } };
|
|
448
|
-
const symbol = declaredSymbol(checker.getSymbolAtLocation(expression), checker);
|
|
449
|
-
if (symbol === undefined)
|
|
450
|
-
return `${storyFile}: the meta's component cannot be resolved`;
|
|
451
|
-
const subcomponents = subcomponentsOf(source, checker);
|
|
452
|
-
if (typeof subcomponents === "string")
|
|
453
|
-
return subcomponents;
|
|
454
|
-
return componentOf(symbol, expression, checker, storyFile, subcomponents);
|
|
455
|
-
};
|
|
456
|
-
/** Whether an export is a component: a name in PascalCase whose value can be called. */
|
|
457
|
-
const isComponentExport = (symbol, checker, node) => /^[A-Z]/.test(symbol.getName()) &&
|
|
458
|
-
checker.getTypeOfSymbolAtLocation(symbol, node).getCallSignatures().length > 0;
|
|
459
|
-
/**
|
|
460
|
-
* The component asked for among a file's: the one named, or the only one.
|
|
461
|
-
*
|
|
462
|
-
* @returns the export, or a message naming the components when there is none or several.
|
|
463
|
-
*/
|
|
464
|
-
const chosenComponent = (components, exportName, file) => {
|
|
465
|
-
const chosen = exportName === undefined ? components : components.filter((each) => each.getName() === exportName);
|
|
466
|
-
const [only] = chosen;
|
|
467
|
-
if (only !== undefined && chosen.length === 1)
|
|
468
|
-
return only;
|
|
469
|
-
const names = components.map((each) => each.getName()).join(", ") || "none";
|
|
470
|
-
return exportName === undefined
|
|
471
|
-
? `${file} exports ${components.length === 0 ? "no component" : `several components (${names})`}: name one`
|
|
472
|
-
: `${file} exports no component ${exportName} (its components: ${names})`;
|
|
473
|
-
};
|
|
474
|
-
/**
|
|
475
|
-
* A component a file exports, read from its types, for writing its first stories (`forgebook new`).
|
|
476
|
-
*
|
|
477
|
-
* @param file - the component's file, absolute.
|
|
478
|
-
* @param exportName - the export to read; the file's only component when none.
|
|
479
|
-
* @returns the component, or a message saying why it cannot be read (no component, several, a name not exported).
|
|
480
|
-
*/
|
|
481
|
-
export const extractFileComponent = (file, exportName) => {
|
|
482
|
-
const program = programFor(file);
|
|
483
|
-
if (typeof program === "string")
|
|
484
|
-
return program;
|
|
485
|
-
const { checker } = program;
|
|
486
|
-
const source = program.program.getSourceFile(file);
|
|
487
|
-
if (source === undefined)
|
|
488
|
-
return `${file} is not in its program`;
|
|
489
|
-
const moduleSymbol = checker.getSymbolAtLocation(source);
|
|
490
|
-
if (moduleSymbol === undefined)
|
|
491
|
-
return `${file} exports nothing`;
|
|
492
|
-
const components = checker
|
|
493
|
-
.getExportsOfModule(moduleSymbol)
|
|
494
|
-
.filter((each) => isComponentExport(declaredSymbol(each, checker) ?? each, checker, source));
|
|
495
|
-
const export_ = chosenComponent(components, exportName, file);
|
|
496
|
-
if (typeof export_ === "string")
|
|
497
|
-
return export_;
|
|
498
|
-
const symbol = declaredSymbol(export_, checker) ?? export_;
|
|
499
|
-
const declaration = symbol.declarations?.[0];
|
|
500
|
-
const name = declaration === undefined ? undefined : ts.getNameOfDeclaration(declaration);
|
|
501
|
-
if (name === undefined || !ts.isIdentifier(name))
|
|
502
|
-
return `${file}: ${export_.getName()} has no name to read it at`;
|
|
503
|
-
return componentOf(symbol, name, checker, file, { docs: [], sources: [] });
|
|
504
|
-
};
|
|
505
|
-
//# sourceMappingURL=extractDocs.js.map
|
|
15
|
+
export const extractComponentDocs = (storyFile) => functionComponentDocs(storyFile);
|
package/dist/src/vite/react.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import type { Options as ReactPluginOptions } from "@vitejs/plugin-react";
|
|
1
2
|
import type { FrameworkAdapter } from "forgebook/vite/adapter";
|
|
2
3
|
/**
|
|
3
4
|
* React, as the plugin reads its components: the docs module the docs' worker loads is this package's
|
|
4
5
|
* `extractDocs.ts` (the props from the first parameter's type, events as `on*` props); the generated pages draw with
|
|
5
|
-
* its renderer and write code with its JSX writer; Vite gets React's plugin
|
|
6
|
+
* its renderer and write code with its JSX writer; Vite gets React's plugin, and a story file's import of Storybook's
|
|
7
|
+
* preview API reaches Forgebook's preview hooks.
|
|
6
8
|
*
|
|
9
|
+
* @param reactPlugin - the options of `@vitejs/plugin-react`, for a library whose files need them (JSX in `.js` files:
|
|
10
|
+
* `{ include: /\.[jt]sx?$/ }`); its defaults by default. Forgebook's own plugins around it stay.
|
|
7
11
|
* @returns the adapter.
|
|
8
12
|
*/
|
|
9
|
-
export declare const react: () => FrameworkAdapter;
|
|
10
|
-
//# sourceMappingURL=react.d.ts.map
|
|
13
|
+
export declare const react: (reactPlugin?: ReactPluginOptions) => FrameworkAdapter;
|