diff --git a/package.json b/package.json index cb07368681..2aebb2c1f7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "clean": "lerna run clean && lerna clean --yes && rm -rf node_modules", "lint": "tslint -p tsconfig.json && npm run types", "types": "tsc --noEmit", - "test": "jest", + "test": "jest --config ./scripts/jest/root.config.js", "test:integration": "yarn run build && lerna exec --scope lwc-integration -- yarn sauce", "test:performance": "lerna exec --scope benchmark -- best --runner remote", "build": "lerna run build --ignore benchmark --ignore lwc-integration", diff --git a/packages/babel-plugin-transform-lwc-class/jest.config.js b/packages/babel-plugin-transform-lwc-class/jest.config.js new file mode 100644 index 0000000000..45171e32f3 --- /dev/null +++ b/packages/babel-plugin-transform-lwc-class/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'babel-plugin-transform-lwc-class', +}; diff --git a/packages/lwc-compiler/jest.config.js b/packages/lwc-compiler/jest.config.js new file mode 100644 index 0000000000..42052a2c38 --- /dev/null +++ b/packages/lwc-compiler/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'lwc-compiler', +}; diff --git a/packages/lwc-engine/jest.config.js b/packages/lwc-engine/jest.config.js new file mode 100644 index 0000000000..9ec582e029 --- /dev/null +++ b/packages/lwc-engine/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'lwc-engine', +}; diff --git a/packages/lwc-engine/package.json b/packages/lwc-engine/package.json index 3436340206..28d40b09c1 100644 --- a/packages/lwc-engine/package.json +++ b/packages/lwc-engine/package.json @@ -9,9 +9,9 @@ "clean": "rm -rf dist", "build": "concurrently \"yarn build:es-and-cjs\" \"yarn build:umd:prod\" \"yarn build:umd:dev\"", "test": "DIR=`pwd` && cd ../../ && yarn test $DIR", - "build:umd:dev": "rollup -c scripts/rollup.config.umd.dev.js", - "build:umd:prod": "rollup -c scripts/rollup.config.umd.prod.js", - "build:es-and-cjs": "rollup -c scripts/rollup.config.es-and-cjs.js" + "build:umd:dev": "rollup -c scripts/rollup/rollup.config.umd.dev.js", + "build:umd:prod": "rollup -c scripts/rollup/rollup.config.umd.prod.js", + "build:es-and-cjs": "rollup -c scripts/rollup/rollup.config.es-and-cjs.js" }, "devDependencies": { "concurrently": "^3.5.1", diff --git a/packages/lwc-engine/scripts/engine.rollup.config.util.js b/packages/lwc-engine/scripts/rollup/engine.rollup.config.util.js similarity index 100% rename from packages/lwc-engine/scripts/engine.rollup.config.util.js rename to packages/lwc-engine/scripts/rollup/engine.rollup.config.util.js diff --git a/packages/lwc-engine/scripts/rollup.config.es-and-cjs.js b/packages/lwc-engine/scripts/rollup/rollup.config.es-and-cjs.js similarity index 80% rename from packages/lwc-engine/scripts/rollup.config.es-and-cjs.js rename to packages/lwc-engine/scripts/rollup/rollup.config.es-and-cjs.js index 8e07fec4af..1a43c815ec 100644 --- a/packages/lwc-engine/scripts/rollup.config.es-and-cjs.js +++ b/packages/lwc-engine/scripts/rollup/rollup.config.es-and-cjs.js @@ -3,11 +3,11 @@ const typescript = require('rollup-plugin-typescript'); const nodeResolve = require('rollup-plugin-node-resolve'); const { generateTargetName, ignoreCircularDependencies } = require('./engine.rollup.config.util'); -const { version } = require('../package.json'); +const { version } = require('../../package.json'); -const entry = path.resolve(__dirname, '../src/framework/main.ts'); -const commonJSDirectory = path.resolve(__dirname, '../dist/commonjs'); -const modulesDirectory = path.resolve(__dirname, '../dist/modules'); +const entry = path.resolve(__dirname, '../../src/framework/main.ts'); +const commonJSDirectory = path.resolve(__dirname, '../../dist/commonjs'); +const modulesDirectory = path.resolve(__dirname, '../../dist/modules'); const banner = (`/* proxy-compat-disable */`); const footer = `/** version: ${version} */`; diff --git a/packages/lwc-engine/scripts/rollup.config.umd.dev.js b/packages/lwc-engine/scripts/rollup/rollup.config.umd.dev.js similarity index 86% rename from packages/lwc-engine/scripts/rollup.config.umd.dev.js rename to packages/lwc-engine/scripts/rollup/rollup.config.umd.dev.js index 25ecc78aa5..9aafc2afa2 100644 --- a/packages/lwc-engine/scripts/rollup.config.umd.dev.js +++ b/packages/lwc-engine/scripts/rollup/rollup.config.umd.dev.js @@ -3,11 +3,11 @@ const replace = require('rollup-plugin-replace'); const typescript = require('rollup-plugin-typescript'); const nodeResolve = require('rollup-plugin-node-resolve'); -const { version } = require('../package.json'); +const { version } = require('../../package.json'); const { generateTargetName, ignoreCircularDependencies } = require('./engine.rollup.config.util'); -const input = path.resolve(__dirname, '../src/framework/main.ts'); -const outputDir = path.resolve(__dirname, '../dist/umd'); +const input = path.resolve(__dirname, '../../src/framework/main.ts'); +const outputDir = path.resolve(__dirname, '../../dist/umd'); const banner = (`/* proxy-compat-disable */`); const footer = `/** version: ${version} */`; diff --git a/packages/lwc-engine/scripts/rollup.config.umd.prod.js b/packages/lwc-engine/scripts/rollup/rollup.config.umd.prod.js similarity index 90% rename from packages/lwc-engine/scripts/rollup.config.umd.prod.js rename to packages/lwc-engine/scripts/rollup/rollup.config.umd.prod.js index 0d8c61f967..4aad8e6786 100644 --- a/packages/lwc-engine/scripts/rollup.config.umd.prod.js +++ b/packages/lwc-engine/scripts/rollup/rollup.config.umd.prod.js @@ -5,11 +5,11 @@ const typescript = require('typescript'); const rollupTypescriptPlugin = require('rollup-plugin-typescript'); const nodeResolve = require('rollup-plugin-node-resolve'); const babelMinify = require('babel-minify'); -const { version } = require('../package.json'); +const { version } = require('../../package.json'); const { generateTargetName, ignoreCircularDependencies } = require('./engine.rollup.config.util'); -const entry = path.resolve(__dirname, '../src/framework/main.ts'); -const outputDir = path.resolve(__dirname, '../dist/umd'); +const entry = path.resolve(__dirname, '../../src/framework/main.ts'); +const outputDir = path.resolve(__dirname, '../../dist/umd'); const banner = (`/* proxy-compat-disable */`); const footer = `/** version: ${version} */`; diff --git a/packages/lwc-engine/src/framework/__tests__/api.spec.ts b/packages/lwc-engine/src/framework/__tests__/api.spec.ts index a0377fae3d..c120661c6c 100644 --- a/packages/lwc-engine/src/framework/__tests__/api.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/api.spec.ts @@ -106,22 +106,28 @@ describe('api', () => { expect(span.getAttribute('is')).toEqual('x-bar'); }); - it('should throw when forceTagName cannot have a shadow root attached to it', () => { + it('should throw if the forceTagName value is a reserved standard element name', () => { class Bar extends Element { - static forceTagName = 'div'; // it can't be a div + static forceTagName = 'div'; } + expect(() => { createElement('x-foo', { is: Bar }); - }).toThrow(); + }).toThrow( + /Invalid static forceTagName property set to "div"/ + ); }); - it('should throw when forceTagName cannot have a shadow root attached to it', () => { + it('should throw if the forceTagName is a custom element name', () => { class Bar extends Element { - static forceTagName = 'x-bar'; // it can't be a custom element name + static forceTagName = 'x-bar'; } + expect(() => { createElement('x-foo', { is: Bar }); - }).toThrow(); + }).toThrow( + /Invalid static forceTagName property set to "x-bar"/ + ); }); }); diff --git a/packages/lwc-engine/src/framework/__tests__/error-boundary.spec.ts b/packages/lwc-engine/src/framework/__tests__/error-boundary.spec.ts index 13010463b5..d57e314dae 100644 --- a/packages/lwc-engine/src/framework/__tests__/error-boundary.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/error-boundary.spec.ts @@ -76,7 +76,7 @@ describe('error boundary component', () => { return html; } } - const boundaryHostElm = createElement('x-boundary', {is: BoundryHost}); + const boundaryHostElm = createElement('x-parent', {is: BoundryHost}); document.body.appendChild(boundaryHostElm); expect(querySelectorAll.call(boundaryHostElm, 'x-boundary-sibling').length).toBe(1); @@ -198,7 +198,7 @@ describe('error boundary component', () => { return html; } } - const boundaryHostElm = createElement('x-boundary', {is: BoundryHost}); + const boundaryHostElm = createElement('x-parent', {is: BoundryHost}); document.body.appendChild(boundaryHostElm); expect(querySelectorAll.call(boundaryHostElm, 'x-boundary-sibling').length).toBe(1); @@ -434,7 +434,7 @@ describe('error boundary component', () => { return html; } } - const boundaryHostElm = createElement('x-boundary', {is: BoundryHost}); + const boundaryHostElm = createElement('x-parent', {is: BoundryHost}); document.body.appendChild(boundaryHostElm); expect(querySelectorAll.call(boundaryHostElm, 'x-boundary-sibling').length).toBe(1); @@ -530,7 +530,7 @@ describe('error boundary component', () => { return html; } } - const boundaryHostElm = createElement('x-boundary', {is: BoundryHost}); + const boundaryHostElm = createElement('x-parent', {is: BoundryHost}); document.body.appendChild(boundaryHostElm); expect(querySelectorAll.call(boundaryHostElm, 'x-boundary-sibling').length).toBe(1); diff --git a/packages/lwc-engine/src/framework/__tests__/events.spec.ts b/packages/lwc-engine/src/framework/__tests__/events.spec.ts index b7d64c8768..92c5c0f6a9 100644 --- a/packages/lwc-engine/src/framework/__tests__/events.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/events.spec.ts @@ -94,7 +94,7 @@ describe('Events on Custom Elements', () => { elm = createElement('x-foo', { is: Foo }); elm.addEventListener('click', clicked2); document.body.appendChild(elm); - cmp.root.querySelector('div').click(); + cmp.template.querySelector('div').click(); expect(result).toEqual([1, 2]); }); @@ -119,7 +119,7 @@ describe('Events on Custom Elements', () => { } elm = createElement('x-foo', { is: Foo }); document.body.appendChild(elm); - cmp.root.querySelector('div').click(); + cmp.template.querySelector('div').click(); expect(result).toEqual([1]); }); @@ -144,7 +144,7 @@ describe('Events on Custom Elements', () => { elm = createElement('x-foo', { is: Foo }); elm.addEventListener('click', clicked2); document.body.appendChild(elm); - cmp.root.querySelector('div').click(); + cmp.template.querySelector('div').click(); expect(result).toEqual([1]); }); @@ -167,7 +167,7 @@ describe('Events on Custom Elements', () => { } elm = createElement('x-foo', { is: Foo }); document.body.appendChild(elm); - cmp.root.querySelector('div').dispatchEvent(new CustomEvent('test', { bubbles: true })); // intentionally without composed: true to see if the root captures can that + cmp.template.querySelector('div').dispatchEvent(new CustomEvent('test', { bubbles: true })); // intentionally without composed: true to see if the root captures can that expect(result).toHaveLength(1); }); @@ -190,7 +190,7 @@ describe('Events on Custom Elements', () => { } elm = createElement('x-foo', { is: Foo }); document.body.appendChild(elm); - cmp.root.querySelector('div').click(); + cmp.template.querySelector('div').click(); expect(result).toHaveLength(2); expect(result[0]).toBe(undefined); // context must be the component expect(result[1]).toBeInstanceOf(Event); diff --git a/packages/lwc-engine/src/framework/__tests__/html-element.spec.ts b/packages/lwc-engine/src/framework/__tests__/html-element.spec.ts index 3d5e7525b3..b2c2bed1dd 100644 --- a/packages/lwc-engine/src/framework/__tests__/html-element.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/html-element.spec.ts @@ -11,23 +11,23 @@ import { querySelector } from "../dom/element"; describe('html-element', () => { describe('#setAttributeNS()', () => { it('should set attribute on host element when element is nested in template', () => { - class MyComponent extends Element { + class Child extends Element { setFoo() { this.setAttributeNS('x', 'foo', 'bar'); } } - MyComponent.publicMethods = ['setFoo']; + Child.publicMethods = ['setFoo']; class Parent extends Element { render() { return ($api) => { - return [$api.c('should-set-attribute-on-host-element-when-element-is-nested-in-template-child', MyComponent, {})] + return [$api.c('x-child', Child, {})] } } } const element = createElement('should-set-attribute-on-host-element-when-element-is-nested-in-template', { is: Parent }); document.body.appendChild(element); - const child = querySelector.call(element, 'should-set-attribute-on-host-element-when-element-is-nested-in-template-child'); + const child = querySelector.call(element, 'x-child'); child.setFoo(); expect(child.hasAttributeNS('x', 'foo')).toBe(true); expect(child.getAttributeNS('x', 'foo')).toBe('bar'); @@ -61,23 +61,23 @@ describe('html-element', () => { describe('#setAttribute()', () => { it('should set attribute on host element when element is nested in template', () => { - class MyComponent extends Element { + class Child extends Element { setFoo() { this.setAttribute('foo', 'bar'); } } - MyComponent.publicMethods = ['setFoo']; + Child.publicMethods = ['setFoo']; class Parent extends Element { render() { return ($api) => { - return [$api.c('should-set-attribute-on-host-element-when-element-is-nested-in-template-child', MyComponent, {})] + return [$api.c('x-child', Child, {})] } } } const element = createElement('should-set-attribute-on-host-element-when-element-is-nested-in-template', { is: Parent }); document.body.appendChild(element); - const child = querySelector.call(element, 'should-set-attribute-on-host-element-when-element-is-nested-in-template-child'); + const child = querySelector.call(element, 'x-child'); child.setFoo(); expect(child.hasAttribute('foo')).toBe(true); expect(child.getAttribute('foo')).toBe('bar'); @@ -111,17 +111,17 @@ describe('html-element', () => { describe('#removeAttributeNS()', () => { it('should remove namespaced attribute on host element when element is nested in template', () => { - class MyComponent extends Element { + class Child extends Element { removeTitle() { this.removeAttributeNS('x', 'title'); } } - MyComponent.publicMethods = ['removeTitle']; + Child.publicMethods = ['removeTitle']; class Parent extends Element { render() { return ($api) => { - return [$api.c('remove-namespaced-attribute-on-host-element-child', MyComponent, { + return [$api.c('x-child', Child, { attrs: { 'x:title': 'foo', } @@ -131,7 +131,7 @@ describe('html-element', () => { } const element = createElement('remove-namespaced-attribute-on-host-element', { is: Parent }); document.body.appendChild(element); - const child = querySelector.call(element, 'remove-namespaced-attribute-on-host-element-child'); + const child = querySelector.call(element, 'x-child'); child.removeTitle(); expect(child.hasAttributeNS('x', 'title')).toBe(false); }); @@ -152,17 +152,17 @@ describe('html-element', () => { describe('#removeAttribute()', () => { it('should remove attribute on host element when element is nested in template', () => { - class MyComponent extends Element { + class Child extends Element { removeTitle() { this.removeAttribute('title'); } } - MyComponent.publicMethods = ['removeTitle']; + Child.publicMethods = ['removeTitle']; class Parent extends Element { render() { return ($api) => { - return [$api.c('element-is-nested-in-template-child', MyComponent, { + return [$api.c('x-child', Child, { attrs: { title: 'foo', } @@ -172,7 +172,7 @@ describe('html-element', () => { } const element = createElement('element-is-nested-in-template', { is: Parent }); document.body.appendChild(element); - const child = querySelector.call(element, 'element-is-nested-in-template-child'); + const child = querySelector.call(element, 'x-child'); child.removeTitle(); expect(child.hasAttribute('title')).toBe(false); }); diff --git a/packages/lwc-engine/src/framework/__tests__/invoker.spec.ts b/packages/lwc-engine/src/framework/__tests__/invoker.spec.ts index 6d76987e17..6f7ce4a585 100644 --- a/packages/lwc-engine/src/framework/__tests__/invoker.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/invoker.spec.ts @@ -154,7 +154,7 @@ describe('invoker', () => { } } function html($api) { - return [$api.c('x-foo', Child, {})]; + return [$api.c('x-child', Child, {})]; } class MyComponent3 extends Element { renderedCallback() { diff --git a/packages/lwc-engine/src/framework/__tests__/upgrade.spec.ts b/packages/lwc-engine/src/framework/__tests__/upgrade.spec.ts index 114b11df9f..b641c1dda6 100644 --- a/packages/lwc-engine/src/framework/__tests__/upgrade.spec.ts +++ b/packages/lwc-engine/src/framework/__tests__/upgrade.spec.ts @@ -4,6 +4,15 @@ import { ComponentConstructor } from "../component"; describe('upgrade', () => { describe('#createElement()', () => { + it('should support constructors with circular dependencies', () => { + const factory = () => class extends Element { }; + factory.__circular__ = true; + + expect( + () => createElement('x-foo', { is: factory }) + ).not.toThrow(); + }); + it('should allow access to profixied default values for public props', () => { const x = [1, 2, 3], y = { foo: 1 }; type MyComponentElement = HTMLElement & { diff --git a/packages/lwc-engine/src/framework/api.ts b/packages/lwc-engine/src/framework/api.ts index a05b9ffc4f..d9964ddbf7 100644 --- a/packages/lwc-engine/src/framework/api.ts +++ b/packages/lwc-engine/src/framework/api.ts @@ -1,9 +1,8 @@ import assert from "./assert"; import { vmBeingRendered, invokeEventListener, EventListenerContext } from "./invoker"; -import { freeze, isArray, isUndefined, isNull, isFunction, isObject, isString, ArrayPush, assign, create, forEach, StringSlice, StringCharCodeAt, isNumber, hasOwnProperty, isTrue } from "./language"; -import { EmptyArray, SPACE_CHAR, ViewModelReflection } from "./utils"; +import { freeze, isArray, isUndefined, isNull, isFunction, isObject, isString, ArrayPush, assign, create, forEach, StringSlice, StringCharCodeAt, isNumber, isTrue } from "./language"; +import { EmptyArray, SPACE_CHAR, ViewModelReflection, resolveCircularModuleDependency } from "./utils"; import { renderVM, createVM, appendVM, removeVM, VM, getCustomElementVM, Slotset, allocateInSlot } from "./vm"; -import { registerComponent } from "./def"; import { ComponentConstructor } from "./component"; import { VNode, VNodeData, VNodes, VElement, VComment, VText, Hooks } from "../3rdparty/snabbdom/types"; import { patchShadowDomEvent, isValidEventForCustomElement } from "./events"; @@ -79,8 +78,8 @@ const hook: Hooks = { renderVM(vm); }, create(oldVNode: VNode, vnode: VNode) { - const { fallback, mode } = vnode.data; - createVM(vnode.sel as string, vnode.elm as HTMLElement, { + const { fallback, mode, ctor } = vnode.data; + createVM(vnode.sel as string, vnode.elm as HTMLElement, ctor, { mode, fallback, }); @@ -215,12 +214,7 @@ export function s(slotName: string, data: VNodeData, children: VNodes, slotset: // [c]ustom element node export function c(sel: string, Ctor: ComponentConstructor, data: VNodeData, children?: VNodes): VElement { - // The compiler produce AMD modules that do not support circular dependencies - // We need to create an indirection to circumvent those cases. - // We could potentially move this check to the definition - if (hasOwnProperty.call(Ctor, '__circular__')) { - Ctor = Ctor(); - } + Ctor = resolveCircularModuleDependency(Ctor); if (process.env.NODE_ENV !== 'production') { assert.isTrue(isString(sel), `c() 1st argument sel must be a string.`); @@ -256,9 +250,8 @@ export function c(sel: string, Ctor: ComponentConstructor, data: VNodeData, chil attrs = assign({}, attrs); attrs.is = sel; } - registerComponent(sel, Ctor); - data = { hook, key, slotset, attrs, on, props }; + data = { hook, key, slotset, attrs, on, props, ctor: Ctor }; data.class = classMap || getMapFromClassName(normalizeStyleString(className)); data.style = styleMap || normalizeStyleString(style); data.token = getCurrentTplToken(); diff --git a/packages/lwc-engine/src/framework/def.ts b/packages/lwc-engine/src/framework/def.ts index 5959d672e6..607ba02e99 100644 --- a/packages/lwc-engine/src/framework/def.ts +++ b/packages/lwc-engine/src/framework/def.ts @@ -25,7 +25,6 @@ import { ArraySlice, isNull, ArrayReduce, - hasOwnProperty, } from "./language"; import { GlobalAOMProperties, @@ -45,12 +44,16 @@ import wireDecorator from "./decorators/wire"; import trackDecorator from "./decorators/track"; import apiDecorator from "./decorators/api"; import { Element as BaseElement } from "./html-element"; -import { EmptyObject, getPropNameFromAttrName, assertValidForceTagName, ViewModelReflection, getAttrNameFromPropName } from "./utils"; +import { + EmptyObject, + getPropNameFromAttrName, + assertValidForceTagName, + ViewModelReflection, + getAttrNameFromPropName, + resolveCircularModuleDependency +} from "./utils"; import { OwnerKey, VM, VMElement, getCustomElementVM } from "./vm"; -declare interface HashTable { - [key: string]: T; -} export interface PropDef { config: number; type: string; // TODO: make this an enum @@ -106,14 +109,8 @@ const reducedDefaultHTMLPropertyNames: PropsDef = ArrayReduce.call(defaultDefHTM const HTML_PROPS: PropsDef = ArrayReduce.call(getOwnPropertyNames(GlobalAOMProperties), propertiesReducer, reducedDefaultHTMLPropertyNames); function getCtorProto(Ctor: any): any { - let proto = getPrototypeOf(Ctor); - // The compiler produce AMD modules that do not support circular dependencies - // We need to create an indirection to circumvent those cases. - // We could potentially move this check to the definition - if (hasOwnProperty.call(proto, '__circular__')) { - proto = proto(); - } - return proto; + const proto = getPrototypeOf(Ctor); + return resolveCircularModuleDependency(proto); } function isElementComponent(Ctor: any, protoSet?: any[]): boolean { @@ -133,10 +130,13 @@ function isElementComponent(Ctor: any, protoSet?: any[]): boolean { function createComponentDef(Ctor: ComponentConstructor): ComponentDef { if (process.env.NODE_ENV !== 'production') { assert.isTrue(isElementComponent(Ctor), `${Ctor} is not a valid component, or does not extends Element from "engine". You probably forgot to add the extend clause on the class declaration.`); + // local to dev block const ctorName = Ctor.name; assert.isTrue(ctorName && isString(ctorName), `${toString(Ctor)} should have a "name" property with string value, but found ${ctorName}.`); assert.isTrue(Ctor.constructor, `Missing ${ctorName}.constructor, ${ctorName} should have a "constructor" property.`); + + assertValidForceTagName(Ctor); } const name: string = Ctor.name; @@ -474,25 +474,3 @@ export function getComponentDef(Ctor: ComponentConstructor): ComponentDef { CtorToDefMap.set(Ctor, def); return def; } - -const TagNameToCtor: HashTable = create(null); - -export function getCtorByTagName(tagName: string): ComponentConstructor | undefined { - return TagNameToCtor[tagName]; - /////// TODO: what is this? -} - -export function registerComponent(tagName: string, Ctor: ComponentConstructor) { - if (process.env.NODE_ENV !== 'production') { - assertValidForceTagName(Ctor); - } - if (!isUndefined(TagNameToCtor[tagName])) { - if (TagNameToCtor[tagName] === Ctor) { - return; - } else if (process.env.NODE_ENV !== 'production') { - // TODO: eventually we should throw, this is only needed for the tests today - assert.logWarning(`Different component class cannot be registered to the same tagName="${tagName}".`); - } - } - TagNameToCtor[tagName] = Ctor; -} diff --git a/packages/lwc-engine/src/framework/modules/__tests__/events.spec.ts b/packages/lwc-engine/src/framework/modules/__tests__/events.spec.ts index f385fb5705..ca363565ea 100644 --- a/packages/lwc-engine/src/framework/modules/__tests__/events.spec.ts +++ b/packages/lwc-engine/src/framework/modules/__tests__/events.spec.ts @@ -26,7 +26,7 @@ describe('module/events', () => { } const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - cmp.root.querySelector('div').click(); + cmp.template.querySelector('div').click(); expect(result).toHaveLength(1); }); @@ -65,10 +65,10 @@ describe('module/events', () => { MyComponent.track = { counter: 1 }; const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - component.root.querySelector('div').click(); + component.template.querySelector('div').click(); component.counter += 1; return Promise.resolve().then( () => { - component.root.querySelector('div').click(); + component.template.querySelector('div').click(); expect(second).toBe(true); expect(result).toEqual([1, 2]); }); @@ -108,11 +108,11 @@ describe('module/events', () => { MyComponent.track = { counter: 1 }; const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - component.root.querySelector('p').click(); + component.template.querySelector('p').click(); component.counter += 1; return Promise.resolve().then( () => { expect(second).toBe(true); - component.root.querySelector('div').click(); + component.template.querySelector('div').click(); expect(result).toEqual([1, 1]); }); }); @@ -141,7 +141,7 @@ describe('module/events', () => { } const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - cmp.root.querySelector('div').click(); + cmp.template.querySelector('div').click(); expect(result).toHaveLength(2); expect(result[0]).toBe(cmp); expect(result[1]).toBeInstanceOf(Event); @@ -166,7 +166,7 @@ describe('module/events', () => { } const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - cmp.root.querySelector('x-child').click(); + cmp.template.querySelector('x-child').click(); expect(result).toHaveLength(1); }); @@ -189,7 +189,7 @@ describe('module/events', () => { } const elm = createElement('x-foo', { is: MyComponent }); document.body.appendChild(elm); - cmp.root.querySelector('x-child').dispatchEvent(new CustomEvent('test', {})); + cmp.template.querySelector('x-child').dispatchEvent(new CustomEvent('test', {})); expect(result).toHaveLength(1); }); diff --git a/packages/lwc-engine/src/framework/modules/__tests__/styles.spec.ts b/packages/lwc-engine/src/framework/modules/__tests__/styles.spec.ts index 2afd618ca8..92e6f13ff9 100644 --- a/packages/lwc-engine/src/framework/modules/__tests__/styles.spec.ts +++ b/packages/lwc-engine/src/framework/modules/__tests__/styles.spec.ts @@ -21,7 +21,7 @@ describe('modules/styles', () => { const elm = createElement('x-cmp', { is: Component }); document.body.appendChild(elm); - expect(cmp.root.querySelector('div').style.display).toBe('inline'); + expect(cmp.template.querySelector('div').style.display).toBe('inline'); }); it('should add style map to the element', () => { const tmpl = $api => [ @@ -41,7 +41,7 @@ describe('modules/styles', () => { const elm = createElement('x-cmp', { is: Component }); document.body.appendChild(elm); - expect(cmp.root.querySelector('div').style.display).toBe('inline'); + expect(cmp.template.querySelector('div').style.display).toBe('inline'); }); it('should patch style to the element', () => { const tmpl = ($api, $cmp) => [ @@ -63,10 +63,10 @@ describe('modules/styles', () => { }; const elm = createElement('x-cmp', { is: MyComponent }); document.body.appendChild(elm); - expect(cmp.root.querySelector('div').style.display).toBe('inline'); + expect(cmp.template.querySelector('div').style.display).toBe('inline'); cmp.counter++; return Promise.resolve().then(() => { - expect(cmp.root.querySelector('div').style.display).toBe('block'); + expect(cmp.template.querySelector('div').style.display).toBe('block'); }); }); it('should patch style map to the element', () => { @@ -89,11 +89,11 @@ describe('modules/styles', () => { }; const elm = createElement('x-cmp', { is: MyComponent }); document.body.appendChild(elm); - expect(cmp.root.querySelector('div').style.display).toBe('inline'); + expect(cmp.template.querySelector('div').style.display).toBe('inline'); cmp.counter++; return Promise.resolve().then(() => { - expect(cmp.root.querySelector('div').style.position).toBe('relative'); - expect(cmp.root.querySelector('div').style.display).toBe(''); + expect(cmp.template.querySelector('div').style.position).toBe('relative'); + expect(cmp.template.querySelector('div').style.display).toBe(''); }); }); }); diff --git a/packages/lwc-engine/src/framework/upgrade.ts b/packages/lwc-engine/src/framework/upgrade.ts index e2f55765b5..c19a9a4782 100644 --- a/packages/lwc-engine/src/framework/upgrade.ts +++ b/packages/lwc-engine/src/framework/upgrade.ts @@ -1,10 +1,9 @@ import assert from "./assert"; import { isUndefined, assign, hasOwnProperty, defineProperties, isNull, isObject, isTrue } from "./language"; import { createVM, removeVM, appendVM, renderVM, getCustomElementVM } from "./vm"; -import { registerComponent, getCtorByTagName } from "./def"; import { ComponentConstructor } from "./component"; import { EmptyNodeList } from "./dom/node"; -import { ViewModelReflection } from "./utils"; +import { ViewModelReflection, resolveCircularModuleDependency } from "./utils"; import { setAttribute } from "./dom/element"; import { shadowRootQuerySelector, shadowRootQuerySelectorAll } from "./dom/traverse"; @@ -91,17 +90,19 @@ export function createElement(sel: string, options: any = {}): HTMLElement { if (!isObject(options) || isNull(options)) { throw new TypeError(); } - const { is } = (options as any); + + const Ctor = resolveCircularModuleDependency((options as any).is); + let { mode, fallback } = (options as any); // TODO: for now, we default to open, but eventually it should default to 'closed' if (mode !== 'closed') { mode = 'open'; } // TODO: for now, we default to true, but eventually it should default to false if (fallback !== false) { fallback = true; } - registerComponent(sel, is); + // extracting the registered constructor just in case we need to force the tagName - const Ctor = getCtorByTagName(sel); const { forceTagName } = Ctor as ComponentConstructor; const tagName = isUndefined(forceTagName) ? sel : forceTagName; + // Create element with correct tagName const element = document.createElement(tagName); if (hasOwnProperty.call(element, ViewModelReflection)) { @@ -109,7 +110,7 @@ export function createElement(sel: string, options: any = {}): HTMLElement { } // In case the element is not initialized already, we need to carry on the manual creation - createVM(sel, element, { mode, fallback, isRoot: true }); + createVM(sel, element, Ctor, { mode, fallback, isRoot: true }); if (isTrue(fallback)) { // We don't support slots on root nodes defineProperties(element, rootNodeFallbackDescriptors); diff --git a/packages/lwc-engine/src/framework/utils.ts b/packages/lwc-engine/src/framework/utils.ts index b78067f3db..fc36a352dc 100644 --- a/packages/lwc-engine/src/framework/utils.ts +++ b/packages/lwc-engine/src/framework/utils.ts @@ -86,4 +86,19 @@ export function assertValidForceTagName(Ctor: ComponentConstructor) { } } +/** + * When LWC is used in the context of an Aura application, the compiler produces AMD + * modules, that doesn't resolve properly circular dependencies between modules. In order + * to circumvent this issue, the module loader returns a factory with a symbol attached + * to it. + * + * This method returns the resolved value if it received a factory as argument. Otherwise + * it returns the original value. + */ +export function resolveCircularModuleDependency(valueOrFactory: any): any { + return hasOwnProperty.call(valueOrFactory, '__circular__') ? + valueOrFactory() : + valueOrFactory; +} + export const usesNativeSymbols = typeof Symbol() === 'symbol'; diff --git a/packages/lwc-engine/src/framework/vm.ts b/packages/lwc-engine/src/framework/vm.ts index b6494a5944..104c2f5a7c 100644 --- a/packages/lwc-engine/src/framework/vm.ts +++ b/packages/lwc-engine/src/framework/vm.ts @@ -4,7 +4,6 @@ import { createComponent, linkComponent, renderComponent, clearReactiveListeners import { patchChildren } from "./patch"; import { ArrayPush, isUndefined, isNull, ArrayUnshift, ArraySlice, create, hasOwnProperty, isTrue, isFalse, isObject, keys } from "./language"; import { ViewModelReflection, addCallbackToNextTick, EmptyObject, EmptyArray, usesNativeSymbols } from "./utils"; -import { getCtorByTagName } from "./def"; import { invokeServiceHook, Services } from "./services"; import { invokeComponentCallback } from "./invoker"; import { parentNodeGetter, parentElementGetter } from "./dom/node"; @@ -170,14 +169,14 @@ export interface CreateOptions { isRoot?: boolean; } -export function createVM(tagName: string, elm: HTMLElement, options: CreateOptions) { +export function createVM(tagName: string, elm: HTMLElement, Ctor: ComponentConstructor, options: CreateOptions) { if (process.env.NODE_ENV !== 'production') { assert.invariant(elm instanceof HTMLElement, `VM creation requires a DOM element instead of ${elm}.`); } if (hasOwnProperty.call(elm, ViewModelReflection)) { return; // already created } - const Ctor = getCtorByTagName(tagName) as ComponentConstructor; + const def = getComponentDef(Ctor); const { isRoot, mode } = options; const fallback = isTrue(options.fallback) || isFalse(usesNativeShadowRoot); diff --git a/packages/lwc-module-resolver/jest.config.js b/packages/lwc-module-resolver/jest.config.js new file mode 100644 index 0000000000..a40cdc9a5c --- /dev/null +++ b/packages/lwc-module-resolver/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'lwc-module-resolver', +}; diff --git a/packages/lwc-template-compiler/jest.config.js b/packages/lwc-template-compiler/jest.config.js new file mode 100644 index 0000000000..7562666402 --- /dev/null +++ b/packages/lwc-template-compiler/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'lwc-template-compiler', +}; diff --git a/packages/lwc-wire-service/jest.config.js b/packages/lwc-wire-service/jest.config.js new file mode 100644 index 0000000000..79c7ac2a82 --- /dev/null +++ b/packages/lwc-wire-service/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'lwc-wire-service', +}; diff --git a/packages/observable-membrane/jest.config.js b/packages/observable-membrane/jest.config.js new file mode 100644 index 0000000000..0008c0a5e7 --- /dev/null +++ b/packages/observable-membrane/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'observable-membrane', +}; diff --git a/packages/postcss-plugin-lwc/jest.config.js b/packages/postcss-plugin-lwc/jest.config.js new file mode 100644 index 0000000000..0badadb726 --- /dev/null +++ b/packages/postcss-plugin-lwc/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'postcss-plugin-lwc', +}; diff --git a/packages/rollup-plugin-lwc-compiler/jest.config.js b/packages/rollup-plugin-lwc-compiler/jest.config.js new file mode 100644 index 0000000000..10528cf76b --- /dev/null +++ b/packages/rollup-plugin-lwc-compiler/jest.config.js @@ -0,0 +1,6 @@ +const BASE_CONFIG = require('../../scripts/jest/base.config'); + +module.exports = { + ...BASE_CONFIG, + displayName: 'rollup-plugin-lwc-compiler', +}; diff --git a/jest.config.js b/scripts/jest/base.config.js similarity index 69% rename from jest.config.js rename to scripts/jest/base.config.js index f7ae879312..b6195027a2 100644 --- a/jest.config.js +++ b/scripts/jest/base.config.js @@ -5,9 +5,6 @@ module.exports = { '.js': require.resolve('ts-jest/preprocessor.js') }, testMatch: [ - '/packages/*/**/__tests__/*.spec.(js|ts)' + '/*/**/__tests__/*.spec.(js|ts)' ], - projects: [ - '' - ] }; diff --git a/scripts/jest/root.config.js b/scripts/jest/root.config.js new file mode 100644 index 0000000000..1f40b544a0 --- /dev/null +++ b/scripts/jest/root.config.js @@ -0,0 +1,14 @@ +module.exports = { + rootDir: '../..', + projects: [ + '/packages/babel-plugin-transform-lwc-class', + '/packages/lwc-compiler', + '/packages/lwc-engine', + '/packages/lwc-module-resolver', + '/packages/lwc-template-compiler', + '/packages/lwc-wire-service', + '/packages/observable-membrane', + '/packages/postcss-plugin-lwc', + '/packages/rollup-plugin-lwc-compiler', + ], +};