Skip to content

Commit

Permalink
fix(serializer): replace LWC scope tokens (BREAKING CHANGE) (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
nolanlawson authored Jun 8, 2023
1 parent a73e0f4 commit 434a43c
Show file tree
Hide file tree
Showing 17 changed files with 399 additions and 113 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
"@lwc/compiler": "^2.45.5",
"@lwc/engine-dom": "^2.45.5",
"@lwc/engine-server": "^2.45.5",
"@lwc/synthetic-shadow": "^2.45.5",
"@lwc/wire-service": "^2.45.5",
"@lwc/compiler": "^2.48.0",
"@lwc/engine-dom": "^2.48.0",
"@lwc/engine-server": "^2.48.0",
"@lwc/synthetic-shadow": "^2.48.0",
"@lwc/wire-service": "^2.48.0",
"@types/jest": "^29.5.0",
"eslint": "^8.37.0",
"jest": "^29.5.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/@lwc/jest-preset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
"/src/**/*.js"
],
"peerDependencies": {
"@lwc/compiler": "*",
"@lwc/engine-dom": "*",
"@lwc/engine-server": ">=2",
"@lwc/synthetic-shadow": "*",
"@lwc/compiler": "^2.48.0",
"@lwc/engine-dom": "^2.48.0",
"@lwc/engine-server": "^2.48.0",
"@lwc/synthetic-shadow": "^2.48.0",
"jest": "^26 || ^27 || ^28 || ^29"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/jest-serializer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"lwc"
],
"dependencies": {
"@lwc/jest-shared": "11.8.0",
"pretty-format": "^29.5.0"
},
"peerDependencies": {
Expand Down
10 changes: 9 additions & 1 deletion packages/@lwc/jest-serializer/src/clean-element-attrs.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
const { isKnownScopeToken } = require('@lwc/jest-shared');
const GUID_ATTR_VALUE = '[shadow:guid]';
const FRAG_ID_ATTR_VALUE = `#${GUID_ATTR_VALUE}`;

Expand Down Expand Up @@ -51,6 +52,13 @@ function cleanElementAttributes(elm) {
ATTRS_TO_REMOVE.forEach((name) => {
elm.removeAttribute(name);
});

for (const { name, value } of [...elm.attributes]) {
if (isKnownScopeToken(name)) {
elm.removeAttribute(name);
elm.setAttribute('__lwc_scope_token__', value);
}
}
}

module.exports = cleanElementAttributes;
18 changes: 18 additions & 0 deletions packages/@lwc/jest-serializer/src/clean-element-classes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

const { isKnownScopeToken } = require('@lwc/jest-shared');

function cleanElementClasses(elm) {
for (const name of [...elm.classList]) {
if (isKnownScopeToken(name)) {
elm.classList.replace(name, '__lwc_scope_token__');
}
}
}

module.exports = cleanElementClasses;
16 changes: 16 additions & 0 deletions packages/@lwc/jest-serializer/src/clean-style-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

const { getKnownScopeTokens } = require('@lwc/jest-shared');

function cleanStyleElement(elm) {
// attributes in the HTML namespace are case-insensitive, so the regex must be case-insensitive
const regex = new RegExp(getKnownScopeTokens().join('|'), 'gi');
elm.textContent = elm.textContent.replace(regex, '__lwc_scope_token__');
}

module.exports = cleanStyleElement;
8 changes: 7 additions & 1 deletion packages/@lwc/jest-serializer/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand All @@ -8,6 +8,8 @@ const PrettyFormat = require('pretty-format');
const DOMElement = PrettyFormat.plugins.DOMElement;

const cleanElementAttributes = require('./clean-element-attrs');
const cleanElementClasses = require('./clean-element-classes');
const cleanStyleElement = require('./clean-style-element');

function test(obj) {
if (typeof obj !== 'object' || obj === null) {
Expand Down Expand Up @@ -52,6 +54,10 @@ function serialize(node, config, indentation, depth, refs, printer) {
const isElement = node.nodeType === 1;
if (isElement) {
cleanElementAttributes(node);
cleanElementClasses(node);
if (node.tagName === 'STYLE') {
cleanStyleElement(node);
}
}

const lightChildren = Array.prototype.slice.call(node.childNodes);
Expand Down
34 changes: 33 additions & 1 deletion packages/@lwc/jest-shared/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,39 @@ function isKnownScopedCssFile(filename) {
return knownScopedCssFiles.has(filename);
}

const knownScopeTokens = new Set();

/**
* Indicate that this string is a scope token (used by LWC for style scoping).
* @param str - scope token string
*/
function addKnownScopeToken(str) {
// attributes in the HTML namespace are case-insensitive, so we treat everything as lowercase
knownScopeTokens.add(str.toLowerCase());
}

/**
* Check if this string is a known scope token
* @param str - string to check
* @returns {boolean} - true if it's a known scope token
*/
function isKnownScopeToken(str) {
// attributes in the HTML namespace are case-insensitive, so we treat everything as lowercase
return knownScopeTokens.has(str.toLowerCase());
}

/**
* Get all known scope tokens
@returns {string[]} - list of known scope tokens
*/
function getKnownScopeTokens() {
return [...knownScopeTokens];
}

module.exports = {
addKnownScopedCssFile,
isKnownScopedCssFile
isKnownScopedCssFile,
addKnownScopeToken,
isKnownScopeToken,
getKnownScopeTokens,
};
5 changes: 3 additions & 2 deletions packages/@lwc/jest-transformer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
"@babel/plugin-transform-modules-commonjs": "^7.21.2",
"@babel/preset-typescript": "^7.21.0",
"@lwc/jest-shared": "11.8.0",
"babel-preset-jest": "^29.5.0"
"babel-preset-jest": "^29.5.0",
"magic-string": "^0.30.0"
},
"peerDependencies": {
"@lwc/compiler": "*",
"@lwc/compiler": "^2.48.0",
"jest": "^26 || ^27 || ^28 || ^29"
},
"engines": {
Expand Down
34 changes: 32 additions & 2 deletions packages/@lwc/jest-transformer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const crypto = require('crypto');

const { isKnownScopedCssFile } = require('@lwc/jest-shared');

const MagicString = require('magic-string');
const babelCore = require('@babel/core');
const lwcCompiler = require('@lwc/compiler');
const jestPreset = require('babel-preset-jest');
Expand Down Expand Up @@ -93,7 +94,7 @@ module.exports = {
}

// Set default module name and namespace value for the namespace because it can't be properly guessed from the path
const { code, map } = lwcCompiler.transformSync(src, filePath, {
const { code, map, cssScopeTokens } = lwcCompiler.transformSync(src, filePath, {
name: 'test',
namespace: 'x',
outputConfig: {
Expand All @@ -112,7 +113,36 @@ module.exports = {
// **Note: .html and .css don't return valid sourcemaps cause they are used for rollup
const config = map && map.version ? { inputSourceMap: map } : {};

return babelCore.transform(code, { ...BABEL_CONFIG, ...config, filename });
let result = babelCore.transform(code, { ...BABEL_CONFIG, ...config, filename });

if (cssScopeTokens) {
// Modify the code so that it calls into @lwc/jest-shared and adds the scope token as a
// known scope token so we can replace it later.
// Note we have to modify the code rather than use @lwc/jest-shared directly because
// the transformer does not run in the same Node process as the serializer.
const magicString = new MagicString(result.code);

magicString.append(`\nconst { addKnownScopeToken } = require('@lwc/jest-shared');`);

for (const scopeToken of cssScopeTokens) {
magicString.append(`\naddKnownScopeToken(${JSON.stringify(scopeToken)});`)
}

const map = magicString.generateMap({
source: filePath,
includeContent: true,
});

const modifiedCode = magicString.toString() + `\n//# sourceMappingURL=${map.toUrl()}\n`;

result = {
...result,
code: modifiedCode,
map,
};
}

return result
},

getCacheKey(sourceText, sourcePath, ...rest) {
Expand Down
18 changes: 9 additions & 9 deletions test/src/modules/serializer/styled/__tests__/styled.spec.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { createElement } from 'lwc';
import Styled from '../styled';

it('serializes component with HTML - styled in native shadow', () => {
it('serializes component with HTML - styled in shadow DOM', () => {
const elm = createElement('serializer-component', { is: Styled });
document.body.appendChild(elm);

if (global['lwc-jest'].nativeShadow) {
expect(elm).toMatchInlineSnapshot(`
<serializer-component
class="x-test_styled-host"
class="__lwc_scope_token__"
>
#shadow-root(open)
<style
Expand All @@ -25,10 +25,10 @@ it('serializes component with HTML - styled in native shadow', () => {
<style
type="text/css"
>
h1.x-test_styled {background: blue;}
h1.__lwc_scope_token__ {background: blue;}
</style>
<h1
class="x-test_styled"
class="__lwc_scope_token__"
>
I am an LWC component
</h1>
Expand All @@ -37,13 +37,13 @@ it('serializes component with HTML - styled in native shadow', () => {
} else {
expect(elm).toMatchInlineSnapshot(`
<serializer-component
class="x-test_styled-host"
x-test_styled-host=""
__lwc_scope_token__=""
class="__lwc_scope_token__"
>
#shadow-root(open)
<h1
class="x-test_styled"
x-test_styled=""
__lwc_scope_token__=""
class="__lwc_scope_token__"
>
I am an LWC component
</h1>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { createElement } from 'lwc';
import StyledMultiple from '../styledMultiple';

it('serializes component with HTML - styled in shadow DOM - multiple attrs/classes/styles', () => {
const elm = createElement('serializer-component', { is: StyledMultiple });
document.body.appendChild(elm);

if (global['lwc-jest'].nativeShadow) {
expect(elm).toMatchInlineSnapshot(`
<serializer-component
class="__lwc_scope_token__"
>
#shadow-root(open)
<style
type="text/css"
>
:host {opacity: 0.5;}h1 {color: red;}.foo {background: azure;}
</style>
<style
type="text/css"
>
:host {color: goldenrod;}h1.__lwc_scope_token__ {background: blue;}.foo.__lwc_scope_token__ {opacity: 0.7;}
</style>
<h1
class="foo bar __lwc_scope_token__"
data-bar="bar"
data-foo="foo"
>
I am an LWC component with multiple classes, attributes, and styles
</h1>
</serializer-component>
`);
} else {
expect(elm).toMatchInlineSnapshot(`
<serializer-component
__lwc_scope_token__=""
class="__lwc_scope_token__"
>
#shadow-root(open)
<h1
__lwc_scope_token__=""
class="foo bar __lwc_scope_token__"
data-bar="bar"
data-foo="foo"
>
I am an LWC component with multiple classes, attributes, and styles
</h1>
</serializer-component>
`);
}
});
11 changes: 11 additions & 0 deletions test/src/modules/serializer/styledMultiple/styledMultiple.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:host {
opacity: 0.5;
}

h1 {
color: red;
}

.foo {
background: azure;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<h1
class="foo bar"
data-foo="foo"
data-bar="bar"
>I am an LWC component with multiple classes, attributes, and styles</h1>
</template>
11 changes: 11 additions & 0 deletions test/src/modules/serializer/styledMultiple/styledMultiple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { LightningElement } from 'lwc';

export default class extends LightningElement {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:host {
color: goldenrod;
}

h1 {
background: blue;
}

.foo {
opacity: 0.7;
}
Loading

0 comments on commit 434a43c

Please sign in to comment.