diff --git a/src/compiler/bundle/app-data-plugin.ts b/src/compiler/bundle/app-data-plugin.ts index 7d3f2a272e2..24a98deaf56 100644 --- a/src/compiler/bundle/app-data-plugin.ts +++ b/src/compiler/bundle/app-data-plugin.ts @@ -148,12 +148,12 @@ const appendGlobalScripts = (globalScripts: GlobalScript[], s: MagicString) => { }; const appendBuildConditionals = (config: d.Config, build: d.BuildConditionals, s: MagicString) => { - const builData = Object.keys(build) + const buildData = Object.keys(build) .sort() .map((key) => key + ': ' + ((build as any)[key] ? 'true' : 'false')) .join(', '); - s.append(`export const BUILD = /* ${config.fsNamespace} */ { ${builData} };\n`); + s.append(`export const BUILD = /* ${config.fsNamespace} */ { ${buildData} };\n`); }; const appendEnv = (config: d.Config, s: MagicString) => { diff --git a/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts b/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts index a5fb8c882e3..dfe4be5ed6a 100644 --- a/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts +++ b/src/compiler/output-targets/dist-custom-elements/custom-elements-types.ts @@ -90,6 +90,10 @@ const generateCustomElementType = (componentsDtsRelPath: string, cmp: d.Componen ` prototype: ${tagNameAsPascal};`, ` new (): ${tagNameAsPascal};`, `};`, + `/**`, + ` * Used to define this component and all nested components recursively.`, + ` */`, + `export const defineCustomElement: () => void;`, ``, ]; diff --git a/src/compiler/output-targets/dist-custom-elements/index.ts b/src/compiler/output-targets/dist-custom-elements/index.ts index c5e93aaaaae..45bab9f6188 100644 --- a/src/compiler/output-targets/dist-custom-elements/index.ts +++ b/src/compiler/output-targets/dist-custom-elements/index.ts @@ -1,11 +1,12 @@ import type * as d from '../../../declarations'; import type { BundleOptions } from '../../bundle/bundle-interface'; import { bundleOutput } from '../../bundle/bundle-output'; -import { catchError, dashToPascalCase, formatComponentRuntimeMeta, hasError, stringifyRuntimeData } from '@utils'; +import { catchError, dashToPascalCase, hasError } from '@utils'; import { getCustomElementsBuildConditionals } from '../dist-custom-elements-bundle/custom-elements-build-conditionals'; import { isOutputTargetDistCustomElements } from '../output-utils'; import { join } from 'path'; import { nativeComponentTransform } from '../../transformers/component-native/tranform-to-native-component'; +import { addDefineCustomElementFunctions } from '../../transformers/component-native/add-define-custom-element-function'; import { optimizeModule } from '../../optimize/optimize-module'; import { removeCollectionImports } from '../../transformers/remove-collection-imports'; import { STENCIL_INTERNAL_CLIENT_ID, USER_INDEX_ENTRY_ID, STENCIL_APP_GLOBALS_ID } from '../../bundle/entry-alias-ids'; @@ -39,7 +40,12 @@ const bundleCustomElements = async ( id: 'customElements', platform: 'client', conditionals: getCustomElementsBuildConditionals(config, buildCtx.components), - customTransformers: getCustomElementBundleCustomTransformer(config, compilerCtx), + customTransformers: getCustomElementBundleCustomTransformer( + config, + compilerCtx, + buildCtx.components, + outputTarget + ), externalRuntime: !!outputTarget.externalRuntime, inlineWorkers: true, inputs: { @@ -69,6 +75,7 @@ const bundleCustomElements = async ( const files = rollupOutput.output.map(async (bundle) => { if (bundle.type === 'chunk') { let code = bundle.code; + const optimizeResults = await optimizeModule(config, compilerCtx, { input: code, isCore: bundle.isEntry, @@ -106,11 +113,11 @@ const addCustomElementInputs = ( if (cmp.isPlain) { exp.push(`export { ${importName} as ${exportName} } from '${cmp.sourceFilePath}';`); } else { - const meta = stringifyRuntimeData(formatComponentRuntimeMeta(cmp, false)); - - exp.push(`import { proxyCustomElement } from '${STENCIL_INTERNAL_CLIENT_ID}';`); - exp.push(`import { ${importName} as ${importAs} } from '${cmp.sourceFilePath}';`); - exp.push(`export const ${exportName} = /*@__PURE__*/proxyCustomElement(${importAs}, ${meta});`); + exp.push( + `import { ${importName} as ${importAs}, defineCustomElement as cmpDefCustomEle } from '${cmp.sourceFilePath}';` + ); + exp.push(`export const ${exportName} = ${importAs};`); + exp.push(`export const defineCustomElement = cmpDefCustomEle;`); } bundleOpts.inputs[cmp.tagName] = coreKey; @@ -134,7 +141,12 @@ const generateEntryPoint = (outputTarget: d.OutputTargetDistCustomElements, _bui return [...imp, ...exp].join('\n') + '\n'; }; -const getCustomElementBundleCustomTransformer = (config: d.Config, compilerCtx: d.CompilerCtx) => { +const getCustomElementBundleCustomTransformer = ( + config: d.Config, + compilerCtx: d.CompilerCtx, + components: d.ComponentCompilerMeta[], + outputTarget: d.OutputTargetDistCustomElements +) => { const transformOpts: d.TransformOptions = { coreImportPath: STENCIL_INTERNAL_CLIENT_ID, componentExport: null, @@ -145,6 +157,7 @@ const getCustomElementBundleCustomTransformer = (config: d.Config, compilerCtx: styleImportData: 'queryparams', }; return [ + addDefineCustomElementFunctions(compilerCtx, components, outputTarget), updateStencilCoreImports(transformOpts.coreImportPath), nativeComponentTransform(compilerCtx, transformOpts), removeCollectionImports(compilerCtx), diff --git a/src/compiler/transformers/add-component-meta-proxy.ts b/src/compiler/transformers/add-component-meta-proxy.ts index 95652efc028..4a16632d7a7 100644 --- a/src/compiler/transformers/add-component-meta-proxy.ts +++ b/src/compiler/transformers/add-component-meta-proxy.ts @@ -15,12 +15,14 @@ export const addModuleMetadataProxies = (tsSourceFile: ts.SourceFile, moduleFile }; const addComponentMetadataProxy = (compilerMeta: d.ComponentCompilerMeta) => { + return ts.createStatement(createComponentMetadataProxy(compilerMeta)); +}; + +export const createComponentMetadataProxy = (compilerMeta: d.ComponentCompilerMeta) => { const compactMeta: d.ComponentRuntimeMetaCompact = formatComponentRuntimeMeta(compilerMeta, true); - const liternalCmpClassName = ts.createIdentifier(compilerMeta.componentClassName); - const liternalMeta = convertValueToLiteral(compactMeta); + const literalCmpClassName = ts.createIdentifier(compilerMeta.componentClassName); + const literalMeta = convertValueToLiteral(compactMeta); - return ts.createStatement( - ts.createCall(ts.createIdentifier(PROXY_CUSTOM_ELEMENT), [], [liternalCmpClassName, liternalMeta]) - ); + return ts.createCall(ts.createIdentifier(PROXY_CUSTOM_ELEMENT), [], [literalCmpClassName, literalMeta]); }; diff --git a/src/compiler/transformers/add-imports.ts b/src/compiler/transformers/add-imports.ts index e949142defa..1bd7c7e808b 100644 --- a/src/compiler/transformers/add-imports.ts +++ b/src/compiler/transformers/add-imports.ts @@ -1,5 +1,6 @@ import type * as d from '../../declarations'; import ts from 'typescript'; +import { createRequireStatement, createImportStatement } from './transform-utils'; export const addImports = ( transformOpts: d.TransformOptions, @@ -13,79 +14,15 @@ export const addImports = ( if (transformOpts.module === 'cjs') { // CommonJS require() - return addCjsRequires(tsSourceFile, importFnNames, importPath); + const newRequire = createRequireStatement(importFnNames, importPath); + const statements = tsSourceFile.statements.slice(); + statements.splice(2, 0, newRequire); + return ts.updateSourceFileNode(tsSourceFile, statements); } // ESM Imports - return addEsmImports(tsSourceFile, importFnNames, importPath); -}; - -const addEsmImports = (tsSourceFile: ts.SourceFile, importFnNames: string[], importPath: string) => { - // ESM Imports - // import { importNames } from 'importPath'; - - const importSpecifiers = importFnNames.map((importKey) => { - const splt = importKey.split(' as '); - let importAs = importKey; - let importFnName = importKey; - - if (splt.length > 1) { - importAs = splt[1]; - importFnName = splt[0]; - } - - return ts.createImportSpecifier( - typeof importFnName === 'string' && importFnName !== importAs ? ts.createIdentifier(importFnName) : undefined, - ts.createIdentifier(importAs) - ); - }); - + const newImport = createImportStatement(importFnNames, importPath); const statements = tsSourceFile.statements.slice(); - const newImport = ts.createImportDeclaration( - undefined, - undefined, - ts.createImportClause(undefined, ts.createNamedImports(importSpecifiers)), - ts.createLiteral(importPath) - ); statements.unshift(newImport); return ts.updateSourceFileNode(tsSourceFile, statements); }; - -const addCjsRequires = (tsSourceFile: ts.SourceFile, importFnNames: string[], importPath: string) => { - // CommonJS require() - // const { a, b, c } = require(importPath); - - const importBinding = ts.createObjectBindingPattern( - importFnNames.map((importKey) => { - const splt = importKey.split(' as '); - let importAs = importKey; - let importFnName = importKey; - - if (splt.length > 1) { - importAs = splt[1]; - importFnName = splt[0]; - } - return ts.createBindingElement(undefined, importFnName, importAs); - }) - ); - - const requireStatement = ts.createVariableStatement( - undefined, - ts.createVariableDeclarationList( - [ - ts.createVariableDeclaration( - importBinding, - undefined, - ts.createCall(ts.createIdentifier('require'), [], [ts.createLiteral(importPath)]) - ), - ], - ts.NodeFlags.Const - ) - ); - - const statements = tsSourceFile.statements.slice(); - - statements.splice(2, 0, requireStatement); - - return ts.updateSourceFileNode(tsSourceFile, statements); -}; diff --git a/src/compiler/transformers/component-native/add-define-custom-element-function.ts b/src/compiler/transformers/component-native/add-define-custom-element-function.ts new file mode 100644 index 00000000000..a74ee1821fc --- /dev/null +++ b/src/compiler/transformers/component-native/add-define-custom-element-function.ts @@ -0,0 +1,245 @@ +import type * as d from '../../../declarations'; +import { createImportStatement, getModuleFromSourceFile } from '../transform-utils'; +import { dashToPascalCase } from '@utils'; +import ts from 'typescript'; +import { createComponentMetadataProxy } from '../add-component-meta-proxy'; +import { addCoreRuntimeApi, RUNTIME_APIS } from '../core-runtime-apis'; + +/** + * Import and define components along with any component dependents within the `dist-custom-elements` output. + * Adds `defineCustomElement()` function for all components. + * @param compilerCtx - current compiler context + * @param components - all current components within the stencil buildCtx + * @param outputTarget - the output target being compiled + * @returns a TS AST transformer factory function + */ +export const addDefineCustomElementFunctions = ( + compilerCtx: d.CompilerCtx, + components: d.ComponentCompilerMeta[], + outputTarget: d.OutputTargetDistCustomElements +): ts.TransformerFactory => { + return () => { + return (tsSourceFile: ts.SourceFile): ts.SourceFile => { + const moduleFile = getModuleFromSourceFile(compilerCtx, tsSourceFile); + const newStatements: ts.Statement[] = []; + const caseStatements: ts.CaseClause[] = []; + const tagNames: string[] = []; + + addCoreRuntimeApi(moduleFile, RUNTIME_APIS.proxyCustomElement); + + if (moduleFile.cmps.length) { + const principalComponent = moduleFile.cmps[0]; + tagNames.push(principalComponent.tagName); + + // wraps the initial component class in a `proxyCustomElement` wrapper. + // This is what will be exported and called from the `defineCustomElement` call. + const proxyDefinition = createComponentMetadataProxy(principalComponent); + const metaExpression = ts.factory.createExpressionStatement( + ts.factory.createBinaryExpression( + ts.factory.createIdentifier(principalComponent.componentClassName), + ts.factory.createToken(ts.SyntaxKind.EqualsToken), + proxyDefinition + ) + ); + newStatements.push(metaExpression); + ts.addSyntheticLeadingComment(proxyDefinition, ts.SyntaxKind.MultiLineCommentTrivia, '@__PURE__', false); + + // define the current component - `customElements.define(tagName, MyProxiedComponent);` + const customElementsDefineCallExpression = ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('customElements'), 'define'), + undefined, + [ts.factory.createIdentifier('tagName'), ts.factory.createIdentifier(principalComponent.componentClassName)] + ); + // create a `case` block that defines the current component. We'll add them to our switch statement later. + caseStatements.push( + createCustomElementsDefineCase(principalComponent.tagName, customElementsDefineCallExpression) + ); + + setupComponentDependencies(moduleFile, components, newStatements, caseStatements, tagNames); + addDefineCustomElementFunction(tagNames, newStatements, caseStatements); + + if (outputTarget.autoDefineCustomElements) { + const conditionalDefineCustomElementCall = createAutoDefinitionExpression( + principalComponent.componentClassName + ); + newStatements.push(conditionalDefineCustomElementCall); + } + } + + tsSourceFile = ts.factory.updateSourceFile(tsSourceFile, [...tsSourceFile.statements, ...newStatements]); + + return tsSourceFile; + }; + }; +}; + +/** + * Adds dependent component import statements and sets up and case blocks + * @param moduleFile current components' module + * @param components all current components within the stencil buildCtx + * @param newStatements new top level statement array to add to that will get added to the AST + * @param caseStatements an array of case statement blocks to add to. Will get added to `defineCustomElement` later + * @param tagNames array of all related component tag-names to add to + */ +const setupComponentDependencies = ( + moduleFile: d.Module, + components: d.ComponentCompilerMeta[], + newStatements: ts.Statement[], + caseStatements: ts.CaseClause[], + tagNames: string[] +) => { + moduleFile.cmps.forEach((cmp) => { + cmp.dependencies.forEach((dCmp) => { + const foundDep = components.find((dComp) => dComp.tagName === dCmp); + const exportName = dashToPascalCase(foundDep.tagName); + const importAs = `$${exportName}DefineCustomElement`; + tagNames.push(foundDep.tagName); + + // Will add `import { defineCustomElement as $ComponentDefineCustomElement } from 'my-nested-component.tsx';` + newStatements.push(createImportStatement([`defineCustomElement as ${importAs}`], foundDep.sourceFilePath)); + + // define a dependent component by recursively calling their own `defineCustomElement()` + const callExpression = ts.factory.createCallExpression(ts.factory.createIdentifier(importAs), undefined, []); + // `case` blocks that define the dependent components. We'll add them to our switch statement later. + caseStatements.push(createCustomElementsDefineCase(foundDep.tagName, callExpression)); + }); + }); +}; + +/** + * Creates a case block which will be used to define components. e.g. + * ``` javascript + * case "my-component": + * if (!customElements.get(tagName)) { + * customElements.define(tagName, MyProxiedComponent); + * // OR for dependent components + * defineCustomElement(tagName); + * } + * break; + * } }); + ``` + * @param tagName the components' tagName saved within stencil. + * @param actionExpression the actual expression to call to define the customElement + * @returns ts AST CaseClause + */ +const createCustomElementsDefineCase = (tagName: string, actionExpression: ts.Expression): ts.CaseClause => { + return ts.factory.createCaseClause(ts.factory.createStringLiteral(tagName), [ + ts.factory.createIfStatement( + ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('customElements'), 'get'), + undefined, + [ts.factory.createIdentifier('tagName')] + ) + ), + ts.factory.createBlock([ts.factory.createExpressionStatement(actionExpression)]) + ), + ts.factory.createBreakStatement(), + ]); +}; + +/** + * Add the main `defineCustomElement` function e.g. + * ```javascript + * function defineCustomElement() { + * const components = ['my-component']; + * components.forEach(tagName => { + * switch (tagName) { + * case "my-component": + * if (!customElements.get(tagName)) { + * customElements.define(tagName, MyProxiedComponent); + * // OR for dependent components + * defineCustomElement(tagName); + * } + * break; + * } + * }); + * } + ``` + * @param tagNames all components that will be defined + * @param newStatements new top level statement array that will get added to the AST + * @param caseStatements an array of case statement blocks. Will get added to `defineCustomElement` later + */ +const addDefineCustomElementFunction = ( + tagNames: string[], + newStatements: ts.Statement[], + caseStatements: ts.CaseClause[] +) => { + const newExpression = ts.factory.createFunctionDeclaration( + undefined, + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + undefined, + ts.factory.createIdentifier('defineCustomElement'), + undefined, + undefined, + undefined, + ts.factory.createBlock( + [ + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + 'components', + undefined, + undefined, + ts.factory.createArrayLiteralExpression( + tagNames.map((tagName) => ts.factory.createStringLiteral(tagName)) + ) + ), + ], + ts.NodeFlags.Const + ) + ), + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('components'), 'forEach'), + undefined, + [ + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + ts.factory.createIdentifier('tagName'), + undefined, + undefined + ), + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createBlock([ + ts.factory.createSwitchStatement( + ts.factory.createIdentifier('tagName'), + ts.factory.createCaseBlock(caseStatements) + ), + ]) + ), + ] + ) + ), + ], + true + ) + ); + newStatements.push(newExpression); +}; + +/** + * Create a call to `defineCustomElement` for the principle web component. + * ```typescript + * defineCustomElement(MyPrincipalComponent); + * ``` + * @returns the expression statement described above + */ +function createAutoDefinitionExpression(componentName: string): ts.ExpressionStatement { + return ts.factory.createExpressionStatement( + ts.factory.createCallExpression(ts.factory.createIdentifier('defineCustomElement'), undefined, [ + ts.factory.createIdentifier(componentName), + ]) + ); +} diff --git a/src/compiler/transformers/transform-utils.ts b/src/compiler/transformers/transform-utils.ts index 9e957132d0d..ce69f21c0b4 100644 --- a/src/compiler/transformers/transform-utils.ts +++ b/src/compiler/transformers/transform-utils.ts @@ -569,6 +569,67 @@ export const isAsyncFn = (typeChecker: ts.TypeChecker, methodDeclaration: ts.Met return typeStr.includes('Promise<'); }; +export const createImportStatement = (importFnNames: string[], importPath: string) => { + // ESM Imports + // import { importNames } from 'importPath'; + + const importSpecifiers = importFnNames.map((importKey) => { + const splt = importKey.split(' as '); + let importAs = importKey; + let importFnName = importKey; + + if (splt.length > 1) { + importAs = splt[1]; + importFnName = splt[0]; + } + + return ts.createImportSpecifier( + typeof importFnName === 'string' && importFnName !== importAs ? ts.createIdentifier(importFnName) : undefined, + ts.createIdentifier(importAs) + ); + }); + + return ts.createImportDeclaration( + undefined, + undefined, + ts.createImportClause(undefined, ts.createNamedImports(importSpecifiers)), + ts.createLiteral(importPath) + ); +}; + +export const createRequireStatement = (importFnNames: string[], importPath: string) => { + // CommonJS require() + // const { a, b, c } = require(importPath); + + const importBinding = ts.createObjectBindingPattern( + importFnNames.map((importKey) => { + const splt = importKey.split(' as '); + let importAs = importKey; + let importFnName = importKey; + + if (splt.length > 1) { + importAs = splt[1]; + importFnName = splt[0]; + } + return ts.createBindingElement(undefined, importFnName, importAs); + }) + ); + + return ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + importBinding, + undefined, + ts.createCall(ts.createIdentifier('require'), [], [ts.createLiteral(importPath)]) + ), + ], + ts.NodeFlags.Const + ) + ); +}; + export interface ConvertIdentifier { __identifier: boolean; __escapedText: string; diff --git a/src/compiler/transformers/update-component-class.ts b/src/compiler/transformers/update-component-class.ts index c512074fa07..46cdca903c7 100644 --- a/src/compiler/transformers/update-component-class.ts +++ b/src/compiler/transformers/update-component-class.ts @@ -62,7 +62,7 @@ const createConstClass = ( ts.createClassExpression(classModifiers, undefined, classNode.typeParameters, heritageClauses, members) ), ], - ts.NodeFlags.Const + ts.NodeFlags.Let ) ); }; diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 86370a289f6..6360b94b29f 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -1916,6 +1916,12 @@ export interface OutputTargetDistCustomElements extends OutputTargetBaseNext { inlineDynamicImports?: boolean; includeGlobalScripts?: boolean; minify?: boolean; + /** + * Enables the auto-definition of a component and its children (recursively) in the custom elements registry. This + * functionality allows consumers to bypass the explicit call to define a component, its children, its children's + * children, etc. Users of this flag should be aware that enabling this functionality may increase bundle size. + */ + autoDefineCustomElements?: boolean; } export interface OutputTargetDistCustomElementsBundle extends OutputTargetBaseNext { diff --git a/test/karma/.gitignore b/test/karma/.gitignore index ac0c752db83..8d51d441336 100644 --- a/test/karma/.gitignore +++ b/test/karma/.gitignore @@ -1,2 +1,3 @@ /www -/test-dist \ No newline at end of file +/test-dist +/test-components diff --git a/test/karma/package.json b/test/karma/package.json index b3af5d127f5..4c753cbc12e 100644 --- a/test/karma/package.json +++ b/test/karma/package.json @@ -19,7 +19,7 @@ "karma.prod": "npm run build.sibling && npm run build.invisible-prehydration && npm run build.app && npm run karma.webpack && npm run build.prerender && npm run karma", "karma.ie": "karma start karma.config.js --browsers=IE --single-run=false", "karma.edge": "karma start karma.config.js --browsers=Edge --single-run=false", - "karma.webpack": "webpack-cli --config test-app/esm-webpack/webpack.config.js", + "karma.webpack": "webpack-cli --config test-app/esm-webpack/webpack.config.js && webpack-cli --config test-app/custom-elements-output-webpack/webpack.config.js && webpack-cli --config test-app/custom-elements-output-tag-class-different/webpack.config.js", "start": "node ../../bin/stencil build --dev --watch --serve --es5" }, "devDependencies": { diff --git a/test/karma/stencil.config.ts b/test/karma/stencil.config.ts index 486146b2ddf..97309cf09c6 100644 --- a/test/karma/stencil.config.ts +++ b/test/karma/stencil.config.ts @@ -20,6 +20,10 @@ export const config: Config = { type: 'dist', dir: 'test-dist', }, + { + type: 'dist-custom-elements', + dir: 'test-components', + }, ], globalScript: 'test-app/global.ts', globalStyle: 'test-app/style-plugin/global-sass-entry.scss', diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index c80daaf12d9..d8ca8970c45 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -63,6 +63,16 @@ export namespace Components { } interface CssVariablesShadowDom { } + interface CustomElementChild { + } + interface CustomElementChildDifferentNameThanClass { + } + interface CustomElementNestedChild { + } + interface CustomElementRoot { + } + interface CustomElementRootDifferentNameThanClass { + } interface CustomEventRoot { } interface DelegatesFocus { @@ -407,6 +417,36 @@ declare global { prototype: HTMLCssVariablesShadowDomElement; new (): HTMLCssVariablesShadowDomElement; }; + interface HTMLCustomElementChildElement extends Components.CustomElementChild, HTMLStencilElement { + } + var HTMLCustomElementChildElement: { + prototype: HTMLCustomElementChildElement; + new (): HTMLCustomElementChildElement; + }; + interface HTMLCustomElementChildDifferentNameThanClassElement extends Components.CustomElementChildDifferentNameThanClass, HTMLStencilElement { + } + var HTMLCustomElementChildDifferentNameThanClassElement: { + prototype: HTMLCustomElementChildDifferentNameThanClassElement; + new (): HTMLCustomElementChildDifferentNameThanClassElement; + }; + interface HTMLCustomElementNestedChildElement extends Components.CustomElementNestedChild, HTMLStencilElement { + } + var HTMLCustomElementNestedChildElement: { + prototype: HTMLCustomElementNestedChildElement; + new (): HTMLCustomElementNestedChildElement; + }; + interface HTMLCustomElementRootElement extends Components.CustomElementRoot, HTMLStencilElement { + } + var HTMLCustomElementRootElement: { + prototype: HTMLCustomElementRootElement; + new (): HTMLCustomElementRootElement; + }; + interface HTMLCustomElementRootDifferentNameThanClassElement extends Components.CustomElementRootDifferentNameThanClass, HTMLStencilElement { + } + var HTMLCustomElementRootDifferentNameThanClassElement: { + prototype: HTMLCustomElementRootDifferentNameThanClassElement; + new (): HTMLCustomElementRootDifferentNameThanClassElement; + }; interface HTMLCustomEventRootElement extends Components.CustomEventRoot, HTMLStencilElement { } var HTMLCustomEventRootElement: { @@ -1014,6 +1054,11 @@ declare global { "css-cmp": HTMLCssCmpElement; "css-variables-no-encapsulation": HTMLCssVariablesNoEncapsulationElement; "css-variables-shadow-dom": HTMLCssVariablesShadowDomElement; + "custom-element-child": HTMLCustomElementChildElement; + "custom-element-child-different-name-than-class": HTMLCustomElementChildDifferentNameThanClassElement; + "custom-element-nested-child": HTMLCustomElementNestedChildElement; + "custom-element-root": HTMLCustomElementRootElement; + "custom-element-root-different-name-than-class": HTMLCustomElementRootDifferentNameThanClassElement; "custom-event-root": HTMLCustomEventRootElement; "delegates-focus": HTMLDelegatesFocusElement; "dom-reattach": HTMLDomReattachElement; @@ -1169,6 +1214,16 @@ declare namespace LocalJSX { } interface CssVariablesShadowDom { } + interface CustomElementChild { + } + interface CustomElementChildDifferentNameThanClass { + } + interface CustomElementNestedChild { + } + interface CustomElementRoot { + } + interface CustomElementRootDifferentNameThanClass { + } interface CustomEventRoot { } interface DelegatesFocus { @@ -1429,6 +1484,11 @@ declare namespace LocalJSX { "css-cmp": CssCmp; "css-variables-no-encapsulation": CssVariablesNoEncapsulation; "css-variables-shadow-dom": CssVariablesShadowDom; + "custom-element-child": CustomElementChild; + "custom-element-child-different-name-than-class": CustomElementChildDifferentNameThanClass; + "custom-element-nested-child": CustomElementNestedChild; + "custom-element-root": CustomElementRoot; + "custom-element-root-different-name-than-class": CustomElementRootDifferentNameThanClass; "custom-event-root": CustomEventRoot; "delegates-focus": DelegatesFocus; "dom-reattach": DomReattach; @@ -1551,6 +1611,11 @@ declare module "@stencil/core" { "css-cmp": LocalJSX.CssCmp & JSXBase.HTMLAttributes; "css-variables-no-encapsulation": LocalJSX.CssVariablesNoEncapsulation & JSXBase.HTMLAttributes; "css-variables-shadow-dom": LocalJSX.CssVariablesShadowDom & JSXBase.HTMLAttributes; + "custom-element-child": LocalJSX.CustomElementChild & JSXBase.HTMLAttributes; + "custom-element-child-different-name-than-class": LocalJSX.CustomElementChildDifferentNameThanClass & JSXBase.HTMLAttributes; + "custom-element-nested-child": LocalJSX.CustomElementNestedChild & JSXBase.HTMLAttributes; + "custom-element-root": LocalJSX.CustomElementRoot & JSXBase.HTMLAttributes; + "custom-element-root-different-name-than-class": LocalJSX.CustomElementRootDifferentNameThanClass & JSXBase.HTMLAttributes; "custom-event-root": LocalJSX.CustomEventRoot & JSXBase.HTMLAttributes; "delegates-focus": LocalJSX.DelegatesFocus & JSXBase.HTMLAttributes; "dom-reattach": LocalJSX.DomReattach & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/custom-elements-output-tag-class-different/custom-element-child.tsx b/test/karma/test-app/custom-elements-output-tag-class-different/custom-element-child.tsx new file mode 100644 index 00000000000..d270f77f1b9 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-tag-class-different/custom-element-child.tsx @@ -0,0 +1,15 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'custom-element-child-different-name-than-class', + shadow: true, +}) +export class CustomElementChild { + render() { + return ( +
+ Child Component Loaded! +
+ ); + } +} diff --git a/test/karma/test-app/custom-elements-output-tag-class-different/custom-element-root.tsx b/test/karma/test-app/custom-elements-output-tag-class-different/custom-element-root.tsx new file mode 100644 index 00000000000..7dae8d64561 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-tag-class-different/custom-element-root.tsx @@ -0,0 +1,16 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'custom-element-root-different-name-than-class', + shadow: true, +}) +export class CustomElementRoot { + render() { + return ( +
+

Root Element Loaded

+ +
+ ); + } +} diff --git a/test/karma/test-app/custom-elements-output-tag-class-different/index.esm.js b/test/karma/test-app/custom-elements-output-tag-class-different/index.esm.js new file mode 100644 index 00000000000..c25719eb529 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-tag-class-different/index.esm.js @@ -0,0 +1,3 @@ +import { defineCustomElement } from '../../test-components/custom-element-root-different-name-than-class'; + +defineCustomElement(); diff --git a/test/karma/test-app/custom-elements-output-tag-class-different/index.html b/test/karma/test-app/custom-elements-output-tag-class-different/index.html new file mode 100644 index 00000000000..31b4230b368 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-tag-class-different/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/test/karma/test-app/custom-elements-output-tag-class-different/karma.spec.ts b/test/karma/test-app/custom-elements-output-tag-class-different/karma.spec.ts new file mode 100644 index 00000000000..959d5df6155 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-tag-class-different/karma.spec.ts @@ -0,0 +1,21 @@ +import { setupDomTests } from '../util'; + +describe('custom-elements-output-tag-class-different', () => { + const { setupDom, tearDownDom } = setupDomTests(document); + let app: HTMLElement; + + beforeEach(async () => { + app = await setupDom('/custom-elements-output-tag-class-different/index.html'); + }); + afterEach(tearDownDom); + + it('defines components and their dependencies', async () => { + expect(customElements.get('custom-element-root-different-name-than-class')).toBeDefined(); + + const elm = app.querySelector('custom-element-root-different-name-than-class'); + expect(elm.shadowRoot).toBeDefined(); + + const childElm = elm.shadowRoot.querySelector('custom-element-child-different-name-than-class'); + expect(childElm.shadowRoot).toBeDefined(); + }); +}); diff --git a/test/karma/test-app/custom-elements-output-tag-class-different/webpack.config.js b/test/karma/test-app/custom-elements-output-tag-class-different/webpack.config.js new file mode 100644 index 00000000000..ec098900f68 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-tag-class-different/webpack.config.js @@ -0,0 +1,19 @@ +const path = require('path'); + +module.exports = { + entry: path.resolve(__dirname, 'index.esm.js'), + output: { + path: path.resolve(__dirname, '..', '..', 'www', 'custom-elements-output-tag-class-different'), + publicPath: '/custom-elements-output-tag-class-different/', + }, + mode: 'production', + optimization: { + minimize: false, + }, + resolve: { + alias: { + '@stencil/core/internal/client': '../../../internal/client', + '@stencil/core/internal/app-data': '../app-data', + }, + }, +}; diff --git a/test/karma/test-app/custom-elements-output-webpack/custom-element-child.tsx b/test/karma/test-app/custom-elements-output-webpack/custom-element-child.tsx new file mode 100644 index 00000000000..0bbb3a2ab62 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/custom-element-child.tsx @@ -0,0 +1,18 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'custom-element-child', + shadow: true, +}) +export class CustomElementChild { + render() { + return ( +
+ Child Component Loaded! + +

Child Nested Component?

+ +
+ ); + } +} diff --git a/test/karma/test-app/custom-elements-output-webpack/custom-element-nested-child.tsx b/test/karma/test-app/custom-elements-output-webpack/custom-element-nested-child.tsx new file mode 100644 index 00000000000..dfe16b1ab0f --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/custom-element-nested-child.tsx @@ -0,0 +1,15 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'custom-element-nested-child', + shadow: true, +}) +export class CustomElementNestedChild { + render() { + return ( +
+ Child Nested Component Loaded! +
+ ); + } +} diff --git a/test/karma/test-app/custom-elements-output-webpack/custom-element-root.tsx b/test/karma/test-app/custom-elements-output-webpack/custom-element-root.tsx new file mode 100644 index 00000000000..6b7e3dae6fb --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/custom-element-root.tsx @@ -0,0 +1,18 @@ +import { Component, h } from '@stencil/core'; + +@Component({ + tag: 'custom-element-root', + shadow: true, +}) +export class CustomElementRoot { + render() { + return ( +
+

Root Element Loaded

+ +

Child Component Loaded?

+ +
+ ); + } +} diff --git a/test/karma/test-app/custom-elements-output-webpack/index.esm.js b/test/karma/test-app/custom-elements-output-webpack/index.esm.js new file mode 100644 index 00000000000..7f86130f282 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/index.esm.js @@ -0,0 +1,3 @@ +import { defineCustomElement } from '../../test-components/custom-element-root'; + +defineCustomElement(); diff --git a/test/karma/test-app/custom-elements-output-webpack/index.html b/test/karma/test-app/custom-elements-output-webpack/index.html new file mode 100644 index 00000000000..8343ac38c00 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/test/karma/test-app/custom-elements-output-webpack/karma.spec.ts b/test/karma/test-app/custom-elements-output-webpack/karma.spec.ts new file mode 100644 index 00000000000..0de12d82b14 --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/karma.spec.ts @@ -0,0 +1,25 @@ +import { setupDomTests } from '../util'; + +describe('custom-elements-output-webpack', () => { + const { setupDom, tearDownDom } = setupDomTests(document); + let app: HTMLElement; + + beforeEach(async () => { + app = await setupDom('/custom-elements-output-webpack/index.html'); + }); + afterEach(tearDownDom); + + it('defines components and their dependencies', async () => { + expect(customElements.get('custom-element-root')).toBeDefined(); + expect(customElements.get('custom-element-child')).toBeDefined(); + expect(customElements.get('custom-element-nested-child')).toBeDefined(); + + const elm = app.querySelector('custom-element-root'); + const childElm = elm.shadowRoot.querySelector('custom-element-child'); + const childNestedElm = childElm.shadowRoot.querySelector('custom-element-nested-child'); + + expect(elm.shadowRoot).toBeDefined(); + expect(childElm.shadowRoot).toBeDefined(); + expect(childNestedElm.shadowRoot).toBeDefined(); + }); +}); diff --git a/test/karma/test-app/custom-elements-output-webpack/webpack.config.js b/test/karma/test-app/custom-elements-output-webpack/webpack.config.js new file mode 100644 index 00000000000..67bdaae124e --- /dev/null +++ b/test/karma/test-app/custom-elements-output-webpack/webpack.config.js @@ -0,0 +1,19 @@ +const path = require('path'); + +module.exports = { + entry: path.resolve(__dirname, 'index.esm.js'), + output: { + path: path.resolve(__dirname, '..', '..', 'www', 'custom-elements-output-webpack'), + publicPath: '/custom-elements-output-webpack/', + }, + mode: 'production', + optimization: { + minimize: false, + }, + resolve: { + alias: { + '@stencil/core/internal/client': '../../../internal/client', + '@stencil/core/internal/app-data': '../app-data', + }, + }, +};