Skip to content

Commit 7c92dbf

Browse files
authoredOct 27, 2022
fix(compiler): account for an existing constructor in convert-decorators (#3776)
This change ensures that statements in an existing constructor are not thrown on the floor in the case that we need to edit a constructor (i.e. when there is a field with `@Prop` that we need to initialize in the constructor). In f977830 we made a change to initialize any class fields decorated with `@Prop()` in a constructor. The code to do this would look for a constructor on the class and, if found, update the body of the constructor with statements to initialize the field. Unfortunately, that commit would drop all existing statements in the constructor on the floor! This broke how some Stencil users initialize fields or do certain side effects, since no code they wrote in their constructors would make it through to the built output. This commit fixes the issue by instead setting the constructor body to be all of our newly created statements followed by any existing statements. This will allow users to initialize fields to custom values in the constructor if they so chose. See #3773 for an issue describing the issue.
1 parent 8c1c35c commit 7c92dbf

File tree

2 files changed

+130
-10
lines changed

2 files changed

+130
-10
lines changed
 

‎src/compiler/transformers/decorators-to-static/convert-decorators.ts

+18-9
Original file line numberDiff line numberDiff line change
@@ -372,18 +372,32 @@ export const updateConstructor = (
372372
const constructorMethod = classMembers[constructorIndex];
373373

374374
if (constructorIndex >= 0 && ts.isConstructorDeclaration(constructorMethod)) {
375-
const hasSuper = constructorMethod.body.statements.some((s) => s.kind === ts.SyntaxKind.SuperKeyword);
375+
const constructorBodyStatements: ts.NodeArray<ts.Statement> =
376+
constructorMethod.body?.statements ?? ts.factory.createNodeArray();
377+
const hasSuper = constructorBodyStatements.some((s) => s.kind === ts.SyntaxKind.SuperKeyword);
376378

377379
if (!hasSuper && needsSuper(classNode)) {
378-
statements = [createConstructorBodyWithSuper(), ...statements];
380+
// if there is no super and it needs one the statements comprising the
381+
// body of the constructor should be:
382+
//
383+
// 1. the `super()` call
384+
// 2. the new statements we've created to initialize fields
385+
// 3. the statements currently comprising the body of the constructor
386+
statements = [createConstructorBodyWithSuper(), ...statements, ...constructorBodyStatements];
387+
} else {
388+
// if no super is needed then the body of the constructor should be:
389+
//
390+
// 1. the new statements we've created to initialize fields
391+
// 2. the statements currently comprising the body of the constructor
392+
statements = [...statements, ...constructorBodyStatements];
379393
}
380394

381395
classMembers[constructorIndex] = ts.factory.updateConstructorDeclaration(
382396
constructorMethod,
383397
constructorMethod.decorators,
384398
constructorMethod.modifiers,
385399
constructorMethod.parameters,
386-
ts.factory.updateBlock(constructorMethod.body, statements)
400+
ts.factory.updateBlock(constructorMethod?.body ?? ts.factory.createBlock([]), statements)
387401
);
388402
} else {
389403
// we don't seem to have a constructor, so let's create one and stick it
@@ -393,12 +407,7 @@ export const updateConstructor = (
393407
}
394408

395409
classMembers = [
396-
ts.factory.createConstructorDeclaration(
397-
undefined,
398-
undefined,
399-
undefined,
400-
ts.factory.createBlock(statements, true)
401-
),
410+
ts.factory.createConstructorDeclaration(undefined, undefined, [], ts.factory.createBlock(statements, true)),
402411
...classMembers,
403412
];
404413
}

‎src/compiler/transformers/test/convert-decorators.spec.ts

+112-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,118 @@ describe('convert-decorators', () => {
149149
);
150150
});
151151

152-
it('should not add a super call to the constructor if necessary', () => {
152+
it('should preserve statements in an existing constructor', () => {
153+
const t = transpileModule(`
154+
@Component({
155+
tag: 'my-component',
156+
})
157+
export class MyComponent {
158+
constructor() {
159+
console.log('boop');
160+
}
161+
}`);
162+
163+
expect(t.outputText).toBe(
164+
c`export class MyComponent {
165+
constructor() {
166+
console.log('boop');
167+
}
168+
169+
static get is() {
170+
return "my-component";
171+
}}`
172+
);
173+
});
174+
175+
it('should preserve statements in an existing constructor w/ @Prop', () => {
176+
const t = transpileModule(`
177+
@Component({
178+
tag: 'my-component',
179+
})
180+
export class MyComponent {
181+
@Prop() count: number;
182+
183+
constructor() {
184+
console.log('boop');
185+
}
186+
}`);
187+
188+
expect(t.outputText).toContain(
189+
c`constructor() {
190+
this.count = undefined;
191+
console.log('boop');
192+
}`
193+
);
194+
});
195+
196+
it('should allow user to initialize field in an existing constructor w/ @Prop', () => {
197+
const t = transpileModule(`
198+
@Component({
199+
tag: 'my-component',
200+
})
201+
export class MyComponent {
202+
@Prop() count: number;
203+
204+
constructor() {
205+
this.count = 3;
206+
}
207+
}`);
208+
209+
// the initialization we do to `undefined` (since no value is present)
210+
// should be before the user's `this.count = 3` to ensure that their code
211+
// wins.
212+
expect(t.outputText).toContain(
213+
c`constructor() {
214+
this.count = undefined;
215+
this.count = 3;
216+
}`
217+
);
218+
});
219+
220+
it('should preserve statements in an existing constructor w/ non-decorated field', () => {
221+
const t = transpileModule(`
222+
@Component({
223+
tag: 'example',
224+
})
225+
export class Example implements FooBar {
226+
private classProps: Array<string>;
227+
228+
constructor() {
229+
this.classProps = ["variant", "theme"];
230+
}
231+
}`);
232+
233+
expect(t.outputText).toBe(
234+
c`export class Example {
235+
constructor() {
236+
this.classProps = ["variant", "theme"];
237+
}}`
238+
);
239+
});
240+
241+
it('should preserve statements in an existing constructor super, decorated field', () => {
242+
const t = transpileModule(`
243+
@Component({
244+
tag: 'example',
245+
})
246+
export class Example extends Parent {
247+
@Prop() foo: string = "bar";
248+
249+
constructor() {
250+
console.log("hello!")
251+
}
252+
}`);
253+
254+
expect(t.outputText).toContain(
255+
c`constructor() {
256+
super();
257+
this.foo = "bar";
258+
console.log("hello!");
259+
}`
260+
);
261+
});
262+
263+
it('should not add a super call to the constructor if not necessary', () => {
153264
const t = transpileModule(`
154265
@Component({tag: 'cmp-a'})
155266
export class CmpA implements Foobar {

0 commit comments

Comments
 (0)
Please sign in to comment.