Skip to content

Commit d571bbb

Browse files
authoredJul 16, 2024··
fix(compiler): no need for commenting selectors anymore (#5892)
* fix(compiler): no need for commenting selectors anymore * prettier * breaking: don't render shadow components when serializeShadowRoot is false * update tests * fix setting style tag * prettier * build fix * remove comment * make default true * prettier
1 parent b571354 commit d571bbb

29 files changed

+239
-333
lines changed
 

‎src/compiler/bundle/bundle-output.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const getRollupOptions = (
121121
userIndexPlugin(config, compilerCtx),
122122
typescriptPlugin(compilerCtx, bundleOpts, config),
123123
extFormatPlugin(config),
124-
extTransformsPlugin(config, compilerCtx, buildCtx, bundleOpts),
124+
extTransformsPlugin(config, compilerCtx, buildCtx),
125125
workerPlugin(config, compilerCtx, buildCtx, bundleOpts.platform, !!bundleOpts.inlineWorkers),
126126
serverPlugin(config, bundleOpts.platform),
127127
...beforePlugins,

‎src/compiler/bundle/ext-transforms-plugin.ts

+1-22
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type * as d from '../../declarations';
55
import { runPluginTransformsEsmImports } from '../plugin/plugin';
66
import { getScopeId } from '../style/scope-css';
77
import { parseImportPath } from '../transformers/stencil-import-path';
8-
import type { BundleOptions } from './bundle-interface';
98

109
/**
1110
* This keeps a map of all the component styles we've seen already so we can create
@@ -33,14 +32,12 @@ const allCmpStyles = new Map<string, ComponentStyleMap>();
3332
* @param config a user-supplied configuration
3433
* @param compilerCtx the current compiler context
3534
* @param buildCtx the current build context
36-
* @param bundleOpts bundle options for Rollup
3735
* @returns a Rollup plugin which carries out the necessary work
3836
*/
3937
export const extTransformsPlugin = (
4038
config: d.ValidatedConfig,
4139
compilerCtx: d.CompilerCtx,
4240
buildCtx: d.BuildCtx,
43-
bundleOpts: BundleOptions,
4441
): Plugin => {
4542
return {
4643
name: 'extTransformsPlugin',
@@ -94,24 +91,6 @@ export const extTransformsPlugin = (
9491

9592
const pluginTransforms = await runPluginTransformsEsmImports(config, compilerCtx, buildCtx, code, filePath);
9693

97-
// We need to check whether the current build is a dev-mode watch build w/ HMR enabled in
98-
// order to know how we'll want to set `commentOriginalSelector` (below). If we are doing
99-
// a hydrate build we need to set this to `true` because commenting-out selectors is what
100-
// gives us support for scoped CSS w/ hydrated components (we don't support shadow DOM and
101-
// styling via that route for them). However, we don't want to comment selectors in dev
102-
// mode when using HMR in the browser, since there we _do_ support putting stylesheets into
103-
// the shadow DOM and commenting out e.g. the `:host` selector in those stylesheets will
104-
// break components' CSS when an HMR update is sent to the browser.
105-
//
106-
// See https://github.com/ionic-team/stencil/issues/3461 for details
107-
const isDevWatchHMRBuild =
108-
config.flags.watch &&
109-
config.flags.dev &&
110-
config.flags.serve &&
111-
(config.devServer?.reloadStrategy ?? null) === 'hmr';
112-
const commentOriginalSelector =
113-
bundleOpts.platform === 'hydrate' && data.encapsulation === 'shadow' && !isDevWatchHMRBuild;
114-
11594
if (data.tag) {
11695
cmp = buildCtx.components.find((c) => c.tagName === data.tag);
11796
const moduleFile = cmp && compilerCtx.moduleMap.get(cmp.sourceFilePath);
@@ -147,7 +126,6 @@ export const extTransformsPlugin = (
147126
tag: data.tag,
148127
encapsulation: data.encapsulation,
149128
mode: data.mode,
150-
commentOriginalSelector,
151129
sourceMap: config.sourceMap,
152130
minify: config.minifyCss,
153131
autoprefixer: config.autoprefixCss,
@@ -214,6 +192,7 @@ export const extTransformsPlugin = (
214192
* as it is not connected to a component.
215193
*/
216194
cssTransformResults.styleText;
195+
217196
buildCtx.stylesUpdated.push({
218197
styleTag: data.tag,
219198
styleMode: data.mode,

‎src/compiler/bundle/test/ext-transforms-plugin.spec.ts

+1-37
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('extTransformsPlugin', () => {
6363

6464
const writeFileSpy = jest.spyOn(compilerCtx.fs, 'writeFile');
6565
return {
66-
plugin: extTransformsPlugin(config, compilerCtx, buildCtx, bundleOpts),
66+
plugin: extTransformsPlugin(config, compilerCtx, buildCtx),
6767
config,
6868
compilerCtx,
6969
buildCtx,
@@ -101,41 +101,5 @@ describe('extTransformsPlugin', () => {
101101

102102
expect(css).toBe(':host { text: pink; }');
103103
});
104-
105-
describe('passing `commentOriginalSelector` to `transformCssToEsm`', () => {
106-
it.each([
107-
[false, 'tag=my-component&encapsulation=scoped'],
108-
[true, 'tag=my-component&encapsulation=shadow'],
109-
[false, 'tag=my-component'],
110-
])('should pass true if %p and hydrate', async (expectation, queryParams) => {
111-
const { plugin, transformCssToEsmSpy } = setup({ platform: 'hydrate' });
112-
// @ts-ignore the Rollup plugins expect to be called in a Rollup context
113-
await plugin.transform('asdf', `/some/stubbed/path/foo.css?${queryParams}`);
114-
expect(transformCssToEsmSpy.mock.calls[0][0].commentOriginalSelector).toBe(expectation);
115-
});
116-
117-
it('should pass false if shadow, hydrate, but using HMR in dev watch mode', async () => {
118-
const { plugin, transformCssToEsmSpy, config } = setup({ platform: 'hydrate' });
119-
120-
config.flags.watch = true;
121-
config.flags.dev = true;
122-
config.flags.serve = true;
123-
config.devServer = { reloadStrategy: 'hmr' };
124-
125-
// @ts-ignore the Rollup plugins expect to be called in a Rollup context
126-
await plugin.transform('asdf', '/some/stubbed/path/foo.css?tag=my-component&encapsulation=shadow');
127-
expect(transformCssToEsmSpy.mock.calls[0][0].commentOriginalSelector).toBe(false);
128-
});
129-
130-
it.each(['tag=my-component&encapsulation=scoped', 'tag=my-component&encapsulation=shadow', 'tag=my-component'])(
131-
'should pass false if %p without hydrate',
132-
async (queryParams) => {
133-
const { plugin, transformCssToEsmSpy } = setup();
134-
// @ts-ignore the Rollup plugins expect to be called in a Rollup context
135-
await plugin.transform('asdf', `/some/stubbed/path/foo.css?${queryParams}`);
136-
expect(transformCssToEsmSpy.mock.calls[0][0].commentOriginalSelector).toBe(false);
137-
},
138-
);
139-
});
140104
});
141105
});

‎src/compiler/config/transpile-options.ts

-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ export const getTranspileCssConfig = (
166166
encapsulation: importData && importData.encapsulation,
167167
mode: importData && importData.mode,
168168
sourceMap: compileOpts.sourceMap !== false,
169-
commentOriginalSelector: false,
170169
minify: false,
171170
autoprefixer: false,
172171
module: compileOpts.module,

‎src/compiler/style/css-to-esm.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,9 @@ const transformCssToEsmModule = (input: d.TransformCssToEsmInput): d.TransformCs
113113
try {
114114
const varNames = new Set([results.defaultVarName]);
115115

116-
if (isString(input.tag)) {
117-
if (input.encapsulation === 'scoped' || (input.encapsulation === 'shadow' && input.commentOriginalSelector)) {
118-
const scopeId = getScopeId(input.tag, input.mode);
119-
results.styleText = scopeCss(results.styleText, scopeId, !!input.commentOriginalSelector);
120-
}
116+
if (isString(input.tag) && input.encapsulation === 'scoped') {
117+
const scopeId = getScopeId(input.tag, input.mode);
118+
results.styleText = scopeCss(results.styleText, scopeId);
121119
}
122120

123121
const cssImports = getCssToEsmImports(varNames, results.styleText, input.file, input.mode);

‎src/compiler/transformers/add-static-style.ts

+12-18
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ import { createStaticGetter, getExternalStyles } from './transform-utils';
1616
* @param classMembers a class to existing members of a class. **this parameter will be mutated** rather than returning
1717
* a cloned version
1818
* @param cmp the metadata associated with the component being evaluated
19-
* @param commentOriginalSelector if `true`, add a comment with the original CSS selector to the style.
2019
*/
2120
export const addStaticStyleGetterWithinClass = (
2221
classMembers: ts.ClassElement[],
2322
cmp: d.ComponentCompilerMeta,
24-
commentOriginalSelector: boolean,
2523
): void => {
26-
const styleLiteral = getStyleLiteral(cmp, commentOriginalSelector);
24+
const styleLiteral = getStyleLiteral(cmp);
2725
if (styleLiteral) {
2826
classMembers.push(createStaticGetter('style', styleLiteral));
2927
}
@@ -41,7 +39,7 @@ export const addStaticStyleGetterWithinClass = (
4139
* @param cmp the metadata associated with the component being evaluated
4240
*/
4341
export const addStaticStylePropertyToClass = (styleStatements: ts.Statement[], cmp: d.ComponentCompilerMeta): void => {
44-
const styleLiteral = getStyleLiteral(cmp, false);
42+
const styleLiteral = getStyleLiteral(cmp);
4543
if (styleLiteral) {
4644
const statement = ts.factory.createExpressionStatement(
4745
ts.factory.createAssignment(
@@ -53,24 +51,20 @@ export const addStaticStylePropertyToClass = (styleStatements: ts.Statement[], c
5351
}
5452
};
5553

56-
const getStyleLiteral = (cmp: d.ComponentCompilerMeta, commentOriginalSelector: boolean) => {
54+
const getStyleLiteral = (cmp: d.ComponentCompilerMeta) => {
5755
if (Array.isArray(cmp.styles) && cmp.styles.length > 0) {
5856
if (cmp.styles.length > 1 || (cmp.styles.length === 1 && cmp.styles[0].modeName !== DEFAULT_STYLE_MODE)) {
5957
// multiple style modes
60-
return getMultipleModeStyle(cmp, cmp.styles, commentOriginalSelector);
58+
return getMultipleModeStyle(cmp, cmp.styles);
6159
} else {
6260
// single style
63-
return getSingleStyle(cmp, cmp.styles[0], commentOriginalSelector);
61+
return getSingleStyle(cmp, cmp.styles[0]);
6462
}
6563
}
6664
return null;
6765
};
6866

69-
const getMultipleModeStyle = (
70-
cmp: d.ComponentCompilerMeta,
71-
styles: d.StyleCompiler[],
72-
commentOriginalSelector: boolean,
73-
) => {
67+
const getMultipleModeStyle = (cmp: d.ComponentCompilerMeta, styles: d.StyleCompiler[]) => {
7468
const styleModes: ts.ObjectLiteralElementLike[] = [];
7569

7670
styles.forEach((style) => {
@@ -83,7 +77,7 @@ const getMultipleModeStyle = (
8377
if (typeof style.styleStr === 'string') {
8478
// inline the style string
8579
// static get style() { return { ios: "string" }; }
86-
const styleLiteral = createStyleLiteral(cmp, style, commentOriginalSelector);
80+
const styleLiteral = createStyleLiteral(cmp, style);
8781
const propStr = ts.factory.createPropertyAssignment(style.modeName, styleLiteral);
8882
styleModes.push(propStr);
8983
} else if (Array.isArray(style.externalStyles) && style.externalStyles.length > 0) {
@@ -106,7 +100,7 @@ const getMultipleModeStyle = (
106100
return ts.factory.createObjectLiteralExpression(styleModes, true);
107101
};
108102

109-
const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, commentOriginalSelector: boolean) => {
103+
const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => {
110104
/**
111105
* the order of these if statements must match with
112106
* - {@link src/compiler/transformers/component-native/native-static-style.ts#addSingleStyleGetter}
@@ -116,7 +110,7 @@ const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, co
116110
if (typeof style.styleStr === 'string') {
117111
// inline the style string
118112
// static get style() { return "string"; }
119-
return createStyleLiteral(cmp, style, commentOriginalSelector);
113+
return createStyleLiteral(cmp, style);
120114
}
121115

122116
if (Array.isArray(style.externalStyles) && style.externalStyles.length > 0) {
@@ -136,11 +130,11 @@ const getSingleStyle = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, co
136130
return null;
137131
};
138132

139-
const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler, commentOriginalSelector: boolean) => {
140-
if (cmp.encapsulation === 'scoped' || (commentOriginalSelector && cmp.encapsulation === 'shadow')) {
133+
const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler) => {
134+
if (cmp.encapsulation === 'scoped') {
141135
// scope the css first
142136
const scopeId = getScopeId(cmp.tagName, style.modeName);
143-
return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId, commentOriginalSelector));
137+
return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId));
144138
}
145139

146140
return ts.factory.createStringLiteral(style.styleStr);

‎src/compiler/transformers/component-hydrate/hydrate-runtime-cmp-meta.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ export const addHydrateRuntimeCmpMeta = (classMembers: ts.ClassElement[], cmp: d
2121
cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim;
2222
}
2323
const staticMember = createStaticGetter('cmpMeta', convertValueToLiteral(cmpMeta));
24-
const commentOriginalSelector = cmp.encapsulation === 'shadow';
25-
addStaticStyleGetterWithinClass(classMembers, cmp, commentOriginalSelector);
24+
addStaticStyleGetterWithinClass(classMembers, cmp);
2625

2726
classMembers.push(staticMember);
2827
};

‎src/compiler/transformers/component-native/native-static-style.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const createStyleLiteral = (cmp: d.ComponentCompilerMeta, style: d.StyleCompiler
9696
if (cmp.encapsulation === 'scoped') {
9797
// scope the css first
9898
const scopeId = getScopeId(cmp.tagName, style.modeName);
99-
return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId, false));
99+
return ts.factory.createStringLiteral(scopeCss(style.styleStr, scopeId));
100100
}
101101

102102
return ts.factory.createStringLiteral(style.styleStr);

‎src/declarations/stencil-private.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1938,7 +1938,6 @@ export interface TransformCssToEsmInput {
19381938
* is not shared by multiple fields, nor is it a composite of multiple modes).
19391939
*/
19401940
mode?: string;
1941-
commentOriginalSelector?: boolean;
19421941
sourceMap?: boolean;
19431942
minify?: boolean;
19441943
docs?: boolean;

‎src/hydrate/platform/hydrate-app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function hydrateApp(
9191
registerHost(elm, Cstr.cmpMeta);
9292

9393
// proxy the host element with the component's metadata
94-
proxyHostElement(elm, Cstr.cmpMeta, opts);
94+
proxyHostElement(elm, Cstr.cmpMeta);
9595
}
9696
}
9797
}

‎src/hydrate/platform/proxy-host-element.ts

+2-12
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import { CMP_FLAGS, MEMBER_FLAGS } from '@utils';
55

66
import type * as d from '../../declarations';
77

8-
export function proxyHostElement(
9-
elm: d.HostElement,
10-
cmpMeta: d.ComponentRuntimeMeta,
11-
opts: d.HydrateFactoryOptions,
12-
): void {
8+
export function proxyHostElement(elm: d.HostElement, cmpMeta: d.ComponentRuntimeMeta): void {
139
if (typeof elm.componentOnReady !== 'function') {
1410
elm.componentOnReady = componentOnReady;
1511
}
@@ -26,14 +22,8 @@ export function proxyHostElement(
2622
mode: 'open',
2723
delegatesFocus: !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDelegatesFocus),
2824
});
29-
} else if (opts.serializeShadowRoot) {
30-
elm.attachShadow({ mode: 'open' });
3125
} else {
32-
/**
33-
* For hydration users may want to render the shadow component as scoped
34-
* component, so we need to assign the element as shadowRoot.
35-
*/
36-
(elm as any).shadowRoot = elm;
26+
elm.attachShadow({ mode: 'open' });
3727
}
3828
}
3929

‎src/hydrate/runner/inspect-element.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type * as d from '../../declarations';
22

33
export function inspectElement(results: d.HydrateResults, elm: Element, depth: number) {
4-
const children = elm.children;
4+
const children = [...Array.from(elm.children), ...Array.from(elm.shadowRoot ? elm.shadowRoot.children : [])];
55

66
for (let i = 0, ii = children.length; i < ii; i++) {
77
const childElm = children[i];

‎src/hydrate/runner/render.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function renderToString(
4747
/**
4848
* Defines whether we render the shadow root as a declarative shadow root or as scoped shadow root.
4949
*/
50-
opts.serializeShadowRoot = Boolean(opts.serializeShadowRoot);
50+
opts.serializeShadowRoot = typeof opts.serializeShadowRoot === 'boolean' ? opts.serializeShadowRoot : true;
5151
/**
5252
* Make sure we wait for components to be hydrated.
5353
*/

‎src/mock-doc/serialize-node.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function normalizeSerializationOptions(opts: Partial<SerializeNodeToHtmlOptions>
2929
removeBooleanAttributeQuotes:
3030
typeof opts.removeBooleanAttributeQuotes !== 'boolean' ? false : opts.removeBooleanAttributeQuotes,
3131
removeHtmlComments: typeof opts.removeHtmlComments !== 'boolean' ? false : opts.removeHtmlComments,
32-
serializeShadowRoot: typeof opts.serializeShadowRoot !== 'boolean' ? false : opts.serializeShadowRoot,
32+
serializeShadowRoot: typeof opts.serializeShadowRoot !== 'boolean' ? true : opts.serializeShadowRoot,
3333
fullDocument: typeof opts.fullDocument !== 'boolean' ? true : opts.fullDocument,
3434
} as const;
3535
}
@@ -229,7 +229,7 @@ function* streamToHtml(
229229

230230
if (EMPTY_ELEMENTS.has(tagName) === false) {
231231
const shadowRoot = (node as HTMLElement).shadowRoot;
232-
if (opts.serializeShadowRoot && shadowRoot != null) {
232+
if (shadowRoot != null && opts.serializeShadowRoot) {
233233
output.indent = output.indent + (opts.indentSpaces ?? 0);
234234

235235
yield* streamToHtml(shadowRoot, opts, output);

‎src/runtime/bootstrap-lazy.ts

+1-9
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import {
1616
import { hmrStart } from './hmr-component';
1717
import { createTime, installDevTools } from './profile';
1818
import { proxyComponent } from './proxy-component';
19-
import { HYDRATED_CSS, HYDRATED_STYLE_ID, PLATFORM_FLAGS, PROXY_FLAGS, SLOT_FB_CSS } from './runtime-constants';
20-
import { convertScopedToShadow, registerStyle } from './styles';
19+
import { HYDRATED_CSS, PLATFORM_FLAGS, PROXY_FLAGS, SLOT_FB_CSS } from './runtime-constants';
2120
import { appDidLoad } from './update-component';
2221
export { setNonce } from '@platform';
2322

@@ -35,10 +34,8 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
3534
const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]');
3635
const dataStyles = /*@__PURE__*/ doc.createElement('style');
3736
const deferredConnectedCallbacks: { connectedCallback: () => void }[] = [];
38-
const styles = /*@__PURE__*/ doc.querySelectorAll(`[${HYDRATED_STYLE_ID}]`);
3937
let appLoadFallback: any;
4038
let isBootstrapping = true;
41-
let i = 0;
4239

4340
Object.assign(plt, options);
4441
plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', doc.baseURI).href;
@@ -52,11 +49,6 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.
5249
// async queue. This will improve the first input delay
5350
plt.$flags$ |= PLATFORM_FLAGS.appLoaded;
5451
}
55-
if (BUILD.hydrateClientSide && BUILD.shadowDom) {
56-
for (; i < styles.length; i++) {
57-
registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true);
58-
}
59-
}
6052

6153
let hasSlotRelocation = false;
6254
lazyBundles.map((lazyBundle) => {

‎src/runtime/initialize-component.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export const initializeComponent = async (
162162
BUILD.shadowDomShim &&
163163
cmpMeta.$flags$ & CMP_FLAGS.needsShadowDomShim
164164
) {
165-
style = await import('@utils/shadow-css').then((m) => m.scopeCss(style, scopeId, false));
165+
style = await import('@utils/shadow-css').then((m) => m.scopeCss(style, scopeId));
166166
}
167167

168168
registerStyle(scopeId, style, !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation));

‎src/runtime/styles.ts

+25-31
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,28 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet
8484
styleElm.setAttribute('nonce', nonce);
8585
}
8686

87-
if (BUILD.hydrateServerSide || BUILD.hotModuleReplacement) {
87+
if (
88+
(BUILD.hydrateServerSide || BUILD.hotModuleReplacement) &&
89+
cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation
90+
) {
8891
styleElm.setAttribute(HYDRATED_STYLE_ID, scopeId);
8992
}
9093

91-
styleContainerNode.insertBefore(styleElm, styleContainerNode.querySelector('link'));
94+
/**
95+
* only attach style tag to <head /> section if:
96+
*/
97+
const injectStyle =
98+
/**
99+
* we render a scoped component
100+
*/
101+
!(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) ||
102+
/**
103+
* we are using shadow dom and render the style tag within the shadowRoot
104+
*/
105+
(cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation && styleContainerNode.nodeName !== 'HEAD');
106+
if (injectStyle) {
107+
styleContainerNode.insertBefore(styleElm, styleContainerNode.querySelector('link'));
108+
}
92109
}
93110

94111
// Add styles for `slot-fb` elements if we're using slots outside the Shadow DOM
@@ -124,7 +141,12 @@ export const attachStyles = (hostRef: d.HostRef) => {
124141
hostRef.$modeName$,
125142
);
126143

127-
if ((BUILD.shadowDom || BUILD.scoped) && BUILD.cssAnnotations && flags & CMP_FLAGS.needsScopedEncapsulation) {
144+
if (
145+
(BUILD.shadowDom || BUILD.scoped) &&
146+
BUILD.cssAnnotations &&
147+
flags & CMP_FLAGS.needsScopedEncapsulation &&
148+
flags & CMP_FLAGS.scopedCssEncapsulation
149+
) {
128150
// only required when we're NOT using native shadow dom (slot)
129151
// or this browser doesn't support native shadow dom
130152
// and this host element was NOT created with SSR
@@ -152,34 +174,6 @@ export const attachStyles = (hostRef: d.HostRef) => {
152174
export const getScopeId = (cmp: d.ComponentRuntimeMeta, mode?: string) =>
153175
'sc-' + (BUILD.mode && mode && cmp.$flags$ & CMP_FLAGS.hasMode ? cmp.$tagName$ + '-' + mode : cmp.$tagName$);
154176

155-
/**
156-
* Convert a 'scoped' CSS string to one appropriate for use in the shadow DOM.
157-
*
158-
* Given a 'scoped' CSS string that looks like this:
159-
*
160-
* ```
161-
* /*!@div*\/div.class-name { display: flex };
162-
* ```
163-
*
164-
* Convert it to a 'shadow' appropriate string, like so:
165-
*
166-
* ```
167-
* /*!@div*\/div.class-name { display: flex }
168-
* ─┬─ ────────┬────────
169-
* │ │
170-
* │ ┌─────────────────┘
171-
* ▼ ▼
172-
* div{ display: flex }
173-
* ```
174-
*
175-
* Note that forward-slashes in the above are escaped so they don't end the
176-
* comment.
177-
*
178-
* @param css a CSS string to convert
179-
* @returns the converted string
180-
*/
181-
export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+)\*\/[^\{]+\{/g, '$1{');
182-
183177
declare global {
184178
export interface CSSStyleSheet {
185179
replaceSync(cssText: string): void;

‎src/runtime/test/shadow.spec.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,17 @@ describe('shadow', () => {
8484
});
8585

8686
const expected = `
87-
<cmp-a class="hydrated sc-cmp-a-h">
87+
<cmp-a class="hydrated">
8888
<!---->
89-
<div class="sc-cmp-a sc-cmp-a-s">
90-
<span class="sc-cmp-a" slot=\"start\">
89+
<div>
90+
<span slot=\"start\">
9191
Start
9292
</span>
93-
<span class='sc-cmp-a sc-cmp-a-s'>
93+
<span>
9494
Text
9595
</span>
96-
<div class='end sc-cmp-a sc-cmp-a-s'>
97-
<span class="sc-cmp-a" slot=\"end\">
96+
<div class="end">
97+
<span slot=\"end\">
9898
End
9999
</span>
100100
</div>

‎src/runtime/vdom/vdom-render.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex:
105105
updateElement(null, newVNode, isSvgMode);
106106
}
107107

108-
if ((BUILD.shadowDom || BUILD.scoped) && isDef(scopeId) && elm['s-si'] !== scopeId) {
108+
/**
109+
* walk up the DOM tree and check if we are in a shadow root because if we are within
110+
* a shadow root DOM we don't need to attach scoped class names to the element
111+
*/
112+
const rootNode = elm.getRootNode() as HTMLElement;
113+
const isElementWithinShadowRoot = !rootNode.querySelector('body');
114+
if (!isElementWithinShadowRoot && BUILD.scoped && isDef(scopeId) && elm['s-si'] !== scopeId) {
109115
// if there is a scopeId and this is the initial render
110116
// then let's add the scopeId as a css class
111117
elm.classList.add((elm['s-si'] = scopeId));

‎src/utils/shadow-css.ts

+6-55
Original file line numberDiff line numberDiff line change
@@ -425,13 +425,7 @@ const scopeSelector = (selector: string, scopeSelectorText: string, hostSelector
425425
.join(', ');
426426
};
427427

428-
const scopeSelectors = (
429-
cssText: string,
430-
scopeSelectorText: string,
431-
hostSelector: string,
432-
slotSelector: string,
433-
commentOriginalSelector: boolean,
434-
) => {
428+
const scopeSelectors = (cssText: string, scopeSelectorText: string, hostSelector: string, slotSelector: string) => {
435429
return processRules(cssText, (rule: CssRule) => {
436430
let selector = rule.selector;
437431
let content = rule.content;
@@ -443,7 +437,7 @@ const scopeSelectors = (
443437
rule.selector.startsWith('@page') ||
444438
rule.selector.startsWith('@document')
445439
) {
446-
content = scopeSelectors(rule.content, scopeSelectorText, hostSelector, slotSelector, commentOriginalSelector);
440+
content = scopeSelectors(rule.content, scopeSelectorText, hostSelector, slotSelector);
447441
}
448442

449443
const cssRule: CssRule = {
@@ -454,13 +448,7 @@ const scopeSelectors = (
454448
});
455449
};
456450

457-
const scopeCssText = (
458-
cssText: string,
459-
scopeId: string,
460-
hostScopeId: string,
461-
slotScopeId: string,
462-
commentOriginalSelector: boolean,
463-
) => {
451+
const scopeCssText = (cssText: string, scopeId: string, hostScopeId: string, slotScopeId: string) => {
464452
cssText = insertPolyfillHostInCssText(cssText);
465453
cssText = convertColonHost(cssText);
466454
cssText = convertColonHostContext(cssText);
@@ -470,7 +458,7 @@ const scopeCssText = (
470458
cssText = convertShadowDOMSelectors(cssText);
471459

472460
if (scopeId) {
473-
cssText = scopeSelectors(cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector);
461+
cssText = scopeSelectors(cssText, scopeId, hostScopeId, slotScopeId);
474462
}
475463

476464
cssText = replaceShadowCssHost(cssText, hostScopeId);
@@ -499,53 +487,16 @@ const replaceShadowCssHost = (cssText: string, hostScopeId: string) => {
499487
return cssText.replace(/-shadowcsshost-no-combinator/g, `.${hostScopeId}`);
500488
};
501489

502-
export const scopeCss = (cssText: string, scopeId: string, commentOriginalSelector: boolean) => {
490+
export const scopeCss = (cssText: string, scopeId: string) => {
503491
const hostScopeId = scopeId + '-h';
504492
const slotScopeId = scopeId + '-s';
505493

506494
const commentsWithHash = extractCommentsWithHash(cssText);
507495

508496
cssText = stripComments(cssText);
509-
const orgSelectors: {
510-
placeholder: string;
511-
comment: string;
512-
}[] = [];
513-
514-
if (commentOriginalSelector) {
515-
const processCommentedSelector = (rule: CssRule) => {
516-
const placeholder = `/*!@___${orgSelectors.length}___*/`;
517-
const comment = `/*!@${rule.selector}*/`;
518-
519-
orgSelectors.push({ placeholder, comment });
520-
rule.selector = placeholder + rule.selector;
521-
return rule;
522-
};
523-
524-
cssText = processRules(cssText, (rule) => {
525-
if (rule.selector[0] !== '@') {
526-
return processCommentedSelector(rule);
527-
} else if (
528-
rule.selector.startsWith('@media') ||
529-
rule.selector.startsWith('@supports') ||
530-
rule.selector.startsWith('@page') ||
531-
rule.selector.startsWith('@document')
532-
) {
533-
rule.content = processRules(rule.content, processCommentedSelector);
534-
return rule;
535-
}
536-
return rule;
537-
});
538-
}
539-
540-
const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId, commentOriginalSelector);
497+
const scoped = scopeCssText(cssText, scopeId, hostScopeId, slotScopeId);
541498
cssText = [scoped.cssText, ...commentsWithHash].join('\n');
542499

543-
if (commentOriginalSelector) {
544-
orgSelectors.forEach(({ placeholder, comment }) => {
545-
cssText = cssText.replace(placeholder, comment);
546-
});
547-
}
548-
549500
scoped.slottedSelectors.forEach((slottedSelector) => {
550501
const regex = new RegExp(escapeRegExpSpecialCharacters(slottedSelector.orgSelector), 'g');
551502
cssText = cssText.replace(regex, slottedSelector.updatedSelector);

‎src/utils/test/scope-css.spec.ts

+2-104
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
* found in the LICENSE file at https://angular.io/license
1212
*/
1313

14-
import { convertScopedToShadow } from '../../runtime/styles';
1514
import { scopeCss } from '../shadow-css';
1615

1716
describe('ShadowCss', function () {
18-
function s(cssText: string, scopeId: string, commentOriginalSelector = false) {
19-
const shim = scopeCss(cssText, scopeId, commentOriginalSelector);
17+
function s(cssText: string, scopeId: string) {
18+
const shim = scopeCss(cssText, scopeId);
2019

2120
const nlRegexp = /\n/g;
2221
return normalizeCSS(shim.replace(nlRegexp, ''));
@@ -26,21 +25,6 @@ describe('ShadowCss', function () {
2625
expect(s('', 'a')).toEqual('');
2726
});
2827

29-
it('should handle empty string, commented org selector', () => {
30-
expect(s('', 'a', true)).toEqual('');
31-
});
32-
33-
it('div', () => {
34-
const r = s('div {}', 'sc-ion-tag', true);
35-
expect(r).toEqual('/*!@div*/div.sc-ion-tag {}');
36-
});
37-
38-
it('should add an attribute to every rule, commented org selector', () => {
39-
const css = 'one {color: red;}two {color: red;}';
40-
const expected = '/*!@one*/one.a {color:red;}/*!@two*/two.a {color:red;}';
41-
expect(s(css, 'a', true)).toEqual(expected);
42-
});
43-
4428
it('should add an attribute to every rule', () => {
4529
const css = 'one {color: red;}two {color: red;}';
4630
const expected = 'one.a {color:red;}two.a {color:red;}';
@@ -71,24 +55,12 @@ describe('ShadowCss', function () {
7155
expect(s(css, 'a')).toEqual(expected);
7256
});
7357

74-
it('should handle media rules, commentOriginalSelector', () => {
75-
const css = '@media screen and (max-width:800px, max-height:100%) {div {font-size:50px;}}';
76-
const expected = '@media screen and (max-width:800px, max-height:100%) {/*!@div*/div.a {font-size:50px;}}';
77-
expect(s(css, 'a', true)).toEqual(expected);
78-
});
79-
8058
it('should handle page rules', () => {
8159
const css = '@page {div {font-size:50px;}}';
8260
const expected = '@page {div.a {font-size:50px;}}';
8361
expect(s(css, 'a')).toEqual(expected);
8462
});
8563

86-
it('should handle page rules, commentOriginalSelector', () => {
87-
const css = '@page {div {font-size:50px;}}';
88-
const expected = '@page {/*!@div*/div.a {font-size:50px;}}';
89-
expect(s(css, 'a', true)).toEqual(expected);
90-
});
91-
9264
it('should handle document rules', () => {
9365
const css = '@document url(http://www.w3.org/) {div {font-size:50px;}}';
9466
const expected = '@document url(http://www.w3.org/) {div.a {font-size:50px;}}';
@@ -113,11 +85,6 @@ describe('ShadowCss', function () {
11385
expect(s(css, 'a')).toEqual(css);
11486
});
11587

116-
it('should handle keyframes rules, commentOriginalSelector', () => {
117-
const css = '@keyframes foo {0% {transform:translate(-50%) scaleX(0);}}';
118-
expect(s(css, 'a', true)).toEqual(css);
119-
});
120-
12188
it('should handle -webkit-keyframes rules', () => {
12289
const css = '@-webkit-keyframes foo {0% {-webkit-transform:translate(-50%) scaleX(0);}}';
12390
expect(s(css, 'a')).toEqual(css);
@@ -153,10 +120,6 @@ describe('ShadowCss', function () {
153120
});
154121

155122
describe(':host', () => {
156-
it('should handle no context, commentOriginalSelector', () => {
157-
expect(s(':host {}', 'a', true)).toEqual('/*!@:host*/.a-h {}');
158-
});
159-
160123
it('should handle no context', () => {
161124
expect(s(':host {}', 'a')).toEqual('.a-h {}');
162125
});
@@ -174,11 +137,6 @@ describe('ShadowCss', function () {
174137
expect(s(':host([a=b]) {}', 'a')).toEqual('[a="b"].a-h {}');
175138
});
176139

177-
it('should handle multiple tag selectors, commenting the original selector', () => {
178-
expect(s(':host(ul,li) {}', 'a', true)).toEqual('/*!@:host(ul,li)*/ul.a-h, li.a-h {}');
179-
expect(s(':host(ul,li) > .z {}', 'a', true)).toEqual('/*!@:host(ul,li) > .z*/ul.a-h > .z.a, li.a-h > .z.a {}');
180-
});
181-
182140
it('should handle multiple tag selectors', () => {
183141
expect(s(':host(ul,li) {}', 'a')).toEqual('ul.a-h, li.a-h {}');
184142
expect(s(':host(ul,li) > .z {}', 'a')).toEqual('ul.a-h > .z.a, li.a-h > .z.a {}');
@@ -193,10 +151,6 @@ describe('ShadowCss', function () {
193151
expect(s(':host([a="b"],[c=d]) {}', 'a')).toEqual('[a="b"].a-h, [c="d"].a-h {}');
194152
});
195153

196-
it('should handle multiple attribute selectors, commentOriginalSelector', () => {
197-
expect(s(':host([a="b"],[c=d]) {}', 'a', true)).toEqual('/*!@:host([a="b"],[c=d])*/[a="b"].a-h, [c="d"].a-h {}');
198-
});
199-
200154
it('should handle pseudo selectors', () => {
201155
expect(s(':host(:before) {}', 'a')).toEqual('.a-h:before {}');
202156
expect(s(':host:before {}', 'a')).toEqual('.a-h:before {}');
@@ -215,13 +169,6 @@ describe('ShadowCss', function () {
215169
});
216170

217171
describe(':host-context', () => {
218-
it('should handle tag selector, commentOriginalSelector', () => {
219-
expect(s(':host-context(div) {}', 'a', true)).toEqual('/*!@:host-context(div)*/div.a-h, div .a-h {}');
220-
expect(s(':host-context(ul) > .y {}', 'a', true)).toEqual(
221-
'/*!@:host-context(ul) > .y*/ul.a-h > .y.a, ul .a-h > .y.a {}',
222-
);
223-
});
224-
225172
it('should handle tag selector', () => {
226173
expect(s(':host-context(div) {}', 'a')).toEqual('div.a-h, div .a-h {}');
227174
expect(s(':host-context(ul) > .y {}', 'a')).toEqual('ul.a-h > .y.a, ul .a-h > .y.a {}');
@@ -247,11 +194,6 @@ describe('ShadowCss', function () {
247194
});
248195

249196
describe('::slotted', () => {
250-
it('should handle *, commentOriginalSelector', () => {
251-
const r = s('::slotted(*) {}', 'sc-ion-tag', true);
252-
expect(r).toEqual('/*!@::slotted(*)*/.sc-ion-tag-s > * {}');
253-
});
254-
255197
it('should handle *', () => {
256198
const r = s('::slotted(*) {}', 'sc-ion-tag');
257199
expect(r).toEqual('.sc-ion-tag-s > * {}');
@@ -306,62 +248,18 @@ describe('ShadowCss', function () {
306248
expect(r).toEqual('.sc-ion-tag-s > * {}, .sc-ion-tag-s > * {}, .sc-ion-tag-s > * {}');
307249
});
308250

309-
it('same selectors, commentOriginalSelector', () => {
310-
const r = s('::slotted(*) {}, ::slotted(*) {}, ::slotted(*) {}', 'sc-ion-tag', true);
311-
expect(r).toEqual(
312-
'/*!@::slotted(*)*/.sc-ion-tag-s > * {}/*!@, ::slotted(*)*/.sc-ion-tag, .sc-ion-tag-s > * {}/*!@, ::slotted(*)*/.sc-ion-tag, .sc-ion-tag-s > * {}',
313-
);
314-
});
315-
316251
it('should combine parent selector when comma', () => {
317252
const r = s('.a .b, .c ::slotted(*) {}', 'sc-ion-tag');
318253
expect(r).toEqual('.a.sc-ion-tag .b.sc-ion-tag, .c.sc-ion-tag-s > *, .c .sc-ion-tag-s > * {}');
319254
});
320255

321-
it('should handle multiple selector, commentOriginalSelector', () => {
322-
const r = s('::slotted(ul), ::slotted(li) {}', 'sc-ion-tag', true);
323-
expect(r).toEqual('/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}');
324-
});
325-
326256
it('should not replace the selector in a `@supports` rule', () => {
327257
expect(s('@supports selector(::slotted(*)) {::slotted(*) {color: red; }}', 'sc-cmp')).toEqual(
328258
'@supports selector(::slotted(*)) {.sc-cmp-s > * {color:red;}}',
329259
);
330260
});
331261
});
332262

333-
describe('convertScopedToShadow', () => {
334-
it('media query', () => {
335-
const input = `@media screen and (max-width:800px, max-height:100%) {/*!@div*/div.a {font-size:50px;}}`;
336-
const expected = `@media screen and (max-width:800px, max-height:100%) {div{font-size:50px;}}`;
337-
expect(convertScopedToShadow(input)).toBe(expected);
338-
});
339-
340-
it('div', () => {
341-
const input = `/*!@div*/div.sc-ion-tag {}`;
342-
const expected = `div{}`;
343-
expect(convertScopedToShadow(input)).toBe(expected);
344-
});
345-
346-
it('new lines', () => {
347-
const input = `/*!@div*/div.sc-ion-tag \n\n\n \t{}`;
348-
const expected = `div{}`;
349-
expect(convertScopedToShadow(input)).toBe(expected);
350-
});
351-
352-
it(':host', () => {
353-
const input = `/*!@:host*/.a-h {}`;
354-
const expected = `:host{}`;
355-
expect(convertScopedToShadow(input)).toBe(expected);
356-
});
357-
358-
it('::slotted', () => {
359-
const input = `/*!@::slotted(ul), ::slotted(li)*/.sc-ion-tag-s > ul, .sc-ion-tag-s > li {}`;
360-
const expected = `::slotted(ul), ::slotted(li){}`;
361-
expect(convertScopedToShadow(input)).toBe(expected);
362-
});
363-
});
364-
365263
it('should handle ::shadow', () => {
366264
const css = s('x::shadow > y {}', 'a');
367265
expect(css).toEqual('x.a > y.a {}');

‎test/end-to-end/src/components.d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ export namespace Components {
4343
}
4444
interface CmpServerVsClient {
4545
}
46+
interface CmpWithSlot {
47+
}
4648
interface DomApi {
4749
}
4850
interface DomInteraction {
@@ -241,6 +243,12 @@ declare global {
241243
prototype: HTMLCmpServerVsClientElement;
242244
new (): HTMLCmpServerVsClientElement;
243245
};
246+
interface HTMLCmpWithSlotElement extends Components.CmpWithSlot, HTMLStencilElement {
247+
}
248+
var HTMLCmpWithSlotElement: {
249+
prototype: HTMLCmpWithSlotElement;
250+
new (): HTMLCmpWithSlotElement;
251+
};
244252
interface HTMLDomApiElement extends Components.DomApi, HTMLStencilElement {
245253
}
246254
var HTMLDomApiElement: {
@@ -406,6 +414,7 @@ declare global {
406414
"cmp-c": HTMLCmpCElement;
407415
"cmp-dsd": HTMLCmpDsdElement;
408416
"cmp-server-vs-client": HTMLCmpServerVsClientElement;
417+
"cmp-with-slot": HTMLCmpWithSlotElement;
409418
"dom-api": HTMLDomApiElement;
410419
"dom-interaction": HTMLDomInteractionElement;
411420
"dom-visible": HTMLDomVisibleElement;
@@ -467,6 +476,8 @@ declare namespace LocalJSX {
467476
}
468477
interface CmpServerVsClient {
469478
}
479+
interface CmpWithSlot {
480+
}
470481
interface DomApi {
471482
}
472483
interface DomInteraction {
@@ -540,6 +551,7 @@ declare namespace LocalJSX {
540551
"cmp-c": CmpC;
541552
"cmp-dsd": CmpDsd;
542553
"cmp-server-vs-client": CmpServerVsClient;
554+
"cmp-with-slot": CmpWithSlot;
543555
"dom-api": DomApi;
544556
"dom-interaction": DomInteraction;
545557
"dom-visible": DomVisible;
@@ -584,6 +596,7 @@ declare module "@stencil/core" {
584596
"cmp-c": LocalJSX.CmpC & JSXBase.HTMLAttributes<HTMLCmpCElement>;
585597
"cmp-dsd": LocalJSX.CmpDsd & JSXBase.HTMLAttributes<HTMLCmpDsdElement>;
586598
"cmp-server-vs-client": LocalJSX.CmpServerVsClient & JSXBase.HTMLAttributes<HTMLCmpServerVsClientElement>;
599+
"cmp-with-slot": LocalJSX.CmpWithSlot & JSXBase.HTMLAttributes<HTMLCmpWithSlotElement>;
587600
"dom-api": LocalJSX.DomApi & JSXBase.HTMLAttributes<HTMLDomApiElement>;
588601
"dom-interaction": LocalJSX.DomInteraction & JSXBase.HTMLAttributes<HTMLDomInteractionElement>;
589602
"dom-visible": LocalJSX.DomVisible & JSXBase.HTMLAttributes<HTMLDomVisibleElement>;
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,85 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`renderToString can render a scoped component within a shadow component 1`] = `"<car-list cars=\\"[{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Vento&quot;,&quot;year&quot;:2024},{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Beetle&quot;,&quot;year&quot;:2023}]\\" class=\\"sc-car-list-h\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-car-list\\">/*!@:host*/.sc-car-list-h{display:block;margin:10px;padding:10px;border:1px solid blue}/*!@ul*/ul.sc-car-list{display:block;margin:0;padding:0}/*!@li*/li.sc-car-list{list-style:none;margin:0;padding:20px}/*!@.selected*/.selected.sc-car-list{font-weight:bold;background:rgb(255, 255, 210)}</style><ul class=\\"sc-car-list\\" c-id=\\"1.0.0.0\\"><li class=\\"sc-car-list\\" c-id=\\"1.1.1.0\\"><car-detail class=\\"sc-car-list\\" custom-hydrate-flag=\\"\\" c-id=\\"1.2.2.0\\" s-id=\\"2\\"><!--r.2--><section class=\\"sc-car-list\\" c-id=\\"2.0.0.0\\"><!--t.2.1.1.0-->2024 VW Vento</section></car-detail></li><li class=\\"sc-car-list\\" c-id=\\"1.3.1.1\\"><car-detail class=\\"sc-car-list\\" custom-hydrate-flag=\\"\\" c-id=\\"1.4.2.0\\" s-id=\\"3\\"><!--r.3--><section class=\\"sc-car-list\\" c-id=\\"3.0.0.0\\"><!--t.3.1.1.0-->2023 VW Beetle</section></car-detail></li></ul></template><!--r.1--></car-list>"`;
3+
exports[`renderToString can render a scoped component within a shadow component 1`] = `"<car-list cars=\\"[{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Vento&quot;,&quot;year&quot;:2024},{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Beetle&quot;,&quot;year&quot;:2023}]\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\"><template shadowrootmode=\\"open\\"><style>:host{display:block;margin:10px;padding:10px;border:1px solid blue}ul{display:block;margin:0;padding:0}li{list-style:none;margin:0;padding:20px}.selected{font-weight:bold;background:rgb(255, 255, 210)}</style><ul c-id=\\"1.0.0.0\\"><li c-id=\\"1.1.1.0\\"><car-detail custom-hydrate-flag=\\"\\" c-id=\\"1.2.2.0\\" s-id=\\"2\\"><!--r.2--><section c-id=\\"2.0.0.0\\"><!--t.2.1.1.0-->2024 VW Vento</section></car-detail></li><li c-id=\\"1.3.1.1\\"><car-detail custom-hydrate-flag=\\"\\" c-id=\\"1.4.2.0\\" s-id=\\"3\\"><!--r.3--><section c-id=\\"3.0.0.0\\"><!--t.3.1.1.0-->2023 VW Beetle</section></car-detail></li></ul></template><!--r.1--></car-list>"`;
44

5-
exports[`renderToString can render nested components 1`] = `"<another-car-list cars=\\"[{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Vento&quot;,&quot;year&quot;:2024},{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Beetle&quot;,&quot;year&quot;:2023}]\\" class=\\"sc-another-car-list-h\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-another-car-list\\">/*!@:host*/.sc-another-car-list-h{display:block;margin:10px;padding:10px;border:1px solid blue}/*!@ul*/ul.sc-another-car-list{display:block;margin:0;padding:0}/*!@li*/li.sc-another-car-list{list-style:none;margin:0;padding:20px}/*!@.selected*/.selected.sc-another-car-list{font-weight:bold;background:rgb(255, 255, 210)}</style><ul class=\\"sc-another-car-list\\" c-id=\\"1.0.0.0\\"><li class=\\"sc-another-car-list\\" c-id=\\"1.1.1.0\\"><another-car-detail class=\\"sc-another-car-list sc-another-car-detail-h\\" custom-hydrate-flag=\\"\\" c-id=\\"1.2.2.0\\" s-id=\\"2\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-another-car-detail\\">/*!@section*/section.sc-another-car-detail{color:green}</style><section class=\\"sc-another-car-detail\\" c-id=\\"2.0.0.0\\"><!--t.2.1.1.0-->2024 VW Vento</section></template><!--r.2--></another-car-detail></li><li class=\\"sc-another-car-list\\" c-id=\\"1.3.1.1\\"><another-car-detail class=\\"sc-another-car-list sc-another-car-detail-h\\" custom-hydrate-flag=\\"\\" c-id=\\"1.4.2.0\\" s-id=\\"3\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-another-car-detail\\">/*!@section*/section.sc-another-car-detail{color:green}</style><section class=\\"sc-another-car-detail\\" c-id=\\"3.0.0.0\\"><!--t.3.1.1.0-->2023 VW Beetle</section></template><!--r.3--></another-car-detail></li></ul></template><!--r.1--></another-car-list>"`;
5+
exports[`renderToString can render a simple shadow component 1`] = `
6+
"<another-car-detail custom-hydrate-flag=\\"\\" s-id=\\"1\\">
7+
<template shadowrootmode=\\"open\\">
8+
<style>
9+
section{color:green}
10+
</style>
11+
</template>
12+
<!--r.1-->
13+
</another-car-detail>"
14+
`;
615
7-
exports[`renderToString renders scoped component 1`] = `"<another-car-detail class=\\"sc-another-car-detail-h\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-another-car-detail\\">/*!@section*/section.sc-another-car-detail{color:green}</style></template><!--r.1--></another-car-detail>"`;
16+
exports[`renderToString can render nested components 1`] = `
17+
"<another-car-list cars=\\"[{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Vento&quot;,&quot;year&quot;:2024},{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Beetle&quot;,&quot;year&quot;:2023}]\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\">
18+
<template shadowrootmode=\\"open\\">
19+
<style>
20+
:host{display:block;margin:10px;padding:10px;border:1px solid blue}ul{display:block;margin:0;padding:0}li{list-style:none;margin:0;padding:20px}.selected{font-weight:bold;background:rgb(255, 255, 210)}
21+
</style>
22+
<ul c-id=\\"1.0.0.0\\">
23+
<li c-id=\\"1.1.1.0\\">
24+
<another-car-detail c-id=\\"1.2.2.0\\" custom-hydrate-flag=\\"\\" s-id=\\"2\\">
25+
<template shadowrootmode=\\"open\\">
26+
<style>
27+
section{color:green}
28+
</style>
29+
<section c-id=\\"2.0.0.0\\">
30+
<!--t.2.1.1.0-->
31+
2024 VW Vento
32+
</section>
33+
</template>
34+
<!--r.2-->
35+
</another-car-detail>
36+
</li>
37+
<li c-id=\\"1.3.1.1\\">
38+
<another-car-detail c-id=\\"1.4.2.0\\" custom-hydrate-flag=\\"\\" s-id=\\"3\\">
39+
<template shadowrootmode=\\"open\\">
40+
<style>
41+
section{color:green}
42+
</style>
43+
<section c-id=\\"3.0.0.0\\">
44+
<!--t.3.1.1.0-->
45+
2023 VW Beetle
46+
</section>
47+
</template>
48+
<!--r.3-->
49+
</another-car-detail>
50+
</li>
51+
</ul>
52+
</template>
53+
<!--r.1-->
54+
</another-car-list>"
55+
`;
856
9-
exports[`renderToString supports passing props to components 1`] = `"<another-car-detail car=\\"{&quot;year&quot;:2024, &quot;make&quot;: &quot;VW&quot;, &quot;model&quot;: &quot;Vento&quot;}\\" class=\\"sc-another-car-detail-h\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-another-car-detail\\">/*!@section*/section.sc-another-car-detail{color:green}</style><section class=\\"sc-another-car-detail\\" c-id=\\"1.0.0.0\\"><!--t.1.1.1.0-->2024 VW Vento</section></template><!--r.1--></another-car-detail>"`;
57+
exports[`renderToString supports passing props to components 1`] = `
58+
"<another-car-detail car=\\"{&quot;year&quot;:2024, &quot;make&quot;: &quot;VW&quot;, &quot;model&quot;: &quot;Vento&quot;}\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\">
59+
<template shadowrootmode=\\"open\\">
60+
<style>
61+
section{color:green}
62+
</style>
63+
<section c-id=\\"1.0.0.0\\">
64+
<!--t.1.1.1.0-->
65+
2024 VW Vento
66+
</section>
67+
</template>
68+
<!--r.1-->
69+
</another-car-detail>"
70+
`;
1071
11-
exports[`renderToString supports passing props to components with a simple object 1`] = `"<another-car-detail car=\\"{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Vento&quot;,&quot;year&quot;:2024}\\" class=\\"sc-another-car-detail-h\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\"><template shadowrootmode=\\"open\\"><style sty-id=\\"sc-another-car-detail\\">/*!@section*/section.sc-another-car-detail{color:green}</style><section class=\\"sc-another-car-detail\\" c-id=\\"1.0.0.0\\"><!--t.1.1.1.0-->2024 VW Vento</section></template><!--r.1--></another-car-detail>"`;
72+
exports[`renderToString supports passing props to components with a simple object 1`] = `
73+
"<another-car-detail car=\\"{&quot;make&quot;:&quot;VW&quot;,&quot;model&quot;:&quot;Vento&quot;,&quot;year&quot;:2024}\\" custom-hydrate-flag=\\"\\" s-id=\\"1\\">
74+
<template shadowrootmode=\\"open\\">
75+
<style>
76+
section{color:green}
77+
</style>
78+
<section c-id=\\"1.0.0.0\\">
79+
<!--t.1.1.1.0-->
80+
2024 VW Vento
81+
</section>
82+
</template>
83+
<!--r.1-->
84+
</another-car-detail>"
85+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:host {
2+
display: block;
3+
border: 1px solid black;
4+
padding: 10px;
5+
}
6+
7+
:host button {
8+
background-color: lightblue;
9+
border: 1px solid blue;
10+
padding: 5px;
11+
}

‎test/end-to-end/src/declarative-shadow-dom/cmp-dsd.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Component, h, Prop, State } from '@stencil/core';
22

33
@Component({
44
tag: 'cmp-dsd',
5+
styleUrl: 'cmp-dsd.css',
56
shadow: true,
67
})
78
export class ComponentDSD {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Component, h } from '@stencil/core';
2+
3+
@Component({
4+
tag: 'cmp-with-slot',
5+
shadow: true,
6+
})
7+
export class ServerVSClientCmp {
8+
render() {
9+
return (
10+
<div>
11+
<div>
12+
<div>
13+
<slot></slot>
14+
</div>
15+
</div>
16+
</div>
17+
);
18+
}
19+
}

‎test/end-to-end/src/declarative-shadow-dom/test.e2e.ts

+31-12
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ describe('renderToString', () => {
6565
expect(await readableToString(streamToString(input))).toContain(renderedHTML);
6666
});
6767

68-
it('renders scoped component', async () => {
68+
it('can render a simple shadow component', async () => {
6969
const { html } = await renderToString('<another-car-detail></another-car-detail>', {
7070
serializeShadowRoot: true,
7171
fullDocument: false,
72+
prettyHtml: true,
7273
});
7374
expect(html).toMatchSnapshot();
7475
});
@@ -79,6 +80,7 @@ describe('renderToString', () => {
7980
{
8081
serializeShadowRoot: true,
8182
fullDocument: false,
83+
prettyHtml: true,
8284
},
8385
);
8486
expect(html).toMatchSnapshot();
@@ -89,6 +91,7 @@ describe('renderToString', () => {
8991
const { html } = await renderToString(`<another-car-detail car=${JSON.stringify(vento)}></another-car-detail>`, {
9092
serializeShadowRoot: true,
9193
fullDocument: false,
94+
prettyHtml: true,
9295
});
9396
expect(html).toMatchSnapshot();
9497
expect(html).toContain('2024 VW Vento');
@@ -102,15 +105,15 @@ describe('renderToString', () => {
102105
fullDocument: false,
103106
},
104107
);
105-
expect(html).toContain('<section class="sc-another-car-detail" c-id="1.0.0.0"><!--t.1.1.1.0--> </section>');
108+
expect(html).toContain('<section c-id="1.0.0.0"><!--t.1.1.1.0--> </section>');
106109
});
107110

108111
it('supports styles for DSD', async () => {
109112
const { html } = await renderToString('<another-car-detail></another-car-detail>', {
110113
serializeShadowRoot: true,
111114
fullDocument: false,
112115
});
113-
expect(html).toContain('section.sc-another-car-detail{color:green}');
116+
expect(html).toContain('<template shadowrootmode="open"><style>section{color:green}</style>');
114117
});
115118

116119
it('only returns the element if we render to DSD', async () => {
@@ -127,6 +130,7 @@ describe('renderToString', () => {
127130
{
128131
serializeShadowRoot: true,
129132
fullDocument: false,
133+
prettyHtml: true,
130134
},
131135
);
132136
expect(html).toMatchSnapshot();
@@ -141,18 +145,18 @@ describe('renderToString', () => {
141145
});
142146
expect(html).toMatchSnapshot();
143147
expect(html).toContain(
144-
'<car-detail class="sc-car-list" custom-hydrate-flag="" c-id="1.2.2.0" s-id="2"><!--r.2--><section class="sc-car-list" c-id="2.0.0.0"><!--t.2.1.1.0-->2024 VW Vento</section></car-detail>',
148+
`<car-detail custom-hydrate-flag=\"\" c-id=\"1.2.2.0\" s-id=\"2\"><!--r.2--><section c-id=\"2.0.0.0\"><!--t.2.1.1.0-->2024 VW Vento</section></car-detail>`,
145149
);
146150
expect(html).toContain(
147-
'<car-detail class="sc-car-list" custom-hydrate-flag="" c-id="1.4.2.0" s-id="3"><!--r.3--><section class="sc-car-list" c-id="3.0.0.0"><!--t.3.1.1.0-->2023 VW Beetle</section></car-detail>',
151+
`<car-detail custom-hydrate-flag=\"\" c-id=\"1.4.2.0\" s-id=\"3\"><!--r.3--><section c-id=\"3.0.0.0\"><!--t.3.1.1.0-->2023 VW Beetle</section></car-detail>`,
148152
);
149153
});
150154

151155
it('can render a scoped component within a shadow component (sync)', async () => {
152156
const input = `<car-list cars=${JSON.stringify([vento, beetle])}></car-list>`;
153157
const expectedResults = [
154-
'<car-detail class="sc-car-list" custom-hydrate-flag="" c-id="1.2.2.0" s-id="2"><!--r.2--><section class="sc-car-list" c-id="2.0.0.0"><!--t.2.1.1.0-->2024 VW Vento</section></car-detail>',
155-
'<car-detail class="sc-car-list" custom-hydrate-flag="" c-id="1.4.2.0" s-id="3"><!--r.3--><section class="sc-car-list" c-id="3.0.0.0"><!--t.3.1.1.0-->2023 VW Beetle</section></car-detail>',
158+
'<car-detail custom-hydrate-flag="" c-id="1.2.2.0" s-id="2"><!--r.2--><section c-id="2.0.0.0"><!--t.2.1.1.0-->2024 VW Vento</section></car-detail>',
159+
'<car-detail custom-hydrate-flag="" c-id="1.4.2.0" s-id="3"><!--r.3--><section c-id="3.0.0.0"><!--t.3.1.1.0-->2023 VW Beetle</section></car-detail>',
156160
] as const;
157161
const opts = {
158162
serializeShadowRoot: true,
@@ -221,21 +225,20 @@ describe('renderToString', () => {
221225
/**
222226
* renders the component with listener with proper vdom annotation, e.g.
223227
* ```html
224-
* <dsd-listen-cmp class="sc-dsd-listen-cmp-h" custom-hydrate-flag="" s-id="1">
228+
* <dsd-listen-cmp custom-hydrate-flag="" s-id="1">
225229
* <template shadowrootmode="open">
226230
* <style sty-id="sc-dsd-listen-cmp">
227231
* .sc-dsd-listen-cmp-h{display:block}
228232
* </style>
229-
* <slot c-id="1.0.0.0" class="sc-dsd-listen-cmp"></slot>
233+
* <slot c-id="1.0.0.0"></slot>
230234
* </template>
231235
* <!--r.1-->
232236
* Hello World
233237
* </dsd-listen-cmp>
234238
* ```
235239
*/
236-
237240
expect(html).toContain(
238-
`<dsd-listen-cmp class=\"sc-dsd-listen-cmp-h\" custom-hydrate-flag=\"\" s-id=\"1\"><template shadowrootmode=\"open\"><style sty-id=\"sc-dsd-listen-cmp\">/*!@:host*/.sc-dsd-listen-cmp-h{display:block}</style><slot class=\"sc-dsd-listen-cmp\" c-id=\"1.0.0.0\"></slot></template><!--r.1-->Hello World</dsd-listen-cmp>`,
241+
`<dsd-listen-cmp custom-hydrate-flag=\"\" s-id=\"1\"><template shadowrootmode=\"open\"><style>:host{display:block}</style><slot c-id=\"1.0.0.0\"></slot></template><!--r.1-->Hello World</dsd-listen-cmp>`,
239242
);
240243

241244
/**
@@ -250,7 +253,7 @@ describe('renderToString', () => {
250253
* </car-detail>
251254
*/
252255
expect(html).toContain(
253-
`<car-detail class=\"sc-car-list\" custom-hydrate-flag=\"\" c-id=\"2.4.2.0\" s-id=\"4\"><!--r.4--><section class=\"sc-car-list\" c-id=\"4.0.0.0\"><!--t.4.1.1.0-->2023 VW Beetle</section></car-detail>`,
256+
`<car-detail custom-hydrate-flag=\"\" c-id=\"2.4.2.0\" s-id=\"4\"><!--r.4--><section c-id=\"4.0.0.0\"><!--t.4.1.1.0-->2023 VW Beetle</section></car-detail>`,
254257
);
255258
});
256259

@@ -267,4 +270,20 @@ describe('renderToString', () => {
267270
expect(afterHydrate).toHaveBeenCalledTimes(1);
268271
expect(html).toContain('<body><div>Hello Universe</div></body>');
269272
});
273+
274+
it('does not render a shadow component if serializeShadowRoot is false', async () => {
275+
const { html } = await renderToString('<another-car-detail></another-car-detail>', {
276+
serializeShadowRoot: false,
277+
fullDocument: false,
278+
});
279+
expect(html).toBe('<another-car-detail custom-hydrate-flag="" s-id="1"><!--r.1--></another-car-detail>');
280+
});
281+
282+
it('does not render a shadow component but its light dom', async () => {
283+
const { html } = await renderToString('<cmp-with-slot>Hello World</cmp-with-slot>', {
284+
serializeShadowRoot: false,
285+
fullDocument: false,
286+
});
287+
expect(html).toBe('<cmp-with-slot custom-hydrate-flag="" s-id="1"><!--r.1-->Hello World</cmp-with-slot>');
288+
});
270289
});

‎test/end-to-end/src/miscellaneous/renderToString.e2e.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('renderToString', () => {
3636
</html>`,
3737
{ fullDocument: true, serializeShadowRoot: false },
3838
);
39+
3940
/**
4041
* starts with a DocType and HTML tag
4142
*/
@@ -44,7 +45,7 @@ describe('renderToString', () => {
4445
* renders hydration styles and custom link tag within the head tag
4546
*/
4647
expect(html).toContain(
47-
'selected.sc-car-list{font-weight:bold;background:rgb(255, 255, 210)}</style><link rel="stylesheet" href="whatever.css"> </head> <body>',
48+
'}</style> <link rel="stylesheet" href="whatever.css"> </head> <body> <div class="__next"> <main> <car-list',
4849
);
4950
});
5051

@@ -78,7 +79,7 @@ describe('renderToString', () => {
7879
* renders hydration styles and custom link tag within the head tag
7980
*/
8081
expect(html).toContain(
81-
'.selected.sc-scoped-car-list{font-weight:bold;background:rgb(255, 255, 210)}</style><style sty-id="sc-another-car-detail">/*!@section*/section.sc-another-car-detail{color:green}</style><link rel="stylesheet" href="whatever.css"> </head> <body> <div class="__next"> <main> <scoped-car-list',
82+
'.selected.sc-scoped-car-list{font-weight:bold;background:rgb(255, 255, 210)}</style><link rel="stylesheet" href="whatever.css"> </head> <body> <div class="__next"> <main> <scoped-car-list cars=',
8283
);
8384
});
8485
});

‎test/end-to-end/test-end-to-end-hydrate.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ async function main() {
88
<head><title>End To End</title></head>
99
<body>
1010
<prerender-cmp></prerender-cmp>
11+
<slot-cmp>Hello World</slot-cmp>
1112
</body>
1213
</html>
1314
`;
@@ -26,10 +27,14 @@ async function main() {
2627
throw new Error(`validated test/end-to-end/hydrate errors!!`);
2728
}
2829

29-
if (results.hydratedCount !== 1) {
30+
if (results.hydratedCount !== 2) {
3031
throw new Error(`invalid hydratedCount: ${results.hydratedCount}`);
3132
}
32-
if (results.components.length !== 1 || results.components[0].tag !== 'prerender-cmp') {
33+
if (
34+
results.components.length !== 2 ||
35+
results.components[0].tag !== 'prerender-cmp' ||
36+
results.components[1].tag !== 'slot-cmp'
37+
) {
3338
throw new Error(`invalid components: ${results.components}`);
3439
}
3540
if (results.httpStatus !== 200) {

0 commit comments

Comments
 (0)
Please sign in to comment.