diff --git a/.gitignore b/.gitignore index 2bb8dea3c8..897d95d676 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ coverage/ .project/ .nx .nx-cache +.eslintcache lib/ __benchmarks_results__/ diff --git a/package.json b/package.json index 2147bd0d9d..a05a372f86 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "scripts": { "prepare": "husky && yarn build", - "lint": "eslint .", + "lint": "eslint . --cache", "format": "prettier --write .", "bundlesize": "node scripts/bundlesize/bundlesize.mjs", "build": "nx run-many --target=build --exclude=@lwc/perf-benchmarks,@lwc/perf-benchmarks-components,@lwc/integration-tests,lwc", @@ -74,11 +74,11 @@ "vitest": "^2.1.8" }, "lint-staged": { - "*.{js,mjs,ts}": "eslint", + "*.{js,mjs,ts}": "eslint --cache", "*.{css,js,json,md,mjs,ts,yaml,yml}": "prettier --write", "{packages/**/package.json,scripts/tasks/check-and-rewrite-package-json.js}": "node ./scripts/tasks/check-and-rewrite-package-json.js", "{LICENSE-CORE.md,**/LICENSE.md,yarn.lock,scripts/tasks/generate-license-files.js,scripts/shared/bundled-dependencies.js}": "node ./scripts/tasks/generate-license-files.js", - "*.{only,skip}": "eslint --no-eslintrc --plugin '@lwc/eslint-plugin-lwc-internal' --rule '@lwc/lwc-internal/forbidden-filename: error'" + "*.{only,skip}": "eslint --cache --no-eslintrc --plugin '@lwc/eslint-plugin-lwc-internal' --rule '@lwc/lwc-internal/forbidden-filename: error'" }, "workspaces": [ "packages/@lwc/*", diff --git a/packages/@lwc/engine-core/src/framework/hydration.ts b/packages/@lwc/engine-core/src/framework/hydration.ts index e04f1d9f73..1a924a3071 100644 --- a/packages/@lwc/engine-core/src/framework/hydration.ts +++ b/packages/@lwc/engine-core/src/framework/hydration.ts @@ -63,6 +63,11 @@ import type { import type { VM } from './vm'; import type { RendererAPI } from './renderer'; +type Classes = Omit, 'add'>; + +// Used as a perf optimization to avoid creating and discarding sets unnecessarily. +const EMPTY_SET: Classes = new Set(); + // These values are the ones from Node.nodeType (https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType) const enum EnvNodeTypes { ELEMENT = 1, @@ -618,6 +623,23 @@ function validateAttrs( return nodesAreCompatible; } +function checkClassesCompatibility(first: Classes, second: Classes): boolean { + if (first.size !== second.size) { + return false; + } + for (const f of first) { + if (!second.has(f)) { + return false; + } + } + for (const s of second) { + if (!first.has(s)) { + return false; + } + } + return true; +} + function validateClassAttr( vnode: VBaseElement | VStatic, elm: Element, @@ -634,16 +656,18 @@ function validateClassAttr( // Use a Set because we don't care to validate mismatches for 1) different ordering in SSR vs CSR, or 2) // duplicated class names. These don't have an effect on rendered styles. - const elmClasses = new Set(ArrayFrom(elm.classList)); - let vnodeClasses: Set; + const elmClasses = elm.classList.length ? new Set(ArrayFrom(elm.classList)) : EMPTY_SET; + let vnodeClasses: Classes; if (!isUndefined(className)) { // ignore empty spaces entirely, filter them out using `filter(..., Boolean)` - vnodeClasses = new Set(ArrayFilter.call(StringSplit.call(className, /\s+/), Boolean)); + const classes = ArrayFilter.call(StringSplit.call(className, /\s+/), Boolean); + vnodeClasses = classes.length ? new Set(classes) : EMPTY_SET; } else if (!isUndefined(classMap)) { - vnodeClasses = new Set(keys(classMap)); + const classes = keys(classMap); + vnodeClasses = classes.length ? new Set(classes) : EMPTY_SET; } else { - vnodeClasses = new Set(); + vnodeClasses = EMPTY_SET; } // ---------- Step 2: handle the scope tokens @@ -658,7 +682,11 @@ function validateClassAttr( // Consequently, hydration mismatches will occur if scoped CSS token classnames // are rendered during SSR. This needs to be accounted for when validating. if (!isNull(scopeToken)) { - vnodeClasses.add(scopeToken); + if (vnodeClasses === EMPTY_SET) { + vnodeClasses = new Set([scopeToken]); + } else { + (vnodeClasses as Set).add(scopeToken); + } } // This tells us which `*-host` scope token was rendered to the element's class. @@ -672,25 +700,10 @@ function validateClassAttr( // ---------- Step 3: check for compatibility - let nodesAreCompatible = true; + const classesAreCompatible = checkClassesCompatibility(vnodeClasses, elmClasses); - if (vnodeClasses.size !== elmClasses.size) { - nodesAreCompatible = false; - } else { - for (const vnodeClass of vnodeClasses) { - if (!elmClasses.has(vnodeClass)) { - nodesAreCompatible = false; - } - } - for (const elmClass of elmClasses) { - if (!vnodeClasses.has(elmClass)) { - nodesAreCompatible = false; - } - } - } - - if (process.env.NODE_ENV !== 'production' && !nodesAreCompatible) { - const prettyPrint = (set: Set) => + if (process.env.NODE_ENV !== 'production' && !classesAreCompatible) { + const prettyPrint = (set: Classes) => JSON.stringify(ArrayJoin.call(ArraySort.call(ArrayFrom(set)), ' ')); logWarn( `Mismatch hydrating element <${getProperty( @@ -703,7 +716,7 @@ function validateClassAttr( ); } - return nodesAreCompatible; + return classesAreCompatible; } function validateStyleAttr( diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/context-slotted/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/error.txt similarity index 100% rename from packages/@lwc/engine-server/src/__tests__/fixtures/context-slotted/error.txt rename to packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/error.txt diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/expected.html new file mode 100644 index 0000000000..79ceaa13f4 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/expected.html @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/index.js b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/index.js new file mode 100644 index 0000000000..4bd94488e9 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/index.js @@ -0,0 +1,3 @@ +export const tagName = 'x-comments-text'; +export { default } from 'x/comments-text'; +export * from 'x/comments-text'; diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/modules/x/comments-text/comments-text.html b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/modules/x/comments-text/comments-text.html new file mode 100644 index 0000000000..1138d4e911 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/modules/x/comments-text/comments-text.html @@ -0,0 +1,6 @@ + diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/modules/x/comments-text/comments-text.js b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/modules/x/comments-text/comments-text.js new file mode 100644 index 0000000000..4f3a42e964 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/adjacent-text-nodes/preserve-comments-on/empty copy/modules/x/comments-text/comments-text.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class extends LightningElement { + isTrue = true; + isFalse = false; + foo = ''; + bar = undefined; + baz = null; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-aria/dynamic/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-aria/dynamic/expected.html index e5f6090a1b..6baed8c3be 100644 --- a/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-aria/dynamic/expected.html +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/attribute-aria/dynamic/expected.html @@ -14,7 +14,7 @@