Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(serializer): replace LWC scope tokens (BREAKING CHANGE) #185

Merged
merged 5 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor Author

@nolanlawson nolanlawson Jun 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lwc/compiler v2.48.0 added the cssScopeTokens return value, which we need. We might as well bump every other LWC peer dep as well, since folks shouldn't be mixing-and-matching different versions of LWC packages.

"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