@workday/canvas-kit-docs
Version:
Documentation components of Canvas Kit components
345 lines (344 loc) • 12.1 kB
JavaScript
import { createParserPlugin, getValueDeclaration, defaultJSDoc, getSymbolFromNode, getFullJsDocComment, } from '../docParser';
import t from '../traverse';
function capitalize(string) {
return string[0].toUpperCase() + string.substring(1);
}
export const modelParser = createParserPlugin((node, parser) => {
var _a;
/**
* Replace submenus with symbols rather than types.
*
* For example:
* ```ts
* const menu = useMenuModel()
*
* const state = {
* menu
* }
* ```
*
* In this example, the `type` of `state.menu` will be `useMenuModel` instead of an object
* representing the return type of `useMenuModel`
*
* Before:
* ```json
* {
* kind: 'object',
* properties: [
* {
* kind: 'parameter',
* name: 'state',
* // ...
* }
* ```
*
* After:
* ```json
* {
* kind: 'symbol',
* name: 'useMenuModel'
* }
* ```
*/
if (t.isShorthandPropertyAssignment(node)) {
const { checker } = parser;
const symbol = getSymbolFromNode(parser, node);
const declaration = getValueDeclaration(symbol);
const name = (symbol === null || symbol === void 0 ? void 0 : symbol.name) || t.isIdentifier(node.name) ? node.name.text : '';
if (declaration && declaration.getSourceFile()) {
// ShorthandPropertyAssignment doesn't give the original variable declaration
const originalDeclaration = t(declaration.getSourceFile())
.find('VariableDeclaration')
.map(node => {
if (t.isIdentifier(node.name) &&
node.name.text === name &&
node.initializer &&
t.isCallExpression(node.initializer) &&
t.isIdentifier(node.initializer.expression) &&
node.initializer.expression.text.includes('Model')) {
return {
kind: 'symbol',
name: node.initializer.expression.text.replace('use', ''),
};
}
return;
})
.filter(v => !!v)[0];
if (originalDeclaration) {
const jsDoc = symbol ? getFullJsDocComment(checker, symbol) : defaultJSDoc;
return {
kind: 'property',
name,
defaultValue: undefined,
type: originalDeclaration,
required: false,
...jsDoc,
};
}
}
}
/**
* Replace `typeof useMyModel.TConfig` with a symbol of `MyModelConfig`. This remaps the entire
* type to point to a symbol of the model config instead.
*
* For example,
* ```ts
* defaultConfig: {
* menuConfig: {} as typeof useMyModel.TConfig
* }
* ```
*
* Before:
* ```json
* {
* kind: 'object',
* properties: [...]
* }
* ```
*
* After:
* ```json
* {
* kind: 'symbol',
* name: 'MyModelConfig'
* }
* ```
*/
if (t.isTypeReference(node)) {
const firstArgument = (_a = node.typeArguments) === null || _a === void 0 ? void 0 : _a[0];
if (firstArgument && t.isTypeQuery(firstArgument) && t.isIdentifier(firstArgument.exprName)) {
const modelName = firstArgument.exprName.text;
return { kind: 'symbol', name: modelName };
}
}
if (t.isPropertyAssignment(node) &&
t.isIdentifier(node.name) &&
t.isAsExpression(node.initializer) &&
t.isTypeQuery(node.initializer.type) &&
t.isQualifiedName(node.initializer.type.exprName) &&
t.isIdentifier(node.initializer.type.exprName.left) &&
t.isIdentifier(node.initializer.type.exprName.right) &&
// TConfig is special from models
node.initializer.type.exprName.right.text === 'TConfig') {
const symbol = getSymbolFromNode(parser, node);
const jsDoc = (symbol && getFullJsDocComment(parser.checker, symbol)) || defaultJSDoc;
return {
kind: 'property',
name: node.name.text,
defaultValue: parser.getValueFromNode(node.initializer.expression),
required: false,
type: {
kind: 'symbol',
name: node.initializer.type.exprName.left.text.replace('use', '') + 'Config',
},
...jsDoc,
};
}
/**
* This parsing matcher handles models created using `createModelHook` and adds additional symbols
* for use by other models or components.
*
* For example:
* ```ts
* const useMyModel = createModelHook({
* defaultConfig: {
* // ...
* },
* requiredConfig: {
* // ...
* }
* })(config => {
* return {
* state: {
* // ...
* },
* events: {
* // ...
* }
* }
* })
* ```
*
* This node parser will add the following symbols:
* - MyModelConfig
* - MyModelState
* - MyModelEvents
*
* These symbols will be used by the model itself as well as in other models that make this model
* a subModel (including config) and components that use models. Without this parser, a model
* would be a generic function.
*/
if (t.isVariableDeclaration(node) &&
node.initializer &&
t.isIdentifier(node.name) &&
t.isCallExpression(node.initializer) &&
t.isCallExpression(node.initializer.expression) &&
t.isIdentifier(node.initializer.expression.expression) &&
node.initializer.expression.expression.text === 'createModelHook') {
// we have a model
const modelName = node.name.text.replace('use', '');
// get the symbol for the model hook
const symbol = getSymbolFromNode(parser, node);
const jsDoc = symbol ? getFullJsDocComment(parser.checker, symbol) : defaultJSDoc;
const options = node.initializer.expression.arguments[0];
const optionsType = parser.checker.getTypeAtLocation(options);
const configProps = optionsType.getProperties().reduce((result, symbol) => {
if (['defaultConfig', 'requiredConfig'].includes(symbol.getName())) {
// The declaration of the config
const declaration = getValueDeclaration(symbol);
if (declaration) {
result[symbol.getName()] = parser.checker
.getTypeAtLocation(declaration)
.getProperties()
.map(p => {
// Each property of the config
const prop = getValueDeclaration(p);
return {
...parser.getValueFromNode(prop),
required: symbol.getName() === 'requiredConfig',
};
});
}
}
return result;
}, {});
const modelProps = {};
const returnProps = {};
const type = parser.checker.getTypeAtLocation(node.initializer.arguments[0]);
type.getCallSignatures().forEach(signature => {
signature
.getReturnType()
.getProperties()
.forEach(property => {
const declaration = getValueDeclaration(property);
if (declaration) {
if (['state', 'events'].includes(property.getName())) {
modelProps[property.getName()] = parser.checker
.getTypeAtLocation(declaration)
.getProperties()
.map(prop => {
return {
...parser.getValueFromNode(getValueDeclaration(prop)),
defaultValue: undefined, // no defaults for state/events
};
});
}
else {
returnProps[property.getName()] = parser.getValueFromNode(declaration);
}
}
});
});
const { state, events } = modelProps;
const fileName = node.getSourceFile().fileName;
// Add special symbols
// Model
parser.symbols.push({
name: modelName,
fileName,
...jsDoc,
type: {
kind: 'model',
state,
events,
modelProperties: Object.keys(returnProps).map(p => returnProps[p]),
},
});
// Model config
const eventConfig = (events || []).reduce((result, event) => {
const type = event.type;
// callback of the event
result.push({
...event,
defaultValue: undefined,
name: `on${capitalize(event.name)}`,
required: false,
type: {
...type,
parameters: [
{
...defaultJSDoc,
kind: 'parameter',
name: 'state',
required: true,
type: {
kind: 'symbol',
name: `${modelName}State`,
fileName,
},
},
].concat(...type.parameters),
returnType: {
kind: 'primitive',
value: 'void',
},
},
});
// guard of the event
result.push({
...event,
defaultValue: undefined,
name: `should${capitalize(event.name)}`,
required: false,
type: {
...type,
parameters: [
{
...defaultJSDoc,
kind: 'parameter',
name: 'state',
required: true,
type: {
kind: 'symbol',
name: `${modelName}State`,
fileName,
},
},
].concat(...type.parameters),
returnType: {
kind: 'primitive',
value: 'boolean',
},
},
});
return result;
}, []);
parser.symbols.push({
name: `${modelName}Config`,
fileName,
...jsDoc,
type: {
kind: 'object',
properties: (configProps['defaultConfig'] || []).concat(configProps['requiredConfig'], eventConfig),
},
});
// Model State
parser.symbols.push({
name: `${modelName}State`,
fileName,
...defaultJSDoc,
type: {
kind: 'object',
properties: state,
},
});
// Model Events
parser.symbols.push({
name: `${modelName}Events`,
fileName,
...defaultJSDoc,
type: {
kind: 'object',
properties: events,
},
});
// Model Hook
return {
kind: 'modelHook',
name: `use${modelName}`,
defaultConfig: configProps['defaultConfig'] || [],
requiredConfig: configProps['requiredConfig'] || [],
};
}
return;
});