forked from ionic-team/stencil
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(bundling): allow proper webpack treeshaking (ionic-team#3248)
this commit solves an issue where stencil projects using webpack were unable to treeshake properly. specifically, this is a workaround to a webpack issue (webpack/webpack#14963) where webpack fails to treeshake when a variable is reassigned. with this commit, we introduce a new transformer to be run during the typescript transpilation process, `proxyCustomElement` that takes a stencil component's class initializer and hoists it as the first argument of `proxyCustomElement`. this eliminates the need to reassign the variable in the final output, which was causing code generated using the `dist-custom-elements` output target to fail to treeshake when used in a webpack project. with the introduction of this separate transformer, the creation of the `proxyCustomElement` call is removed from the `addDefineCustomElementFunctions` transformer. this was done for two reasons: 1. separation of concerns - proxying the component is not strictly necessary when creating `define` calls 2. proxying must occur after the initializer has been generated. currently, this occurs in `nativeComponentTransform`. therefore, this step must occur after `nativeComponentTransform`. as a part of creating this new transformer, a new function, `createAnonymousClassMetadataProxy` was created. we intentionally choose not to use the existing proxy creation funcitons in the same file where the new file is defiend in order to pass the class initializer directly to our new helper function. update-component-class has a variable statement creation call that was modified from `const` to `let` in 6987e43. With this commit, we can safely revert this change as we no longer redefine the variable holding the stencil component
- Loading branch information
1 parent
eebf68b
commit 5dccc85
Showing
7 changed files
with
417 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
98 changes: 98 additions & 0 deletions
98
src/compiler/transformers/component-native/proxy-custom-element-function.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,98 @@ | ||
import ts from 'typescript'; | ||
import type * as d from '../../../declarations'; | ||
import { createAnonymousClassMetadataProxy } from '../add-component-meta-proxy'; | ||
import { addImports } from '../add-imports'; | ||
import { RUNTIME_APIS } from '../core-runtime-apis'; | ||
import { getModuleFromSourceFile } from '../transform-utils'; | ||
|
||
/** | ||
* Proxy custom elements for the `dist-custom-elements` output target. This function searches for a Stencil component's | ||
* class initializer (found on the righthand side of the '=' operator): | ||
* | ||
* ```ts | ||
* const MyComponent = class extends HTMLElement { // Implementation omitted } | ||
* ``` | ||
* | ||
* and wraps the initializer into a `proxyCustomElement` call: | ||
* | ||
* ```ts | ||
* const MyComponent = proxyCustomElement(class extends HTMLElement { // Implementation omitted }, componentMetadata); | ||
* ``` | ||
* | ||
* This is to work around an issue where treeshaking does not work for webpack users, whose details are captured in full | ||
* in [this issue on the webpack GitHub repo](https://github.com/webpack/webpack/issues/14963). | ||
* | ||
* @param compilerCtx current compiler context | ||
* @param transformOpts transpilation options for the current build | ||
* @returns a TypeScript AST transformer factory function that performs the above described transformation | ||
*/ | ||
export const proxyCustomElement = ( | ||
compilerCtx: d.CompilerCtx, | ||
transformOpts: d.TransformOptions | ||
): ts.TransformerFactory<ts.SourceFile> => { | ||
return () => { | ||
return (tsSourceFile: ts.SourceFile): ts.SourceFile => { | ||
const moduleFile = getModuleFromSourceFile(compilerCtx, tsSourceFile); | ||
if (!moduleFile.cmps.length) { | ||
return tsSourceFile; | ||
} | ||
|
||
const principalComponent = moduleFile.cmps[0]; | ||
|
||
for (let [stmtIndex, stmt] of tsSourceFile.statements.entries()) { | ||
if (ts.isVariableStatement(stmt)) { | ||
for (let [declarationIndex, declaration] of stmt.declarationList.declarations.entries()) { | ||
if (declaration.name.getText() !== principalComponent.componentClassName) { | ||
continue; | ||
} | ||
|
||
// wrap the Stencil component's class declaration in a component proxy | ||
const proxyCreationCall = createAnonymousClassMetadataProxy(principalComponent, declaration.initializer); | ||
ts.addSyntheticLeadingComment(proxyCreationCall, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false); | ||
|
||
// update the component's variable declaration to use the new initializer | ||
const proxiedComponentDeclaration = ts.factory.updateVariableDeclaration( | ||
declaration, | ||
declaration.name, | ||
declaration.exclamationToken, | ||
declaration.type, | ||
proxyCreationCall | ||
); | ||
|
||
// update the declaration list that contains the updated variable declaration | ||
const updatedDeclarationList = ts.factory.updateVariableDeclarationList(stmt.declarationList, [ | ||
...stmt.declarationList.declarations.slice(0, declarationIndex), | ||
proxiedComponentDeclaration, | ||
...stmt.declarationList.declarations.slice(declarationIndex + 1), | ||
]); | ||
|
||
// update the variable statement containing the updated declaration list | ||
const updatedVariableStatement = ts.factory.updateVariableStatement( | ||
stmt, | ||
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], | ||
updatedDeclarationList | ||
); | ||
|
||
// update the source file's statements to use the new variable statement | ||
tsSourceFile = ts.factory.updateSourceFile(tsSourceFile, [ | ||
...tsSourceFile.statements.slice(0, stmtIndex), | ||
updatedVariableStatement, | ||
...tsSourceFile.statements.slice(stmtIndex + 1), | ||
]); | ||
|
||
// finally, ensure that the proxyCustomElement function is imported | ||
tsSourceFile = addImports( | ||
transformOpts, | ||
tsSourceFile, | ||
[RUNTIME_APIS.proxyCustomElement], | ||
transformOpts.coreImportPath | ||
); | ||
|
||
return tsSourceFile; | ||
} | ||
} | ||
} | ||
return tsSourceFile; | ||
}; | ||
}; | ||
}; |
109 changes: 109 additions & 0 deletions
109
src/compiler/transformers/test/add-component-meta-proxy.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,109 @@ | ||
import type * as d from '../../../declarations'; | ||
import { createAnonymousClassMetadataProxy } from '../add-component-meta-proxy'; | ||
import * as TransformUtils from '../transform-utils'; | ||
import * as FormatComponentRuntimeMeta from '../../../utils/format-component-runtime-meta'; | ||
import ts from 'typescript'; | ||
import { HTML_ELEMENT } from '../core-runtime-apis'; | ||
|
||
describe('add-component-meta-proxy', () => { | ||
describe('createAnonymousClassMetadataProxy()', () => { | ||
let classExpr: ts.ClassExpression; | ||
let htmlElementHeritageClause: ts.HeritageClause; | ||
let literalMetadata: ts.StringLiteral; | ||
|
||
let formatComponentRuntimeMetaSpy: jest.SpyInstance< | ||
ReturnType<typeof FormatComponentRuntimeMeta.formatComponentRuntimeMeta>, | ||
Parameters<typeof FormatComponentRuntimeMeta.formatComponentRuntimeMeta> | ||
>; | ||
let convertValueToLiteralSpy: jest.SpyInstance< | ||
ReturnType<typeof TransformUtils.convertValueToLiteral>, | ||
Parameters<typeof TransformUtils.convertValueToLiteral> | ||
>; | ||
|
||
beforeEach(() => { | ||
htmlElementHeritageClause = ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ | ||
ts.factory.createExpressionWithTypeArguments(ts.factory.createIdentifier(HTML_ELEMENT), []), | ||
]); | ||
|
||
classExpr = ts.factory.createClassExpression( | ||
undefined, | ||
undefined, | ||
'MyComponent', | ||
undefined, | ||
[htmlElementHeritageClause], | ||
undefined | ||
); | ||
literalMetadata = ts.factory.createStringLiteral('MyComponent'); | ||
|
||
formatComponentRuntimeMetaSpy = jest.spyOn(FormatComponentRuntimeMeta, 'formatComponentRuntimeMeta'); | ||
formatComponentRuntimeMetaSpy.mockImplementation( | ||
(_compilerMeta: d.ComponentCompilerMeta, _includeMethods: boolean) => [0, 'tag-name'] | ||
); | ||
|
||
convertValueToLiteralSpy = jest.spyOn(TransformUtils, 'convertValueToLiteral'); | ||
convertValueToLiteralSpy.mockImplementation((_compactMeta: d.ComponentRuntimeMetaCompact) => literalMetadata); | ||
}); | ||
|
||
afterEach(() => { | ||
formatComponentRuntimeMetaSpy.mockRestore(); | ||
convertValueToLiteralSpy.mockRestore(); | ||
}); | ||
|
||
it('returns a call expression', () => { | ||
const result: ts.CallExpression = createAnonymousClassMetadataProxy( | ||
// TODO(STENCIL-378): Replace with a getMockComponentCompilerMeta() call | ||
[] as unknown as d.ComponentCompilerMeta, | ||
classExpr | ||
); | ||
|
||
expect(ts.isCallExpression(result)).toBe(true); | ||
}); | ||
|
||
it('wraps the initializer in PROXY_CUSTOM_ELEMENT', () => { | ||
const result: ts.CallExpression = createAnonymousClassMetadataProxy( | ||
// TODO(STENCIL-378): Replace with a getMockComponentCompilerMeta() call | ||
[] as unknown as d.ComponentCompilerMeta, | ||
classExpr | ||
); | ||
|
||
expect((result.expression as ts.Identifier).escapedText).toBe('___stencil_proxyCustomElement'); | ||
}); | ||
|
||
it("doesn't add any type arguments to the call", () => { | ||
const result: ts.CallExpression = createAnonymousClassMetadataProxy( | ||
// TODO(STENCIL-378): Replace with a getMockComponentCompilerMeta() call | ||
[] as unknown as d.ComponentCompilerMeta, | ||
classExpr | ||
); | ||
|
||
expect(result.typeArguments).toHaveLength(0); | ||
}); | ||
|
||
it('adds the correct arguments to the PROXY_CUSTOM_ELEMENT call', () => { | ||
const result: ts.CallExpression = createAnonymousClassMetadataProxy( | ||
// TODO(STENCIL-378): Replace with a getMockComponentCompilerMeta() call | ||
[] as unknown as d.ComponentCompilerMeta, | ||
classExpr | ||
); | ||
|
||
expect(result.arguments).toHaveLength(2); | ||
expect(result.arguments[0]).toBe(classExpr); | ||
expect(result.arguments[1]).toBe(literalMetadata); | ||
}); | ||
|
||
it('includes the heritage clause', () => { | ||
const result: ts.CallExpression = createAnonymousClassMetadataProxy( | ||
// TODO(STENCIL-378): Replace with a getMockComponentCompilerMeta() call | ||
[] as unknown as d.ComponentCompilerMeta, | ||
classExpr | ||
); | ||
|
||
expect(result.arguments.length).toBeGreaterThanOrEqual(1); | ||
const createdClassExpression = result.arguments[0]; | ||
|
||
expect(ts.isClassExpression(createdClassExpression)).toBe(true); | ||
expect((createdClassExpression as ts.ClassExpression).heritageClauses).toHaveLength(1); | ||
expect((createdClassExpression as ts.ClassExpression).heritageClauses[0]).toBe(htmlElementHeritageClause); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.