Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@netflix/x-element

Package Overview
Dependencies
Maintainers
10
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@netflix/x-element - npm Package Compare versions

Comparing version 1.0.0-rc.54 to 1.0.0-rc.55

demo/performance/index.css

148

demo/performance/index.js

@@ -5,67 +5,3 @@ import XElement from '../../x-element.js';

class DefaultPerformanceElement extends XElement {
static get properties() {
return {
base: {
type: Number,
initial: 3,
},
height: {
type: Number,
initial: 4,
},
hypotenuse: {
type: Number,
input: ['base', 'height'],
compute: Math.hypot,
},
valid: {
type: Boolean,
input: ['hypotenuse'],
compute: hypotenuse => !!hypotenuse,
reflect: true,
},
perfect: {
type: Boolean,
input: ['hypotenuse'],
compute: hypotenuse => Number.isInteger(hypotenuse),
reflect: true,
},
title: {
type: String,
internal: true,
input: ['valid', 'perfect'],
compute: (valid, perfect) => {
return !valid
? 'This is not a triangle.'
: perfect
? 'This is a perfect triangle.'
: 'This is a triangle.';
},
},
};
}
static template(html) {
return ({ base, height, hypotenuse, title }) => html`
<style>
:host {
display: block;
}
:host(:not([valid])) {
color: red;
}
:host([perfect]) {
color: green;
}
</style>
<code base="${base}" height="${height}" .title="${title}">
Math.hypot(${base}, ${height}) = ${hypotenuse}
<code>
`;
}
}
customElements.define('default-performance', DefaultPerformanceElement);
class LitHtmlPerformanceElement extends DefaultPerformanceElement {
class LitHtmlElement extends XElement {
// Use lit-html's template engine rather than the built-in x-element engine.

@@ -78,5 +14,4 @@ static get templateEngine() {

}
customElements.define('lit-html-performance', LitHtmlPerformanceElement);
class UhtmlPerformanceElement extends DefaultPerformanceElement {
class UhtmlElement extends XElement {
// Use µhtml's template engine rather than the built-in x-element engine.

@@ -87,2 +22,79 @@ static get templateEngine() {

}
customElements.define('uhtml-performance', UhtmlPerformanceElement);
/*
// Reference template string (since it's hard to read as an interpolated array).
// Note that there are no special characters here so strings == strings.raw.
<div id="p1" attr="${attr}"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="${id}" boolean ?hidden="${hidden}" .title="${title}">${content1} -- ${content2}</div></div></div></div>
*/
const getStrings = () => {
const strings = ['<div id="p1" attr="', '"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="', '" boolean ?hidden="', '" .title="', '">', ' -- ', '</div></div></div></div>'];
strings.raw = ['<div id="p1" attr="', '"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="', '" boolean ?hidden="', '" .title="', '">', ' -- ', '</div></div></div></div>'];
return strings;
};
const getValues = ({ attr, id, hidden, title, content1, content2 }) => {
const values = [attr, id, hidden, title, content1, content2];
return values;
};
const run = async (output, constructor) => {
output.textContent = '';
const { render, html } = constructor.templateEngine;
const injectCount = 100000;
const initialCount = 1000000;
const updateCount = 1000000;
// Test inject performance.
await new Promise(resolve => setTimeout(resolve, 0));
let injectSum = 0;
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const injectContainer = document.createElement('div');
for (let iii = 0; iii < injectCount; iii++) {
const strings = getStrings();
const values = getValues(injectProperties);
const t0 = performance.now();
render(injectContainer, html(strings, ...values));
const t1 = performance.now();
injectSum += t1 - t0;
}
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`;
// Test initial performance.
await new Promise(resolve => setTimeout(resolve, 0));
let initialSum = 0;
const initialStrings = getStrings();
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' };
const initialValues = getValues(initialProperties);
const initialContainer = document.createElement('div');
for (let iii = 0; iii < initialCount; iii++) {
const t0 = performance.now();
render(initialContainer, html(initialStrings, ...initialValues));
const t1 = performance.now();
initialSum += t1 - t0;
}
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`;
// Test update performance.
await new Promise(resolve => setTimeout(resolve, 0));
let updateSum = 0;
const updateStrings = getStrings();
const updateProperties = [
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' },
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' },
];
const updateContainer = document.createElement('div');
for (let iii = 0; iii < updateCount; iii++) {
const values = getValues(updateProperties[iii % 2]);
const t0 = performance.now();
render(updateContainer, html(updateStrings, ...values));
const t1 = performance.now();
updateSum += t1 - t0;
}
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`;
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`;
};
await run(document.getElementById('default'), XElement);
await run(document.getElementById('lit-html'), LitHtmlElement);
await run(document.getElementById('uhtml'), UhtmlElement);

@@ -5,3 +5,3 @@ {

"description": "Custom Element base class.",
"version": "1.0.0-rc.54",
"version": "1.0.0-rc.55",
"license": "SEE LICENSE IN LICENSE",

@@ -8,0 +8,0 @@ "repository": "https://github.com/Netflix/x-element",

@@ -68,42 +68,2 @@ import XElement from '../x-element.js';

it('does not recognize single-quoted attributes', () => {
const getTemplate = () => {
return html`<div id="target" ignore-me='${'foo'}'>Gotta double-quote those.</div>`;
};
const container = document.createElement('div');
document.body.append(container);
render(container, getTemplate());
assert(container.querySelector('#target').getAttribute('ignore-me') !== 'foo');
container.remove();
});
it('does not recognize single-quoted properties', () => {
const getTemplate = () => {
return html`<div id="target" .ignoreMe='${'foo'}'>Gotta double-quote those.</div>`;
};
const container = document.createElement('div');
document.body.append(container);
render(container, getTemplate());
assert(container.querySelector('#target').ignoreMe !== 'foo');
container.remove();
});
it('refuses to reuse a template', () => {
const templateResultReference = html`<div id="target"></div>`;
const container = document.createElement('div');
document.body.append(container);
render(container, templateResultReference);
assert(!!container.querySelector('#target'));
render(container, null);
assert(!container.querySelector('#target'));
let error;
try {
render(container, templateResultReference);
} catch (e) {
error = e;
}
assert(error?.message === 'Unexpected re-injection of template result.', error.message);
container.remove();
});
it('renders nullish templates', () => {

@@ -128,3 +88,3 @@ const getTemplate = () => {

const getTemplate = ({ content }) => {
return html`<div id="target">${content}</div>`;
return html`<div id="target">a b ${content}</div>`;
};

@@ -134,5 +94,5 @@ const container = document.createElement('div');

render(container, getTemplate({ content: 'Interpolated.' }));
assert(container.querySelector('#target').textContent === 'Interpolated.');
assert(container.querySelector('#target').textContent === 'a b Interpolated.');
render(container, getTemplate({ content: 'Updated.' }));
assert(container.querySelector('#target').textContent === 'Updated.');
assert(container.querySelector('#target').textContent === 'a b Updated.');
container.remove();

@@ -176,4 +136,4 @@ });

it('renders attributes', () => {
const getTemplate = ({ attr }) => {
return html`<div id="target" attr="${attr}"></div>`;
const getTemplate = ({ attr, content }) => {
return html`<div id="target" attr="${attr}" f="b">Something<span>${content}</span></div>`;
};

@@ -214,3 +174,8 @@ const container = document.createElement('div');

const getTemplate = ({ prop }) => {
return html`<div id="target" .prop="${prop}"></div>`;
return html`\
<div
id="target"
foo-bar
.prop="${prop}">
</div>`;
};

@@ -387,6 +352,3 @@ const container = document.createElement('div');

// TODO: trying to find escaped values in strings is possible via a regex, but
// making that performant is nuanced. I.e., it's easy to cause catastrophic
// backtracking.
it.todo('renders elements with "<" or ">" characters in attributes', () => {
it('renders elements with "<" or ">" characters in attributes', () => {
// Note the "/", "<", and ">" characters.

@@ -770,2 +732,82 @@ const getTemplate = ({ width, height }) => {

describe('rendering errors', () => {
describe('templating', () => {
it.skip('throws when attempting to interpolate within a style tag', () => {});
it.skip('throws when attempting to interpolate within a script tag', () => {});
it('throws for unquoted attributes', () => {
const templateResultReference = html`<div id="target" not-ok=${'foo'}>Gotta double-quote those.</div>`;
const container = document.createElement('div');
document.body.append(container);
let error;
try {
render(container, templateResultReference);
} catch (e) {
error = e;
}
assert(error?.message === `Found invalid template near " not-ok=".`, error.message);
container.remove();
});
it('throws for single-quoted attributes', () => {
const templateResultReference = html`<div id="target" not-ok='${'foo'}'>Gotta double-quote those.</div>`;
const container = document.createElement('div');
document.body.append(container);
let error;
try {
render(container, templateResultReference);
} catch (e) {
error = e;
}
assert(error?.message === `Found invalid template near " not-ok='".`, error.message);
container.remove();
});
it('throws for unquoted properties', () => {
const templateResultReference = html`<div id="target" .notOk=${'foo'}>Gotta double-quote those.</div>`;
const container = document.createElement('div');
document.body.append(container);
let error;
try {
render(container, templateResultReference);
} catch (e) {
error = e;
}
assert(error?.message === `Found invalid template near " .notOk=".`, error.message);
container.remove();
});
it('throws for single-quoted properties', () => {
const templateResultReference = html`<div id="target" .notOk='${'foo'}'>Gotta double-quote those.</div>`;
const container = document.createElement('div');
document.body.append(container);
let error;
try {
render(container, templateResultReference);
} catch (e) {
error = e;
}
assert(error?.message === `Found invalid template near " .notOk='".`, error.message);
container.remove();
});
it('throws for re-injection of template result', () => {
const templateResultReference = html`<div id="target"></div>`;
const container = document.createElement('div');
document.body.append(container);
render(container, templateResultReference);
assert(!!container.querySelector('#target'));
render(container, null);
assert(!container.querySelector('#target'));
let error;
try {
render(container, templateResultReference);
} catch (e) {
error = e;
}
assert(error?.message === 'Unexpected re-injection of template result.', error.message);
container.remove();
});
});
describe('ifDefined', () => {

@@ -772,0 +814,0 @@ it('throws if used on a "boolean-attribute"', () => {

@@ -1065,5 +1065,7 @@ /** Base element class for creating custom elements. */

static #updaters = new WeakMap();
static #ATTRIBUTE = /<[a-zA-Z0-9-]+[^>]* ([a-z][a-z-]*)="$/;
static #BOOLEAN_ATTRIBUTE = /<[a-zA-Z0-9-]+[^>]* \?([a-z][a-z-]*)="$/;
static #PROPERTY = /<[a-zA-Z0-9-]+[^>]* \.([a-z][a-zA-Z0-9_]*)="$/;
static #OPEN = /<[a-z][a-z0-9-]*(?=\s)/g;
static #STEP = /\s+[a-z][a-z0-9-]*(?=[\s>])|\s+[a-z][a-zA-Z0-9-]*="[^"]*"/y;
static #CLOSE = />/g;
static #ATTRIBUTE = /\s+(\??([a-z][a-zA-Z0-9-]*))="$/y;
static #PROPERTY = /\s+\.([a-z][a-zA-Z0-9_]*)="$/y;

@@ -1081,30 +1083,44 @@ #type = null;

if (!this.#analysis) {
let string = '';
for (const [key, value] of Object.entries(this.#strings)) {
string += value;
const attributeMatch = string.match(Template.#ATTRIBUTE);
const booleanAttributeMatch = !attributeMatch ? string.match(Template.#BOOLEAN_ATTRIBUTE) : null;
const propertyMatch = !attributeMatch && !booleanAttributeMatch ? string.match(Template.#PROPERTY) : null;
if (attributeMatch) {
// We found a match like this: html`<div title="${value}"></div>`.
const name = attributeMatch[1];
string = string.slice(0, -2 - name.length) + `x-element-attribute-$${key}="${name}`;
} else if (booleanAttributeMatch) {
// We found a match like this: html`<div ?hidden="${!!value}"></div>`.
const name = booleanAttributeMatch[1];
string = string.slice(0, -3 - name.length) + `x-element-boolean-attribute-$${key}="${name}`;
} else if (propertyMatch) {
// We found a match like this: html`<div .title="${value}"></div>`.
const name = propertyMatch[1];
string = string.slice(0, -3 - name.length) + `x-element-property-$${key}="${name}`;
} else if (Number(key) < this.#strings.length - 1) {
let html = '';
const state = { inside: false, index: 0 };
for (let iii = 0; iii < this.#strings.length; iii++) {
const string = this.#strings[iii];
html += string;
Template.#exhaustString(string, state);
if (state.inside) {
Template.#ATTRIBUTE.lastIndex = state.index;
const attributeMatch = Template.#ATTRIBUTE.exec(string);
if (attributeMatch) {
const name = attributeMatch[2];
if (attributeMatch[1].startsWith('?')) {
// We found a match like this: html`<div ?hidden="${!!value}"></div>`.
html = html.slice(0, -3 - name.length) + `x-element-boolean-attribute-$${iii}="${name}`;
} else {
// We found a match like this: html`<div title="${value}"></div>`.
html = html.slice(0, -2 - name.length) + `x-element-attribute-$${iii}="${name}`;
}
state.index = 1; // Accounts for an expected quote character next.
} else {
Template.#PROPERTY.lastIndex = state.index;
const propertyMatch = Template.#PROPERTY.exec(string);
if (propertyMatch) {
// We found a match like this: html`<div .title="${value}"></div>`.
const name = propertyMatch[1];
html = html.slice(0, -3 - name.length) + `x-element-property-$${iii}="${name}`;
state.index = 1; // Accounts for an expected quote character next.
} else {
throw new Error(`Found invalid template near "${string.slice(state.index)}".`);
}
}
} else {
// Assume it's a match like this: html`<div>${value}</div>`.
string += `<!--x-element-content-$${key}-->`;
html += `<!--x-element-content-$${iii}-->`;
state.index = 0; // No characters to account for. Reset to zero.
}
}
if (this.#type === 'svg') {
string = `<svg xmlns="http://www.w3.org/2000/svg">${string}</svg>`;
html = `<svg xmlns="http://www.w3.org/2000/svg">${html}</svg>`;
}
const element = document.createElement('template');
element.innerHTML = string;
element.innerHTML = html;
const blueprint = Template.#evaluate(element.content); // mutates element.

@@ -1212,2 +1228,30 @@ this.#analysis = { element, blueprint };

static #exhaustString(string, state) {
if (!state.inside) {
// We're outside the opening tag.
Template.#OPEN.lastIndex = state.index;
const openMatch = Template.#OPEN.exec(string);
if (openMatch) {
state.inside = true;
state.index = Template.#OPEN.lastIndex;
Template.#exhaustString(string, state);
}
} else {
// We're inside the opening tag.
Template.#STEP.lastIndex = state.index;
let stepMatch = Template.#STEP.exec(string);
while (stepMatch) {
state.index = Template.#STEP.lastIndex;
stepMatch = Template.#STEP.exec(string);
}
Template.#CLOSE.lastIndex = state.index;
const closeMatch = Template.#CLOSE.exec(string);
if (closeMatch) {
state.inside = false;
state.index = Template.#CLOSE.lastIndex;
Template.#exhaustString(string, state);
}
}
}
static #evaluate(node, path) {

@@ -1214,0 +1258,0 @@ path = path ?? [];

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc