Skip to content

Commit afa4c2d

Browse files
johnjenkinsJohn Jenkins
andauthored
fix(mixin): allow args as a ctor argument & fix super call order during spec tests (#6467)
* chore: fix mixin spec testing * chore: --------- Co-authored-by: John Jenkins <john.jenkins@nanoporetech.com>
1 parent 87b0fd7 commit afa4c2d

File tree

5 files changed

+245
-75
lines changed

5 files changed

+245
-75
lines changed

src/compiler/transformers/component-lazy/transform-lazy-component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type * as d from '../../../declarations';
44
import { addImports } from '../add-imports';
55
import { addLegacyApis } from '../core-runtime-apis';
66
import { updateStyleImports } from '../style-imports';
7-
import { getComponentMeta, getModuleFromSourceFile, updateMixin } from '../transform-utils';
7+
import { getComponentMeta, getModuleFromSourceFile, updateConstructor, updateMixin } from '../transform-utils';
88
import { updateLazyComponentClass } from './lazy-component';
99

1010
/**
@@ -39,6 +39,10 @@ export const lazyComponentTransform = (
3939
return updateLazyComponentClass(transformOpts, styleStatements, node, moduleFile, cmp, buildCtx);
4040
} else if (module?.isMixin) {
4141
return updateMixin(node, moduleFile, cmp, transformOpts);
42+
} else if (buildCtx.config._isTesting && buildCtx.config.flags.spec) {
43+
// because (during spec tests) *only* the component class is added as a module
44+
// let's tidy up all class nodes in testing mode
45+
return updateConstructor(node, Array.from(node.members), [], []);
4246
}
4347
}
4448
return ts.visitEachChild(node, visitNode, transformCtx);

src/compiler/transformers/decorators-to-static/import-alias-map.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class ImportAliasMap extends Map<StencilDecorator, string> {
1515
* @param sourceFile The source file to parse
1616
*/
1717
private generateImportAliasMap(sourceFile: ts.SourceFile) {
18-
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration);
18+
const importDeclarations = sourceFile?.statements.filter(ts.isImportDeclaration) ?? [];
1919

2020
for (const importDeclaration of importDeclarations) {
2121
if (importDeclaration.moduleSpecifier.getText().includes('@stencil/core')) {

src/compiler/transformers/static-to-meta/class-extension.ts

Lines changed: 169 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ type DeDupeMember =
2121
| d.ComponentCompilerEvent
2222
| d.ComponentCompilerChangeHandler;
2323

24+
type DependentClass = {
25+
classNode: ts.ClassDeclaration;
26+
sourceFile: ts.SourceFile;
27+
fileName: string;
28+
};
29+
2430
/**
2531
* Given two arrays of static members, return a new array containing only the
2632
* members from the first array that are not present in the second array.
@@ -42,6 +48,105 @@ const deDupeMembers = <T extends DeDupeMember>(dedupeMembers: T[], staticMembers
4248
);
4349
};
4450

51+
/**
52+
* Helper function to resolve and process an extended class from a module.
53+
* This handles:
54+
* 1. Resolving the module path
55+
* 2. Getting the source file
56+
* 3. Finding the class declaration
57+
* 4. Adding to dependent classes tree
58+
*
59+
* @param compilerCtx
60+
* @param buildCtx
61+
* @param classDeclaration the current class being analyzed
62+
* @param currentSource the source file of the current class
63+
* @param moduleSpecifier the module path to resolve
64+
* @param className the name of the class to find in the resolved module
65+
* @param dependentClasses the array to add found classes to
66+
* @param keepLooking whether to continue recursively looking for more extended classes
67+
* @param typeChecker
68+
* @param ogModule
69+
* @returns the found class declaration or undefined
70+
*/
71+
function resolveAndProcessExtendedClass(
72+
compilerCtx: d.CompilerCtx,
73+
buildCtx: d.BuildCtx,
74+
classDeclaration: ts.ClassDeclaration,
75+
currentSource: ts.SourceFile,
76+
moduleSpecifier: string,
77+
className: string,
78+
dependentClasses: DependentClass[],
79+
keepLooking: boolean,
80+
typeChecker: ts.TypeChecker,
81+
ogModule: d.Module,
82+
): ts.ClassDeclaration | undefined {
83+
const foundFile = tsResolveModuleName(buildCtx.config, compilerCtx, moduleSpecifier, currentSource.fileName);
84+
85+
if (!foundFile?.resolvedModule || !className) {
86+
return undefined;
87+
}
88+
89+
// 1) resolve the module name to a file
90+
let foundSource: ts.SourceFile = compilerCtx.moduleMap.get(
91+
foundFile.resolvedModule.resolvedFileName,
92+
)?.staticSourceFile;
93+
94+
if (!foundSource) {
95+
// Stencil only loads full-fledged component modules from node_modules collections,
96+
// so if we didn't find the source file in the module map,
97+
// let's create a temporary program and get the source file from there
98+
foundSource = tsGetSourceFile(buildCtx.config, foundFile);
99+
100+
if (!foundSource) {
101+
// ts could not resolve the module. Likely because `allowJs` is not set to `true`
102+
const err = buildWarn(buildCtx.diagnostics);
103+
err.messageText = `Unable to resolve import "${moduleSpecifier}" from "${currentSource.fileName}".
104+
This can happen when trying to resolve .js files and "allowJs" is not set to "true" in your tsconfig.json.`;
105+
if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration);
106+
return undefined;
107+
}
108+
}
109+
110+
// 2) get the exported declaration from the module
111+
const matchedStatement = foundSource.statements.find(matchesNamedDeclaration(className));
112+
if (!matchedStatement) {
113+
// we couldn't find the imported declaration as an exported statement in the module
114+
const err = buildWarn(buildCtx.diagnostics);
115+
err.messageText = `Unable to find "${className}" in the imported module "${moduleSpecifier}".
116+
Please import class / mixin-factory declarations directly and not via barrel files.`;
117+
if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration);
118+
return undefined;
119+
}
120+
121+
let foundClassDeclaration = matchedStatement
122+
? ts.isClassDeclaration(matchedStatement)
123+
? matchedStatement
124+
: undefined
125+
: undefined;
126+
127+
if (!foundClassDeclaration && matchedStatement) {
128+
// the found `extends` type does not resolve to a class declaration;
129+
// if it's wrapped in a function - let's try and find it inside
130+
foundClassDeclaration = findClassWalk(matchedStatement);
131+
keepLooking = false;
132+
}
133+
134+
if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) {
135+
// 3) if we found the class declaration, push it and check if it itself extends from another class
136+
dependentClasses.push({
137+
classNode: foundClassDeclaration,
138+
sourceFile: foundSource,
139+
fileName: foundFile.resolvedModule.resolvedFileName,
140+
});
141+
142+
if (keepLooking) {
143+
buildExtendsTree(compilerCtx, foundClassDeclaration, dependentClasses, typeChecker, buildCtx, ogModule);
144+
}
145+
}
146+
147+
return foundClassDeclaration;
148+
}
149+
45150
/**
46151
* A recursive function that walks the AST to find a class declaration.
47152
* @param node the current AST node
@@ -53,6 +158,21 @@ function findClassWalk(node?: ts.Node, name?: string): ts.ClassDeclaration | und
53158
if (!node) return undefined;
54159
if (node && ts.isClassDeclaration(node) && (!name || node.name?.text === name)) {
55160
return node;
161+
} else if (
162+
node &&
163+
ts.isVariableDeclaration(node) &&
164+
// @ts-ignore
165+
(!name || name === (node.name?.text || node.name?.escapedText)) &&
166+
node.initializer &&
167+
ts.isArrowFunction(node.initializer)
168+
) {
169+
// handle case where class is wrapped in a mixin factory function
170+
let found: ts.ClassDeclaration | undefined;
171+
ts.forEachChild(node.initializer.body, (child) => {
172+
if (found) return;
173+
if (ts.isClassDeclaration(child)) found = child;
174+
});
175+
return found;
56176
}
57177
let found: ts.ClassDeclaration | undefined;
58178

@@ -109,7 +229,7 @@ function matchesNamedDeclaration(name: string) {
109229
function buildExtendsTree(
110230
compilerCtx: d.CompilerCtx,
111231
classDeclaration: ts.ClassDeclaration,
112-
dependentClasses: { classNode: ts.ClassDeclaration; sourceFile: ts.SourceFile; fileName: string }[],
232+
dependentClasses: DependentClass[],
113233
typeChecker: ts.TypeChecker,
114234
buildCtx: d.BuildCtx,
115235
ogModule: d.Module,
@@ -153,6 +273,9 @@ function buildExtendsTree(
153273
// if it's wrapped in a function - let's try and find it inside
154274
const node = aliasedSymbol?.declarations?.[0];
155275
foundClassDeclaration = findClassWalk(node);
276+
if (!node) {
277+
throw 'revert to sad path';
278+
}
156279
keepLooking = false;
157280
}
158281

@@ -227,83 +350,57 @@ function buildExtendsTree(
227350
if (element.name.getText() === extendee.getText()) {
228351
// 3) check the name matches the `extends` type expression
229352
const className = element.propertyName?.getText() || element.name.getText();
230-
const foundFile = tsResolveModuleName(
231-
buildCtx.config,
353+
const moduleSpecifier = statement.moduleSpecifier.getText().replaceAll(/['"]/g, '');
354+
355+
resolveAndProcessExtendedClass(
232356
compilerCtx,
233-
statement.moduleSpecifier.getText().replaceAll(/['"]/g, ''),
234-
currentSource.fileName,
357+
buildCtx,
358+
classDeclaration,
359+
currentSource,
360+
moduleSpecifier,
361+
className,
362+
dependentClasses,
363+
keepLooking,
364+
typeChecker,
365+
ogModule,
235366
);
236-
237-
if (foundFile?.resolvedModule && className) {
238-
// 4) resolve the module name to a file
239-
let foundSource: ts.SourceFile = compilerCtx.moduleMap.get(
240-
foundFile.resolvedModule.resolvedFileName,
241-
)?.staticSourceFile;
242-
243-
if (!foundSource) {
244-
// Stencil only loads full-fledged component modules from node_modules collections,
245-
// so if we didn't find the source file in the module map,
246-
// let's create a temporary program and get the source file from there
247-
foundSource = tsGetSourceFile(buildCtx.config, foundFile);
248-
249-
if (!foundSource) {
250-
// ts could not resolve the module. Likely because `allowJs` is not set to `true`
251-
const err = buildWarn(buildCtx.diagnostics);
252-
err.messageText = `Unable to resolve import "${statement.moduleSpecifier.getText()}" from "${currentSource.fileName}".
253-
This can happen when trying to resolve .js files and "allowJs" is not set to "true" in your tsconfig.json.`;
254-
if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration);
255-
return;
256-
}
257-
}
258-
259-
const matchedStatement = foundSource.statements.find(matchesNamedDeclaration(className));
260-
if (!matchedStatement) {
261-
// we couldn't find the imported declaration as an exported statement in the module
262-
const err = buildWarn(buildCtx.diagnostics);
263-
err.messageText = `Unable to find "${className}" in the imported module "${statement.moduleSpecifier.getText()}".
264-
Please import class / mixin-factory declarations directly and not via barrel files.`;
265-
if (!buildCtx.config._isTesting) augmentDiagnosticWithNode(err, classDeclaration);
266-
return;
267-
}
268-
269-
foundClassDeclaration = matchedStatement
270-
? ts.isClassDeclaration(matchedStatement)
271-
? matchedStatement
272-
: undefined
273-
: undefined;
274-
275-
if (!foundClassDeclaration && matchedStatement) {
276-
// 5.b) the found `extends` type does not resolve to a class declaration;
277-
// if it's wrapped in a function - let's try and find it inside
278-
foundClassDeclaration = findClassWalk(matchedStatement);
279-
keepLooking = false;
280-
}
281-
282-
if (foundClassDeclaration && !dependentClasses.some((dc) => dc.classNode === foundClassDeclaration)) {
283-
// 6) if we found the class declaration, push it and check if it itself extends from another class
284-
dependentClasses.push({
285-
classNode: foundClassDeclaration,
286-
sourceFile: foundSource,
287-
fileName: currentSource.fileName,
288-
});
289-
290-
if (keepLooking) {
291-
buildExtendsTree(
292-
compilerCtx,
293-
foundClassDeclaration,
294-
dependentClasses,
295-
typeChecker,
296-
buildCtx,
297-
ogModule,
298-
);
299-
}
300-
return;
301-
}
302-
}
303367
}
304368
});
305369
}
306370
});
371+
372+
if (!importStatements.length) {
373+
// we're in a cjs module (probably in a Jest test) - loop through require modules statements
374+
const requireStatements = currentSource.statements.filter(ts.isVariableStatement);
375+
requireStatements.forEach((statement) => {
376+
statement.declarationList.declarations.forEach((declaration) => {
377+
if (
378+
declaration.initializer &&
379+
ts.isCallExpression(declaration.initializer) &&
380+
ts.isIdentifier(declaration.initializer.expression) &&
381+
declaration.initializer.expression.escapedText === 'require' &&
382+
declaration.initializer.arguments.length === 1 &&
383+
ts.isStringLiteral(declaration.initializer.arguments[0])
384+
) {
385+
const moduleSpecifier = declaration.initializer.arguments[0].text.replaceAll(/['"]/g, '');
386+
const className = extendee.getText();
387+
388+
resolveAndProcessExtendedClass(
389+
compilerCtx,
390+
buildCtx,
391+
classDeclaration,
392+
currentSource,
393+
moduleSpecifier,
394+
className,
395+
dependentClasses,
396+
keepLooking,
397+
typeChecker,
398+
ogModule,
399+
);
400+
}
401+
});
402+
});
403+
}
307404
}
308405
});
309406

src/compiler/transformers/transform-utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,8 @@ export function foundSuper(constructorBodyStatements: ts.NodeArray<ts.Statement>
11391139

11401140
/**
11411141
* Helper util for updating the constructor on a class declaration AST node.
1142+
* - Adds a `super()` call if needed
1143+
* - Merges in any provided statements into the constructor body
11421144
*
11431145
* @param classNode the class node whose constructor will be updated
11441146
* @param classMembers a list of class members for that class
@@ -1161,6 +1163,7 @@ export const updateConstructor = (
11611163

11621164
if (constructorIndex < 0 && !statements?.length && !needsSuper(classNode)) return classMembers;
11631165

1166+
// we have a constructor; let's update it
11641167
if (constructorIndex >= 0 && ts.isConstructorDeclaration(constructorMethod)) {
11651168
const constructorBodyStatements = constructorMethod.body?.statements;
11661169
let foundSuperCall = foundSuper(constructorBodyStatements);
@@ -1192,7 +1195,7 @@ export const updateConstructor = (
11921195
classMembers[constructorIndex] = ts.factory.updateConstructorDeclaration(
11931196
constructorMethod,
11941197
retrieveTsModifiers(constructorMethod),
1195-
constructorMethod.parameters.concat(parameters ?? []),
1198+
[...[...(parameters ?? []), ...constructorMethod.parameters]],
11961199
ts.factory.updateBlock(constructorMethod?.body ?? ts.factory.createBlock([]), statements),
11971200
);
11981201
} else {

0 commit comments

Comments
 (0)