diff --git a/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts b/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts index 27f39233d58..2579a183a6b 100644 --- a/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts +++ b/src/compiler/output-targets/dist-hydrate-script/generate-hydrate-app.ts @@ -5,12 +5,17 @@ import { RollupOptions } from 'rollup'; import { rollup, type RollupBuild } from 'rollup'; import { + STENCIL_APP_DATA_ID, STENCIL_HYDRATE_FACTORY_ID, STENCIL_INTERNAL_HYDRATE_ID, STENCIL_MOCK_DOC_ID, } from '../../bundle/entry-alias-ids'; import { bundleHydrateFactory } from './bundle-hydrate-factory'; -import { HYDRATE_FACTORY_INTRO, HYDRATE_FACTORY_OUTRO } from './hydrate-factory-closure'; +import { + HYDRATE_FACTORY_INTRO, + HYDRATE_FACTORY_OUTRO, + MODE_RESOLUTION_CHAIN_DECLARATION, +} from './hydrate-factory-closure'; import { updateToHydrateComponents } from './update-to-hydrate-components'; import { writeHydrateOutputs } from './write-hydrate-outputs'; @@ -50,6 +55,7 @@ export const generateHydrateApp = async ( const packageDir = join(config.sys.getCompilerExecutingPath(), '..', '..'); const input = join(packageDir, 'internal', 'hydrate', 'runner.js'); const mockDoc = join(packageDir, 'mock-doc', 'index.js'); + const appData = join(packageDir, 'internal', 'app-data', 'index.js'); const rollupOptions: RollupOptions = { ...config.rollupConfig.inputOptions, @@ -67,6 +73,9 @@ export const generateHydrateApp = async ( if (id === STENCIL_MOCK_DOC_ID) { return mockDoc; } + if (id === STENCIL_APP_DATA_ID) { + return appData; + } return null; }, load(id) { @@ -75,6 +84,14 @@ export const generateHydrateApp = async ( } return null; }, + transform(code) { + /** + * Remove the modeResolutionChain variable from the generated code. + * This variable is redefined in `HYDRATE_FACTORY_INTRO` to ensure we can + * use it within the hydrate and global runtime. + */ + return code.replace(`var ${MODE_RESOLUTION_CHAIN_DECLARATION}`, ''); + }, }, ], treeshake: false, diff --git a/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts b/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts index 844eac429c9..467765395fe 100644 --- a/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts +++ b/src/compiler/output-targets/dist-hydrate-script/hydrate-factory-closure.ts @@ -1,6 +1,17 @@ export const HYDRATE_APP_CLOSURE_START = `/*hydrateAppClosure start*/`; +export const MODE_RESOLUTION_CHAIN_DECLARATION = `modeResolutionChain = [];`; + +/** + * This is the entry point for the hydrate factory. + * + * __Note:__ the `modeResolutionChain` will be uncommented in the + * `src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts` file. This enables us to use + * one module resolution chain across hydrate and core runtime. + */ export const HYDRATE_FACTORY_INTRO = ` +// const ${MODE_RESOLUTION_CHAIN_DECLARATION} + export function hydrateFactory($stencilWindow, $stencilHydrateOpts, $stencilHydrateResults, $stencilAfterHydrate, $stencilHydrateResolve) { var globalThis = $stencilWindow; var self = $stencilWindow; diff --git a/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts b/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts index 1e097a07ccf..516e767de16 100644 --- a/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts +++ b/src/compiler/output-targets/dist-hydrate-script/write-hydrate-outputs.ts @@ -3,6 +3,7 @@ import { basename } from 'path'; import type { RollupOutput } from 'rollup'; import type * as d from '../../../declarations'; +import { MODE_RESOLUTION_CHAIN_DECLARATION } from './hydrate-factory-closure'; import { relocateHydrateContextConst } from './relocate-hydrate-context'; export const writeHydrateOutputs = ( @@ -58,6 +59,15 @@ const writeHydrateOutput = async ( rollupOutput.output.map(async (output) => { if (output.type === 'chunk') { output.code = relocateHydrateContextConst(config, compilerCtx, output.code); + + /** + * Enable the line where we define `modeResolutionChain` for the hydrate module. + */ + output.code = output.code.replace( + `// const ${MODE_RESOLUTION_CHAIN_DECLARATION}`, + `const ${MODE_RESOLUTION_CHAIN_DECLARATION}`, + ); + const filePath = join(hydrateAppDirPath, output.fileName); await compilerCtx.fs.writeFile(filePath, output.code, { immediateWrite: true }); } diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 2c37a9fef7f..975b9da1f5f 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -2,6 +2,7 @@ import type { ConfigFlags } from '../cli/config-flags'; import type { PrerenderUrlResults, PrintLine } from '../internal'; import type { BuildCtx, CompilerCtx } from './stencil-private'; import type { JsonDocs } from './stencil-public-docs'; +import type { ResolutionHandler } from './stencil-public-runtime'; export * from './stencil-public-docs'; @@ -955,6 +956,11 @@ export interface SerializeDocumentOptions extends HydrateDocumentOptions { * @default true */ fullDocument?: boolean; + /** + * Style modes to render the component in. + * @see https://stenciljs.com/docs/styling#style-modes + */ + modes?: ResolutionHandler[]; } export interface HydrateFactoryOptions extends SerializeDocumentOptions { diff --git a/src/hydrate/runner/render.ts b/src/hydrate/runner/render.ts index 1254e4b315e..920034388b6 100644 --- a/src/hydrate/runner/render.ts +++ b/src/hydrate/runner/render.ts @@ -1,6 +1,7 @@ import { Readable } from 'node:stream'; import { hydrateFactory } from '@hydrate-factory'; +import { modeResolutionChain, setMode } from '@platform'; import { MockWindow, serializeNodeToHtml } from '@stencil/core/mock-doc'; import { hasError } from '@utils'; @@ -133,7 +134,17 @@ async function render(win: MockWindow, opts: HydrateFactoryOptions, results: Hyd const beforeHydrateFn = typeof opts.beforeHydrate === 'function' ? opts.beforeHydrate : NOOP; try { await Promise.resolve(beforeHydrateFn(win.document)); - return new Promise((resolve) => hydrateFactory(win, opts, results, afterHydrate, resolve)); + return new Promise((resolve) => { + if (Array.isArray(opts.modes)) { + /** + * Reset the mode resolution chain as we expect every `renderToString` call to render + * the components in new environment/document. + */ + modeResolutionChain.length = 0; + opts.modes.forEach((mode) => setMode(mode)); + } + return hydrateFactory(win, opts, results, afterHydrate, resolve); + }); } catch (e) { renderCatchError(results, e); return finalizeHydrate(win, win.document, opts, results); diff --git a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts index 6f80c6285ca..b3d1ef54a01 100644 --- a/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts +++ b/test/end-to-end/src/declarative-shadow-dom/test.e2e.ts @@ -290,4 +290,33 @@ describe('renderToString', () => { }); expect(html).toBe('Hello World'); }); + + describe('modes in declarative shadow dom', () => { + it('renders components in ios mode', async () => { + const { html } = await renderToString('', { + fullDocument: false, + prettyHtml: true, + modes: [() => 'ios'], + }); + expect(html).toContain('