-
Notifications
You must be signed in to change notification settings - Fork 795
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(compiler): handle ts 5.0 static members
this commit fixes a bug where static class members that are intialized on a stencil component result in 2 `export` statements of a component class being generated. as a result of 2 `ExportDeclaration`s being created, the compilation process will fail. this fix involves detecting initialized static members of a class before they go through the transpilation phase, because a class can be transformed in such a way that we lose the ability to determine if something like the following was originally authored: ```ts class MyComponent { static myVar = 1; } ``` as a result, whether or not this static initializer exists or not is stuffed onto the component's metadata. this allows us to store if an additional transformation is needed or not after transpilation has occurred. specifcially, should we add an `export` keyword to a modified class declaration in the form: ```ts // determine if this `VariableDeclaration` needs an `export` const MyComponent = class {} ``` Additional Information Consider a TypeScript class that: - has a static member that is defined on the class - is exported from the file in which it is defined ```ts export class MyComponent { static myVar = 1; } ``` [In TypeScript v4.9 and lower](https://www.typescriptlang.org/play?ts=4.9.5#code/KYDwDg9gTgLgBAYwDYEMDOa4FkCeBhCAW0gDtgT4BvAKAEg0YUYBLBOQnANRSjgF44ARgDc1AL5A) the class is transpiled as such: ```js export class MyComponent { } MyComponent.myVar = 1; ``` [Starting with TypeScript v5.0](https://www.typescriptlang.org/play?ts=5.0.4#code/KYDwDg9gTgLgBAYwDYEMDOa4FkCeBhCAW0gDtgT4BvAKAEg0YUYBLBOQnANRSjgF44ARgDc1AL5A), the class is transpiled like so: ```js class MyComponent { } MyComponent.myVar = 1; export { MyComponent }; ``` Where the `export` occurs separate from the class's declaration: ```diff - export class MyComponent { + class MyComponent { } MyComponent.myVar = 1; + export { MyComponent }; ``` Note this only occurs when both conditions above are met. The following code snippet compiles the same using TypeScript [v4.9](https://www.typescriptlang.org/play?ts=4.9.5#code/KYDwDg9gTgLgBAYwDYEMDOa4FkCeBhCAW0gDtgT4BvAKAF8g) and [v5.0](https://www.typescriptlang.org/play?ts=5.0.4&ssl=2&ssc=2&pln=1&pc=1#code/KYDwDg9gTgLgBAYwDYEMDOa4FkCeBhCAW0gDtgT4BvAKAF8g): ```ts export class MyComponent { } // transpiles to export class MyComponent { } ``` The difference in compilation when both conditions are met leads to a bug in Stencil v3.3.0, the first version of the library which supports TypeScript v5.0. Stencil made the assumption that the `export` keyword continues to be inlined with the class declaration, rather than a new `ExportDeclaration` instance being created. In Stencil v3.3.0, the `export` keyword is added to the generated code: ```ts export class MyComponent {}; MyComponent.myVar = 1; export { MyComponent }; // 2 exports causes an error! ``` Two exports of the class (`MyComponent`) causes errors following the TypeScript compilation step in Stencil: ```console [ ERROR ] Rollup: Parse Error: <FILE_PATH> Duplicate export 'MyComponent' (Note that you need plugins to import files that are not JavaScript) Error parsing: <FILE_PATH> ``` In an ideal world, we delegate inserting `export` to the TypeScript compiler. However, this is not possible as the `ExportDeclaration` is _only_ created for classes that are exported and have static members. So while removing Stencil's `export` keyword injection will work for: ```ts export class MyComponent { static myVar = 1; } // becomes the following on TS 5.0 if we remove Stencil's `export` injection class MyComponent { } MyComponent.myVar = 1; export { MyComponent }; ``` it will not work for: ```ts export class MyComponent { myVar = 1; } // becomes the following on TS 5.0 if we remove Stencil's `export` injection class MyComponent { constructor() { this.myVar = 1; } } ``` as `export` is removed from `MyComponent` in the compilation process. This behavior is controlled by the [`TransformOptions#componentExport` field](https://github.com/ionic-team/stencil/blob/bd1023a67c5224cbae3e445170cb13227a5cf8fb/src/declarations/stencil-public-compiler.ts#L2683). Import injection should only occur when neither of two aforementioned criteria are met: - has a static member that is defined on the class - is exported from the file in which it is defined
- Loading branch information
1 parent
c83c113
commit 632a06e
Showing
22 changed files
with
450 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
src/compiler/transformers/decorators-to-static/convert-static-members.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import ts from 'typescript'; | ||
|
||
/** | ||
* Helper function to detect if a class element fits the following criteria: | ||
* - It is a property declaration (e.g. `foo`) | ||
* - It has an initializer (e.g. `foo *=1*`) | ||
* - The property declaration has the `static` modifier on it (e.g. `*static* foo =1`) | ||
* @param classElm the class member to test | ||
* @returns true if the class member fits the above criteria, false otherwise | ||
*/ | ||
export const hasStaticInitializerInClass = (classElm: ts.ClassElement): boolean => { | ||
return ( | ||
ts.isPropertyDeclaration(classElm) && | ||
classElm.initializer !== undefined && | ||
Array.isArray(classElm.modifiers) && | ||
classElm.modifiers!.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
src/compiler/transformers/test/convert-static-members.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import ts from 'typescript'; | ||
|
||
import { hasStaticInitializerInClass } from '../decorators-to-static/convert-static-members'; | ||
|
||
describe('convert-static-members', () => { | ||
describe('hasStaticInitializerInClass', () => { | ||
it('returns true for a static property with an initializer', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithStaticMember'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
ts.factory.createStringLiteral('initial value') | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); | ||
}); | ||
|
||
it('returns true for a private static property with an initializer', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithStaticMember'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.PrivateKeyword), ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
ts.factory.createStringLiteral('initial value') | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); | ||
}); | ||
|
||
it('returns true for a static property with an initializer with multiple members', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithStaticAndNonStaticMembers'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
undefined, | ||
ts.factory.createIdentifier('nonStaticProperty'), | ||
undefined, | ||
undefined, | ||
ts.factory.createStringLiteral('some value') | ||
), | ||
ts.factory.createPropertyDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
ts.factory.createStringLiteral('initial value') | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(true); | ||
}); | ||
|
||
it('returns false for a class without any members', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithNoMembers'), | ||
undefined, | ||
undefined, | ||
[] // no members for this class | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); | ||
}); | ||
|
||
it('returns false for a static property without an initializer', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithUninitializedStaticMember'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
undefined // the initializer is false | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); | ||
}); | ||
|
||
it('returns false for a private static property without an initializer', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithUninitializedStaticMember'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.PrivateKeyword), ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
undefined // the initializer is false | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); | ||
}); | ||
|
||
it('returns false for a modified property with an initializer', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithNonStaticMember'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.PrivateKeyword)], // the property is declared as private | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
ts.factory.createStringLiteral('initial value') | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); | ||
}); | ||
|
||
it('returns false for an unmodified property with an initializer', () => { | ||
const classWithStaticMembers = ts.factory.createClassDeclaration( | ||
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.factory.createIdentifier('ClassWithUnmodifiedMembers'), | ||
undefined, | ||
undefined, | ||
[ | ||
ts.factory.createPropertyDeclaration( | ||
undefined, // the property declaration has no modifiers | ||
ts.factory.createIdentifier('propertyName'), | ||
undefined, | ||
undefined, | ||
ts.factory.createStringLiteral('initial value') | ||
), | ||
] | ||
); | ||
expect(classWithStaticMembers.members.some(hasStaticInitializerInClass)).toBe(false); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { transpileModule } from './transpile'; | ||
|
||
describe('parse static members', () => { | ||
it('places a static getter on the component', () => { | ||
const t = transpileModule(` | ||
@Component({tag: 'cmp-a'}) | ||
export class CmpA { | ||
static myStatic = 'a value'; | ||
render() { | ||
return <div>Hello, I have {CmpA.myStatic}</div> | ||
} | ||
} | ||
`); | ||
|
||
expect(t.outputText.includes('static get stencilHasStaticMembersWithInit() { return true; }')).toBe(true); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.