Skip to content

Commit

Permalink
feat: add support for Template Only Components
Browse files Browse the repository at this point in the history
  • Loading branch information
josemarluedke committed Mar 19, 2024
1 parent aba64bc commit 1da8199
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 13 deletions.
33 changes: 28 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
ComponentDoc,
Property,
PropertyType,
ElementProperty
ElementProperty,
TemplateOnlyComponent
} from './types';

const DEFAULT_IGNORE = [
Expand All @@ -21,6 +22,20 @@ const DEFAULT_IGNORE = [
'dist/**'
];

function findTOCUsage(node: ts.Node): ts.TypeReferenceNode | undefined {
// Check if this node is a TypeReferenceNode and its typeName is "TOC"
if (
ts.isTypeReferenceNode(node) &&
ts.isIdentifier(node.typeName) &&
node.typeName.text === 'TOC'
) {
return node;
}

// Continue traversing the tree
return ts.forEachChild(node, findTOCUsage);
}

export function parse(sources: Source[]): ComponentDoc[] {
const components: ComponentDoc[] = [];

Expand All @@ -47,7 +62,7 @@ export function parse(sources: Source[]): ComponentDoc[] {
const checker = program.getTypeChecker();
const parser = new Parser(checker);

const maybeComponents: ts.ClassDeclaration[] = [];
const maybeComponents: (ts.ClassDeclaration | TemplateOnlyComponent)[] = [];

filePaths
.map((filePath) => program.getSourceFile(filePath))
Expand All @@ -59,15 +74,23 @@ export function parse(sources: Source[]): ComponentDoc[] {
sourceFile.statements.forEach((stmt) => {
if (ts.isClassDeclaration(stmt)) {
maybeComponents.push(stmt);
} else {
const templateOnlyComponent = findTOCUsage(stmt);
if (templateOnlyComponent) {
maybeComponents.push({
isTemplateOnly: true,
fileName: sourceFile.fileName,
node: templateOnlyComponent,
stmt: stmt
});
}
}
});
});

components.push(
...maybeComponents
.filter(
(declaration) => declaration.name && parser.isComponent(declaration)
)
.filter((declaration) => parser.isComponent(declaration))
.map((component) => parser.getComponentDoc(component))
.map((component) => {
return {
Expand Down
74 changes: 67 additions & 7 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
Property,
PropertyType,
ComponentDoc,
ElementProperty
ElementProperty,
TemplateOnlyComponent
} from './types';

export default class Parser {
Expand All @@ -15,7 +16,38 @@ export default class Parser {
this.checker = checker;
}

getComponentDoc(component: ts.ClassDeclaration): ComponentDoc {
findTOCSymbol = (node: ts.Node): ts.Symbol | undefined => {
if (ts.isVariableDeclaration(node)) {
const symbol = this.checker.getSymbolAtLocation(node.name);

if (symbol) {
return symbol;
}
}

return ts.forEachChild(node, this.findTOCSymbol);
};

getComponentDoc(
component: ts.ClassDeclaration | TemplateOnlyComponent
): ComponentDoc {
if ('isTemplateOnly' in component) {
const symbol = this.findTOCSymbol(component.stmt);
const signature = this.getComponentSignature(component);

const output: ComponentDoc = {
...extractPackageAndComponent(component.fileName),
name: symbol?.name || 'Component',
fileName: component.fileName,
Args: this.getSignatureArgs(signature, component.node),
Blocks: this.getSignatureBlocks(signature, component.node),
Element: this.getSignatureElement(signature, component.node),
...this.getDocumentationFromSymbol(symbol)
};

return output;
}

const type = this.checker.getTypeAtLocation(component);
const signature = this.getComponentSignature(component);
const fileName = component.getSourceFile().fileName;
Expand All @@ -33,8 +65,16 @@ export default class Parser {
return output;
}

getComponentSignature(component: ts.ClassDeclaration): ts.Type | undefined {
const signature = this.extractTypeParameterFromClass(component);
getComponentSignature(
component: ts.ClassDeclaration | TemplateOnlyComponent
): ts.Type | undefined {
let signature: ts.TypeNode | undefined;

if ('isTemplateOnly' in component) {
signature = this.extractTypeParameterFromTOC(component.node);
} else {
signature = this.extractTypeParameterFromClass(component);
}

if (signature) {
return this.checker.getTypeAtLocation(signature);
Expand Down Expand Up @@ -177,6 +217,18 @@ export default class Parser {
return undefined;
}

extractTypeParameterFromTOC(
node: ts.TypeReferenceNode
): ts.TypeNode | undefined {
if (node.typeArguments?.length) {
const typeArgument = node.typeArguments?.[0];

return typeArgument;
}

return undefined;
}

getPropertyType(
symbol: ts.Symbol | undefined,
type: ts.Type,
Expand Down Expand Up @@ -290,14 +342,22 @@ export default class Parser {
return false;
}

// We consider a component a class declaration that has "args" property
isComponent(component: ts.ClassDeclaration): boolean {
// We consider a component a class declaration that has "args" property or a template only component
isComponent(component: ts.ClassDeclaration | TemplateOnlyComponent): boolean {
if ('isTemplateOnly' in component) {
return true;
}

const componentType = this.checker.getTypeAtLocation(component);

return !!componentType.getProperty('args');
}

getDocumentationFromSymbol(symbol: ts.Symbol): DocumentationComment {
getDocumentationFromSymbol(symbol?: ts.Symbol): DocumentationComment {
if (!symbol) {
return { description: '', tags: {} };
}

let description = ts.displayPartsToString(
symbol.getDocumentationComment(this.checker)
);
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,10 @@ export interface ComponentDoc extends DocumentationComment {
Blocks: Property[];
Element: ElementProperty | undefined;
}

export interface TemplateOnlyComponent {
fileName: string;
isTemplateOnly: true;
node: ts.TypeReferenceNode;
stmt: ts.Statement;
}
18 changes: 18 additions & 0 deletions tests/__fixtures__/spinner.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable node/no-missing-import */
import type { TOC } from '@ember/component/template-only';
import { type SpinnerVariants } from '@frontile/theme';

/**
* My cool component
* @default cool
*/
declare const Spinner: TOC<{
Element: SVGElement;
Args: {
class?: string;
size?: SpinnerVariants['size'];
intent?: SpinnerVariants['intent'];
};
}>;
export { Spinner };
export default Spinner;
57 changes: 57 additions & 0 deletions tests/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1322,5 +1322,62 @@ It allows the consumer to render a link for example",
"package": "unknown",
"tags": {},
},
{
"Args": [
{
"defaultValue": undefined,
"description": "",
"identifier": "class",
"isInternal": false,
"isRequired": false,
"tags": {},
"type": {
"type": "string",
},
},
{
"defaultValue": undefined,
"description": "",
"identifier": "intent",
"isInternal": false,
"isRequired": false,
"tags": {},
"type": {
"type": "SpinnerVariants",
},
},
{
"defaultValue": undefined,
"description": "",
"identifier": "size",
"isInternal": false,
"isRequired": false,
"tags": {},
"type": {
"type": "SpinnerVariants",
},
},
],
"Blocks": [],
"Element": {
"description": "",
"identifier": "Element",
"type": {
"type": "SVGElement",
},
"url": "https://developer.mozilla.org/en-US/docs/Web/API/SVGElement",
},
"description": "My cool component",
"fileName": "spinner.d.ts",
"module": "spinner",
"name": "Spinner",
"package": "unknown",
"tags": {
"default": {
"name": "default",
"value": "cool",
},
},
},
]
`;
2 changes: 1 addition & 1 deletion tests/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function inspect(obj: unknown): void {
// pattern: 'test.js'
root: path.resolve(path.join(__dirname, '../../frontile')),
// pattern: 'packages/*/declarations/components/*.d.ts'
pattern: 'packages/overlays/declarations/components/popover.d.ts'
pattern: 'packages/utilities/declarations/components/spinner.d.ts'
}
// {
// options: {
Expand Down

0 comments on commit 1da8199

Please sign in to comment.