diff --git a/packages/@lwc/engine-core/src/framework/rendering.ts b/packages/@lwc/engine-core/src/framework/rendering.ts index e7f11b9497..2a43d1bf8a 100644 --- a/packages/@lwc/engine-core/src/framework/rendering.ts +++ b/packages/@lwc/engine-core/src/framework/rendering.ts @@ -25,7 +25,7 @@ import { logError } from '../shared/logger'; import { getComponentTag } from '../shared/format'; import { EmptyArray, shouldBeFormAssociated } from './utils'; import { markComponentAsDirty } from './component'; -import { getScopeTokenClass } from './stylesheet'; +import { getScopeTokenClass, isValidScopeToken } from './stylesheet'; import { lockDomMutation, patchElementWithRestrictions, unlockDomMutation } from './restrictions'; import { appendVM, @@ -605,6 +605,10 @@ function applyStyleScoping(elm: Element, owner: VM, renderer: RendererAPI) { // Set the class name for `*.scoped.css` style scoping. const scopeToken = getScopeTokenClass(owner, /* legacy */ false); if (!isNull(scopeToken)) { + if (!isValidScopeToken(scopeToken)) { + // See W-16614556 + throw new Error('stylesheet token must be a valid string'); + } // TODO [#2762]: this dot notation with add is probably problematic // probably we should have a renderer api for just the add operation getClassList(elm).add(scopeToken); @@ -614,6 +618,10 @@ function applyStyleScoping(elm: Element, owner: VM, renderer: RendererAPI) { if (lwcRuntimeFlags.ENABLE_LEGACY_SCOPE_TOKENS) { const legacyScopeToken = getScopeTokenClass(owner, /* legacy */ true); if (!isNull(legacyScopeToken)) { + if (!isValidScopeToken(legacyScopeToken)) { + // See W-16614556 + throw new Error('stylesheet token must be a valid string'); + } // TODO [#2762]: this dot notation with add is probably problematic // probably we should have a renderer api for just the add operation getClassList(elm).add(legacyScopeToken); diff --git a/packages/@lwc/engine-core/src/framework/stylesheet.ts b/packages/@lwc/engine-core/src/framework/stylesheet.ts index 1f65b40591..5836dd8d56 100644 --- a/packages/@lwc/engine-core/src/framework/stylesheet.ts +++ b/packages/@lwc/engine-core/src/framework/stylesheet.ts @@ -9,6 +9,7 @@ import { ArrayPush, isArray, isNull, + isString, isTrue, isUndefined, KEY__NATIVE_ONLY_CSS, @@ -29,6 +30,8 @@ import type { Template } from './template'; import type { VM } from './vm'; import type { Stylesheet, Stylesheets } from '@lwc/shared'; +const VALID_SCOPE_TOKEN_REGEX = /^[a-zA-Z0-9\-_]+$/; + // These are only used for HMR in dev mode // The "pure" annotations are so that Rollup knows for sure it can remove these from prod mode let stylesheetsToCssContent: WeakMap> = /*@__PURE__@*/ new WeakMap(); @@ -394,3 +397,12 @@ export function unrenderStylesheet(stylesheet: Stylesheet) { cssContentToAbortControllers.delete(cssContent); } } + +export function isValidScopeToken(token: unknown) { + if (!isString(token)) { + return false; + } + + // See W-16614556 + return lwcRuntimeFlags.DISABLE_SCOPE_TOKEN_VALIDATION || VALID_SCOPE_TOKEN_REGEX.test(token); +} diff --git a/packages/@lwc/engine-core/src/framework/template.ts b/packages/@lwc/engine-core/src/framework/template.ts index ddb93728fb..eeec81ae8b 100644 --- a/packages/@lwc/engine-core/src/framework/template.ts +++ b/packages/@lwc/engine-core/src/framework/template.ts @@ -26,7 +26,12 @@ import api from './api'; import { RenderMode, resetComponentRoot, runWithBoundaryProtection, ShadowMode } from './vm'; import { assertNotProd, EmptyObject } from './utils'; import { defaultEmptyTemplate, isTemplateRegistered } from './secure-template'; -import { createStylesheet, getStylesheetsContent, updateStylesheetToken } from './stylesheet'; +import { + createStylesheet, + getStylesheetsContent, + isValidScopeToken, + updateStylesheetToken, +} from './stylesheet'; import { logOperationEnd, logOperationStart, OperationId } from './profiler'; import { getTemplateOrSwappedTemplate, setActiveVM } from './hot-swaps'; import { getMapFromClassName } from './modules/computed-class-attr'; @@ -66,14 +71,6 @@ export function setVMBeingRendered(vm: VM | null) { vmBeingRendered = vm; } -const VALID_SCOPE_TOKEN_REGEX = /^[a-zA-Z0-9\-_]+$/; - -// See W-16614556 -// TODO [#2826]: freeze the template object -function isValidScopeToken(token: any) { - return isString(token) && VALID_SCOPE_TOKEN_REGEX.test(token); -} - function validateSlots(vm: VM) { assertNotProd(); // this method should never leak to prod @@ -274,6 +271,7 @@ function buildParseFragmentFn( } // See W-16614556 + // TODO [#2826]: freeze the template object if ( (hasStyleToken && !isValidScopeToken(stylesheetToken)) || (hasLegacyToken && !isValidScopeToken(legacyStylesheetToken)) diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts b/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts index 41eeccc04b..1b2370aa8b 100755 --- a/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts +++ b/packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts @@ -132,8 +132,13 @@ function testFixtures(options?: RollupLwcOptions) { let result; let err; try { + config?.features?.forEach((flag) => { + lwcEngineServer.setFeatureFlagForTest(flag, true); + }); + const module: LightningElementConstructor = (await import(compiledFixturePath)) .default; + result = formatHTML( lwcEngineServer.renderComponent('fixture-test', module, config?.props ?? {}) ); @@ -144,6 +149,10 @@ function testFixtures(options?: RollupLwcOptions) { err = _err?.message || 'An empty error occurred?!'; } + config?.features?.forEach((flag) => { + lwcEngineServer.setFeatureFlagForTest(flag, false); + }); + return { 'expected.html': result, 'error.txt': err, diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/config.json new file mode 100644 index 0000000000..684891f21c --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/config.json @@ -0,0 +1,4 @@ +{ + "entry": "x/component", + "features": ["DISABLE_SCOPE_TOKEN_VALIDATION"] +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/error.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/expected.html new file mode 100644 index 0000000000..7b0314f9b7 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/expected.html @@ -0,0 +1,8 @@ + + +

+ je suis une pomme de terre +

+
\ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.html new file mode 100755 index 0000000000..eb1bb5d211 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.js b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.js new file mode 100755 index 0000000000..a3c0636c82 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; +import cmp from './component.html'; +cmp.stylesheetToken = 'stylesheet.token'; + +export default class HelloWorld extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.scoped.css b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.scoped.css new file mode 100644 index 0000000000..b76940b35d --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/component.scoped.css @@ -0,0 +1,3 @@ +p { + font-size: 2em; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/tmpl.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/tmpl.html new file mode 100755 index 0000000000..eb1bb5d211 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token-extended/modules/x/component/tmpl.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/config.json b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/config.json new file mode 100644 index 0000000000..171b328e90 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/config.json @@ -0,0 +1,3 @@ +{ + "entry": "x/component" +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/error.txt b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/error.txt new file mode 100644 index 0000000000..d537efdd06 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/error.txt @@ -0,0 +1 @@ +stylesheet token must be a valid string \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/expected.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.html new file mode 100755 index 0000000000..eb1bb5d211 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.js b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.js new file mode 100755 index 0000000000..a3c0636c82 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.js @@ -0,0 +1,7 @@ +import { LightningElement } from 'lwc'; +import cmp from './component.html'; +cmp.stylesheetToken = 'stylesheet.token'; + +export default class HelloWorld extends LightningElement { + static renderMode = 'light'; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.scoped.css b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.scoped.css new file mode 100644 index 0000000000..b76940b35d --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/component.scoped.css @@ -0,0 +1,3 @@ +p { + font-size: 2em; +} diff --git a/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/tmpl.html b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/tmpl.html new file mode 100755 index 0000000000..eb1bb5d211 --- /dev/null +++ b/packages/@lwc/engine-server/src/__tests__/fixtures/scope-token/modules/x/component/tmpl.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/packages/@lwc/features/src/index.ts b/packages/@lwc/features/src/index.ts index c3f454830c..6ced3ff8de 100644 --- a/packages/@lwc/features/src/index.ts +++ b/packages/@lwc/features/src/index.ts @@ -20,6 +20,7 @@ const features: FeatureFlagMap = { ENABLE_FORCE_SHADOW_MIGRATE_MODE: null, ENABLE_EXPERIMENTAL_SIGNALS: null, DISABLE_SYNTHETIC_SHADOW: null, + DISABLE_SCOPE_TOKEN_VALIDATION: null, LEGACY_LOCKER_ENABLED: null, }; diff --git a/packages/@lwc/features/src/types.ts b/packages/@lwc/features/src/types.ts index 61c58b985d..a700b4d9bf 100644 --- a/packages/@lwc/features/src/types.ts +++ b/packages/@lwc/features/src/types.ts @@ -76,6 +76,11 @@ export interface FeatureFlagMap { */ DISABLE_SYNTHETIC_SHADOW: FeatureFlagValue; + /** + * If true, the contents of stylesheet scope tokens are not validated. + */ + DISABLE_SCOPE_TOKEN_VALIDATION: FeatureFlagValue; + /** * If true, then lightning legacy locker is supported, otherwise lightning legacy locker will not function * properly. diff --git a/packages/@lwc/integration-karma/test/rendering/sanitize-stylesheet-token/index.spec.js b/packages/@lwc/integration-karma/test/rendering/sanitize-stylesheet-token/index.spec.js index a2e307d762..e3bfd3b55c 100644 --- a/packages/@lwc/integration-karma/test/rendering/sanitize-stylesheet-token/index.spec.js +++ b/packages/@lwc/integration-karma/test/rendering/sanitize-stylesheet-token/index.spec.js @@ -67,10 +67,11 @@ props.forEach((prop) => { if ( process.env.NATIVE_SHADOW && - process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION + process.env.DISABLE_STATIC_CONTENT_OPTIMIZATION && + Ctor !== Scoping ) { // If we're rendering in native shadow and the static content optimization is disabled, - // then there's no problem with non-string stylesheet tokens because they are only rendered + // then there's no problem with invalid stylesheet tokens because they are only rendered // as class attribute values using either `classList` or `setAttribute` (and this only applies // when `*.scoped.css` is being used). expect(elm.shadowRoot.children.length).toBe(1); diff --git a/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts b/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts index 20a3da7988..4f97be924a 100644 --- a/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts +++ b/packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts @@ -12,4 +12,6 @@ export const expectedFailures = new Set([ 'attribute-global-html/as-component-prop/without-@api/config.json', 'known-boolean-attributes/default-def-html-attributes/static-on-component/config.json', 'wire/errors/throws-when-colliding-prop-then-method/config.json', + 'scope-token/config.json', + 'scope-token-extended/config.json', ]); diff --git a/scripts/test-utils/test-fixture-dir.ts b/scripts/test-utils/test-fixture-dir.ts index 152bf7d12b..982d10754a 100644 --- a/scripts/test-utils/test-fixture-dir.ts +++ b/scripts/test-utils/test-fixture-dir.ts @@ -10,6 +10,7 @@ import path from 'node:path'; import { AssertionError } from 'node:assert'; import { test } from 'vitest'; import * as glob from 'glob'; + const { globSync } = glob; type TestFixtureOutput = { [filename: string]: unknown };