Skip to content

Commit 6fa63fd

Browse files
committed
feat(compiler): adds more compiler checks
- Component must be the only decorator - Component must be exported - Component must the only export of a module - Component can not inherit from a base class
1 parent 2865fae commit 6fa63fd

File tree

2 files changed

+73
-17
lines changed

2 files changed

+73
-17
lines changed

src/compiler/transpile/datacollection/component-decorator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ export function getComponentDecoratorMeta(diagnostics: d.Diagnostic[], checker:
2222
throw new Error(`tag missing in component decorator: ${JSON.stringify(componentOptions, null, 2)}`);
2323
}
2424

25+
if (node.heritageClauses && node.heritageClauses.some(c => c.token === ts.SyntaxKind.ExtendsKeyword)) {
26+
throw new Error(`Classes decorated with @Component can not extend from a base class.
27+
Inherency is temporarily disabled for stencil components.`);
28+
}
29+
30+
// check if class has more than one decorator
31+
if (node.decorators.length > 1) {
32+
throw new Error(`@Component({ tag: "${componentOptions.tag}"}) can not be decorated with more decorators at the same time`);
33+
}
34+
2535
const symbol = checker.getSymbolAtLocation(node.name);
2636

2737
const cmpMeta: d.ComponentMeta = {

src/compiler/transpile/datacollection/gather-metadata.ts

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,91 @@ import { normalizeAssetsDir } from '../../component-plugins/assets-plugin';
1313
import { normalizeStyles } from '../../style/normalize-styles';
1414
import { validateComponentClass } from './validate-component';
1515
import * as ts from 'typescript';
16+
import { buildError } from '../../util';
17+
import { isDecoratorNamed } from './utils';
1618

1719

1820
export function gatherMetadata(config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, typeChecker: ts.TypeChecker): ts.TransformerFactory<ts.SourceFile> {
1921

2022
return (transformContext) => {
2123

22-
function visit(node: ts.Node, tsSourceFile: ts.SourceFile, moduleFile: d.ModuleFile): ts.VisitResult<ts.Node> {
24+
function visit(node: ts.Node, tsSourceFile: ts.SourceFile, moduleFile: d.ModuleFile) {
2325

24-
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
25-
getCollections(config, compilerCtx, buildCtx.collections, moduleFile, node as ts.ImportDeclaration);
26-
}
27-
28-
if (ts.isClassDeclaration(node)) {
29-
const cmpMeta = visitClass(buildCtx.diagnostics, typeChecker, node as ts.ClassDeclaration, tsSourceFile);
30-
if (cmpMeta) {
31-
moduleFile.cmpMeta = cmpMeta;
26+
try {
27+
if (node.kind === ts.SyntaxKind.ImportDeclaration) {
28+
getCollections(config, compilerCtx, buildCtx.collections, moduleFile, node as ts.ImportDeclaration);
29+
}
3230

33-
cmpMeta.stylesMeta = normalizeStyles(config, moduleFile.sourceFilePath, cmpMeta.stylesMeta);
34-
cmpMeta.assetsDirsMeta = normalizeAssetsDir(config, moduleFile.sourceFilePath, cmpMeta.assetsDirsMeta);
31+
if (ts.isClassDeclaration(node)) {
32+
const cmpMeta = visitClass(buildCtx.diagnostics, typeChecker, node as ts.ClassDeclaration, tsSourceFile);
33+
if (cmpMeta) {
34+
if (moduleFile.cmpMeta) {
35+
throw new Error(`More than one @Component() class in a single file is not valid`);
36+
}
37+
moduleFile.cmpMeta = cmpMeta;
38+
39+
cmpMeta.stylesMeta = normalizeStyles(config, moduleFile.sourceFilePath, cmpMeta.stylesMeta);
40+
cmpMeta.assetsDirsMeta = normalizeAssetsDir(config, moduleFile.sourceFilePath, cmpMeta.assetsDirsMeta);
41+
}
3542
}
36-
}
43+
return node;
3744

38-
return ts.visitEachChild(node, (node) => {
39-
return visit(node, tsSourceFile, moduleFile);
40-
}, transformContext);
45+
} catch ({message}) {
46+
const error = buildError(buildCtx.diagnostics);
47+
error.messageText = message;
48+
error.relFilePath = tsSourceFile.fileName;
49+
}
50+
return undefined;
4151
}
4252

4353
return (tsSourceFile) => {
4454
const moduleFile = getModuleFile(compilerCtx, tsSourceFile.fileName);
4555
moduleFile.externalImports.length = 0;
4656
moduleFile.localImports.length = 0;
57+
moduleFile.cmpMeta = undefined;
4758

48-
return visit(tsSourceFile, tsSourceFile, moduleFile) as ts.SourceFile;
59+
const results = ts.visitEachChild(tsSourceFile, (node) => {
60+
return visit(node, tsSourceFile, moduleFile);
61+
}, transformContext);
62+
63+
if (moduleFile.cmpMeta) {
64+
const fileSymbol = typeChecker.getSymbolAtLocation(tsSourceFile);
65+
const fileExports = (fileSymbol && typeChecker.getExportsOfModule(fileSymbol)) || [];
66+
67+
if (fileExports.length > 1) {
68+
const error = buildError(buildCtx.diagnostics);
69+
error.messageText = `@Component() must be the only export of the module`;
70+
error.relFilePath = tsSourceFile.fileName;
71+
72+
} else if (
73+
fileExports.length === 0 ||
74+
!isComponentClass(fileExports[0])
75+
) {
76+
const error = buildError(buildCtx.diagnostics);
77+
error.messageText = `Missing export in @Component() class`;
78+
error.relFilePath = tsSourceFile.fileName;
79+
}
80+
}
81+
return results;
4982
};
5083
};
5184
}
5285

86+
function isComponentClass(symbol: ts.Symbol) {
87+
const decorators = symbol.valueDeclaration && symbol.valueDeclaration.decorators;
88+
if (!decorators) {
89+
return false;
90+
}
91+
return isDecoratorNamed('Component')(decorators[0]);
92+
}
93+
5394

54-
export function visitClass(diagnostics: d.Diagnostic[], typeChecker: ts.TypeChecker, classNode: ts.ClassDeclaration, sourceFile: ts.SourceFile): d.ComponentMeta | undefined {
95+
export function visitClass(
96+
diagnostics: d.Diagnostic[],
97+
typeChecker: ts.TypeChecker,
98+
classNode: ts.ClassDeclaration,
99+
sourceFile: ts.SourceFile,
100+
): d.ComponentMeta | undefined {
55101
const cmpMeta = getComponentDecoratorMeta(diagnostics, typeChecker, classNode);
56102

57103
if (!cmpMeta) {

0 commit comments

Comments
 (0)