Skip to content

Commit 33363d4

Browse files
feat(global-styles): add global styles support to shadow DOM components (#6268)
* feat(global-styles): add global styles support to shadow DOM components - Create new createShadowRoot utility function that applies global styles via adoptedStyleSheets - Update build system to handle @app-globals imports across all platforms (client, hydrate, testing) - Extend BuildCtx.stylesPromise to return compiled global styles string instead of void - Replace duplicate shadow root creation code with shared utility across runtime files - Add globalStyles export to app-globals module for CSS injection into shadow roots - Update app-data-plugin to include global styles in the bundle generation process This enables global styles to be automatically applied to all shadow DOM components while maintaining encapsulation and performance benefits of constructable stylesheets. * prettier * fix validate build script * prettier * make tests work * minor tweak * prettier * fix e2e tests * tweak
1 parent dc4bd52 commit 33363d4

29 files changed

Lines changed: 231 additions & 49 deletions

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
"import": "./internal/app-data/index.js",
5353
"require": "./internal/app-data/index.cjs"
5454
},
55+
"./internal/app-globals": {
56+
"import": "./internal/app-globals/index.js",
57+
"require": "./internal/app-globals/index.js"
58+
},
5559
"./mock-doc": {
5660
"types": "./mock-doc/index.d.ts",
5761
"import": "./mock-doc/index.js",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { BuildOptions as ESBuildOptions } from 'esbuild';
2+
import fs from 'fs-extra';
3+
import { join } from 'path';
4+
5+
import { BuildOptions } from '../utils/options';
6+
import { writePkgJson } from '../utils/write-pkg-json';
7+
import { getBaseEsbuildOptions } from './utils';
8+
9+
/**
10+
* Get an object containing ESbuild options to build the internal app globals
11+
* file. This function also performs relevant side-effects, like writing a
12+
* `package.json` file to disk.
13+
*
14+
* @param opts build options
15+
* @returns a Promise wrapping an array of ESbuild option objects
16+
*/
17+
export async function getInternalAppGlobalsBundles(opts: BuildOptions): Promise<ESBuildOptions[]> {
18+
const appGlobalsBuildDir = join(opts.buildDir, 'app-globals');
19+
const appGlobalsSrcDir = join(opts.srcDir, 'app-globals');
20+
const outputInternalAppDataDir = join(opts.output.internalDir, 'app-globals');
21+
22+
await fs.emptyDir(outputInternalAppDataDir);
23+
24+
// copy @stencil/core/internal/app-globals/index.d.ts
25+
await fs.copyFile(join(appGlobalsBuildDir, 'index.d.ts'), join(outputInternalAppDataDir, 'index.d.ts'));
26+
27+
// write @stencil/core/internal/app-globals/package.json
28+
writePkgJson(opts, outputInternalAppDataDir, {
29+
name: '@stencil/core/internal/app-globals',
30+
description: 'Used for default app globals.',
31+
main: 'index.js',
32+
module: 'index.js',
33+
sideEffects: false,
34+
});
35+
36+
const appGlobalsBaseOptions: ESBuildOptions = {
37+
...getBaseEsbuildOptions(),
38+
entryPoints: [join(appGlobalsSrcDir, 'index.ts')],
39+
platform: 'node',
40+
};
41+
42+
const appGlobalsESM: ESBuildOptions = {
43+
...appGlobalsBaseOptions,
44+
format: 'esm',
45+
outfile: join(outputInternalAppDataDir, 'index.js'),
46+
};
47+
48+
return [appGlobalsESM];
49+
}

scripts/esbuild/internal-platform-client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export async function getInternalClientBundles(opts: BuildOptions): Promise<ESBu
6060
plugins: [
6161
replace(createReplaceData(opts)),
6262
externalAlias('@app-data', '@stencil/core/internal/app-data'),
63+
externalAlias('@app-globals', '@stencil/core/internal/app-globals'),
6364
externalAlias('@utils/shadow-css', './shadow-css.js'),
6465
findAndReplaceLoadModule(),
6566
],
@@ -89,6 +90,7 @@ export async function getInternalClientBundles(opts: BuildOptions): Promise<ESBu
8990
replace(createReplaceData(opts)),
9091
externalAlias('@platform', '@stencil/core'),
9192
externalAlias('@app-data', '@stencil/core/internal/app-data'),
93+
externalAlias('@app-globals', '@stencil/core/internal/app-globals'),
9294
],
9395
};
9496

scripts/esbuild/internal.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { bundleDts, cleanDts } from '../utils/bundle-dts';
77
import type { BuildOptions } from '../utils/options';
88
import { writePkgJson } from '../utils/write-pkg-json';
99
import { getInternalAppDataBundles } from './internal-app-data';
10+
import { getInternalAppGlobalsBundles } from './internal-app-globals';
1011
import { getInternalClientBundles } from './internal-platform-client';
1112
import { getInternalPlatformHydrateBundles } from './internal-platform-hydrate';
1213
import { getInternalTestingBundle } from './internal-platform-testing';
@@ -57,10 +58,18 @@ export async function buildInternal(opts: BuildOptions) {
5758
const clientPlatformBundles = await getInternalClientBundles(opts);
5859
const hydratePlatformBundles = await getInternalPlatformHydrateBundles(opts);
5960
const appDataBundles = await getInternalAppDataBundles(opts);
61+
const appGlobalsBundles = await getInternalAppGlobalsBundles(opts);
6062
const internalTestingBundle = await getInternalTestingBundle(opts);
6163

6264
return runBuilds(
63-
[shadowCSSBundle, ...clientPlatformBundles, ...hydratePlatformBundles, internalTestingBundle, ...appDataBundles],
65+
[
66+
shadowCSSBundle,
67+
...clientPlatformBundles,
68+
...hydratePlatformBundles,
69+
internalTestingBundle,
70+
...appDataBundles,
71+
...appGlobalsBundles,
72+
],
6473
opts,
6574
);
6675
}

scripts/test/validate-build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ async function validateModuleTreeshake(opts: BuildOptions, moduleName: string, e
366366
},
367367
load(id) {
368368
if (id === '@stencil/core/internal/app-globals') {
369-
return 'export const globalScripts = () => {};';
369+
return 'export const globalScripts = () => {};\nexport const globalStyles = "";';
370370
}
371371
if (id === virtualInputId) {
372372
return `import "${entryId}";`;

src/app-globals/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const globalScripts = /* default */ () => {
22
/**/
33
};
4+
5+
export const globalStyles = /* default */ '';

src/compiler/build/build-ctx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class BuildContext implements d.BuildCtx {
5656
scriptsDeleted: string[] = [];
5757
startTime = Date.now();
5858
styleBuildCount = 0;
59-
stylesPromise: Promise<void> = null;
59+
stylesPromise: Promise<string> = null;
6060
stylesUpdated: d.BuildStyleUpdate[] = [];
6161
timeSpan: d.LoggerTimeSpan = null;
6262
timestamp: string;

src/compiler/bundle/app-data-plugin.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ export const appDataPlugin = (
5858
return null;
5959
},
6060

61-
load(id: string): LoadResult {
61+
async load(id: string): Promise<LoadResult> {
6262
if (id === STENCIL_APP_GLOBALS_ID) {
6363
const s = new MagicString(``);
6464
appendGlobalScripts(globalScripts, s);
65+
await appendGlobalStyles(buildCtx, s);
6566
return s.toString();
6667
}
6768
if (id === STENCIL_APP_DATA_ID) {
@@ -194,6 +195,17 @@ const appendGlobalScripts = (globalScripts: GlobalScript[], s: MagicString) => {
194195
}
195196
};
196197

198+
/**
199+
* Appends the global styles to the MagicString.
200+
*
201+
* @param buildCtx the build context
202+
* @param s the MagicString to append the global styles onto
203+
*/
204+
const appendGlobalStyles = async (buildCtx: d.BuildCtx, s: MagicString) => {
205+
const globalStyles = buildCtx.config.globalStyle ? await buildCtx.stylesPromise : '';
206+
s.append(`export const globalStyles = ${JSON.stringify(globalStyles)};\n`);
207+
};
208+
197209
/**
198210
* Generates the `BUILD` constant that is used at compile-time in a Stencil project
199211
*

src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydr
4242
var CharacterData = $stencilWindow.CharacterData;
4343
var CSS = $stencilWindow.CSS;
4444
var CustomEvent = $stencilWindow.CustomEvent;
45+
var CSSStyleSheet = $stencilWindow.CSSStyleSheet;
4546
var Document = $stencilWindow.Document;
4647
var ShadowRoot = $stencilWindow.ShadowRoot;
4748
var DocumentFragment = $stencilWindow.DocumentFragment;

src/compiler/style/global-styles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ export const generateGlobalStyles = async (
1212
) => {
1313
const outputTargets = config.outputTargets.filter(isOutputTargetDistGlobalStyles);
1414
if (outputTargets.length === 0) {
15-
return;
15+
return '';
1616
}
1717

1818
const globalStyles = await buildGlobalStyles(config, compilerCtx, buildCtx);
1919
if (globalStyles) {
2020
await Promise.all(outputTargets.map((o) => compilerCtx.fs.writeFile(o.file, globalStyles)));
2121
}
22+
23+
return globalStyles;
2224
};
2325

2426
const buildGlobalStyles = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx) => {

0 commit comments

Comments
 (0)