diff --git a/src/compiler/app-core/app-data.ts b/src/compiler/app-core/app-data.ts index 2c941b8e7ae..ff7a7dd4059 100644 --- a/src/compiler/app-core/app-data.ts +++ b/src/compiler/app-core/app-data.ts @@ -187,6 +187,7 @@ export const updateBuildConditionals = (config: ValidatedConfig, b: BuildConditi if (config.hydratedFlag) { b.hydratedAttribute = config.hydratedFlag.selector === 'attribute'; b.hydratedClass = config.hydratedFlag.selector === 'class'; + b.hydratedSelectorName = config.hydratedFlag.name; } else { b.hydratedAttribute = false; b.hydratedClass = false; diff --git a/src/compiler/bundle/app-data-plugin.ts b/src/compiler/bundle/app-data-plugin.ts index 78f05a3e81e..8d10d591838 100644 --- a/src/compiler/bundle/app-data-plugin.ts +++ b/src/compiler/bundle/app-data-plugin.ts @@ -14,7 +14,7 @@ import { APP_DATA_CONDITIONAL, STENCIL_APP_DATA_ID, STENCIL_APP_GLOBALS_ID } fro * @param config the Stencil configuration for a particular project * @param compilerCtx the current compiler context * @param buildCtx the current build context - * @param build the set build conditionals for the build + * @param buildConditionals the set build conditionals for the build * @param platform the platform that is being built * @returns a Rollup plugin which carries out the necessary work */ @@ -22,7 +22,7 @@ export const appDataPlugin = ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, - build: d.BuildConditionals, + buildConditionals: d.BuildConditionals, platform: 'client' | 'hydrate' | 'worker', ): Plugin => { if (!platform) { @@ -68,7 +68,7 @@ export const appDataPlugin = ( // build custom app-data based off of component metadata const s = new MagicString(``); appendNamespace(config, s); - appendBuildConditionals(config, build, s); + appendBuildConditionals(config, buildConditionals, s); appendEnv(config, s); return s.toString(); } @@ -200,13 +200,17 @@ const appendGlobalScripts = (globalScripts: GlobalScript[], s: MagicString) => { * **This function mutates the provided {@link MagicString} argument** * * @param config the configuration associated with the Stencil project - * @param build the build conditionals to serialize into a JS object + * @param buildConditionals the build conditionals to serialize into a JS object * @param s a `MagicString` to append the generated constant onto */ -const appendBuildConditionals = (config: d.ValidatedConfig, build: d.BuildConditionals, s: MagicString): void => { - const buildData = Object.keys(build) +export const appendBuildConditionals = ( + config: d.ValidatedConfig, + buildConditionals: d.BuildConditionals, + s: MagicString, +): void => { + const buildData = Object.keys(buildConditionals) .sort() - .map((key) => key + ': ' + ((build as any)[key] ? 'true' : 'false')) + .map((key) => key + ': ' + JSON.stringify((buildConditionals as any)[key])) .join(', '); s.append(`export const BUILD = /* ${config.fsNamespace} */ { ${buildData} };\n`); diff --git a/src/compiler/bundle/test/app-data-plugin.spec.ts b/src/compiler/bundle/test/app-data-plugin.spec.ts new file mode 100644 index 00000000000..2ebeb7d695f --- /dev/null +++ b/src/compiler/bundle/test/app-data-plugin.spec.ts @@ -0,0 +1,46 @@ +import * as d from '@stencil/core/declarations'; +import { mockValidatedConfig } from '@stencil/core/testing'; +import MagicString from 'magic-string'; + +import { appendBuildConditionals } from '../app-data-plugin'; + +function setup() { + const config = mockValidatedConfig(); + const magicString = new MagicString(''); + return { config, magicString }; +} + +describe('app data plugin', () => { + it('should include the fsNamespace in the appended BUILD constant', () => { + const { config, magicString } = setup(); + appendBuildConditionals(config, {}, magicString); + expect(magicString.toString().includes(`export const BUILD = /* ${config.fsNamespace} */`)).toBe(true); + }); + + it.each([true, false])('should include hydratedAttribute when %p', (hydratedAttribute) => { + const conditionals: d.BuildConditionals = { + hydratedAttribute, + }; + const { config, magicString } = setup(); + appendBuildConditionals(config, conditionals, magicString); + expect(magicString.toString().includes(`hydratedAttribute: ${String(hydratedAttribute)}`)).toBe(true); + }); + + it.each([true, false])('should include hydratedClass when %p', (hydratedClass) => { + const conditionals: d.BuildConditionals = { + hydratedClass, + }; + const { config, magicString } = setup(); + appendBuildConditionals(config, conditionals, magicString); + expect(magicString.toString().includes(`hydratedClass: ${String(hydratedClass)}`)).toBe(true); + }); + + it('should append hydratedSelectorName', () => { + const conditionals: d.BuildConditionals = { + hydratedSelectorName: 'boop', + }; + const { config, magicString } = setup(); + appendBuildConditionals(config, conditionals, magicString); + expect(magicString.toString().includes('hydratedSelectorName: "boop"')).toBe(true); + }); +}); diff --git a/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts b/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts index 36b0948a2a8..d6c057945e5 100644 --- a/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts +++ b/src/compiler/output-targets/dist-hydrate-script/bundle-hydrate-factory.ts @@ -11,6 +11,16 @@ import { rewriteAliasedSourceFileImportPaths } from '../../transformers/rewrite- import { updateStencilCoreImports } from '../../transformers/update-stencil-core-import'; import { getHydrateBuildConditionals } from './hydrate-build-conditionals'; +/** + * Marshall some Rollup options for the hydrate factory and then pass it to our + * {@link bundleOutput} helper + * + * @param config a validated Stencil configuration + * @param compilerCtx the current compiler context + * @param buildCtx the current build context + * @param appFactoryEntryCode an entry code for the app factory + * @returns a promise wrapping a rollup build object + */ export const bundleHydrateFactory = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, @@ -21,7 +31,7 @@ export const bundleHydrateFactory = async ( const bundleOpts: BundleOptions = { id: 'hydrate', platform: 'hydrate', - conditionals: getHydrateBuildConditionals(buildCtx.components), + conditionals: getHydrateBuildConditionals(config, buildCtx.components), customBeforeTransformers: getCustomBeforeTransformers(config, compilerCtx), inlineDynamicImports: true, inputs: { 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 85e9a13dc5a..73b4c5a1f7a 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 @@ -14,6 +14,14 @@ import { HYDRATE_FACTORY_INTRO, HYDRATE_FACTORY_OUTRO } from './hydrate-factory- import { updateToHydrateComponents } from './update-to-hydrate-components'; import { writeHydrateOutputs } from './write-hydrate-outputs'; +/** + * Generate and build the hydrate app and then write it to disk + * + * @param config a validated Stencil configuration + * @param compilerCtx the current compiler context + * @param buildCtx the current build context + * @param outputTargets the output targets for the current build + */ export const generateHydrateApp = async ( config: d.ValidatedConfig, compilerCtx: d.CompilerCtx, diff --git a/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts b/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts index 75224d6fa38..99484b9c3c0 100644 --- a/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts +++ b/src/compiler/output-targets/dist-hydrate-script/hydrate-build-conditionals.ts @@ -1,8 +1,19 @@ import type * as d from '../../../declarations'; -import { getBuildFeatures } from '../../app-core/app-data'; +import { getBuildFeatures, updateBuildConditionals } from '../../app-core/app-data'; -export const getHydrateBuildConditionals = (cmps: d.ComponentCompilerMeta[]) => { +/** + * Get the `BUILD` conditionals for the hydrate build based on the current + * project + * + * @param config a validated Stencil configuration + * @param cmps component metadata + * @returns a populated build conditional object + */ +export const getHydrateBuildConditionals = (config: d.ValidatedConfig, cmps: d.ComponentCompilerMeta[]) => { const build = getBuildFeatures(cmps) as d.BuildConditionals; + // we need to make sure that things like the hydratedClass and flag are + // set for the hydrate build + updateBuildConditionals(config, build); build.slotRelocation = true; build.lazyLoad = true; @@ -32,8 +43,6 @@ export const getHydrateBuildConditionals = (cmps: d.ComponentCompilerMeta[]) => build.cssAnnotations = true; // TODO(STENCIL-854): Remove code related to legacy shadowDomShim field build.shadowDomShim = true; - build.hydratedAttribute = false; - build.hydratedClass = true; // TODO(STENCIL-1305): remove this option build.scriptDataOpts = false; build.attachStyles = true; diff --git a/src/compiler/output-targets/test/build-conditionals.spec.ts b/src/compiler/output-targets/test/build-conditionals.spec.ts index e23ade86c24..df54976bf11 100644 --- a/src/compiler/output-targets/test/build-conditionals.spec.ts +++ b/src/compiler/output-targets/test/build-conditionals.spec.ts @@ -3,6 +3,7 @@ import { mockConfig, mockLoadConfigInit } from '@stencil/core/testing'; import type * as d from '../../../declarations'; import { validateConfig } from '../../config/validate-config'; import { getCustomElementsBuildConditionals } from '../dist-custom-elements/custom-elements-build-conditionals'; +import { getHydrateBuildConditionals } from '../dist-hydrate-script/hydrate-build-conditionals'; import { getLazyBuildConditionals } from '../dist-lazy/lazy-build-conditionals'; describe('build-conditionals', () => { @@ -69,6 +70,15 @@ describe('build-conditionals', () => { const bc = getCustomElementsBuildConditionals(config, cmps); expect(bc.hydrateClientSide).toBe(true); }); + + it('hydratedSelectorName', () => { + userConfig.hydratedFlag = { + name: 'boooop', + }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const bc = getCustomElementsBuildConditionals(config, cmps); + expect(bc.hydratedSelectorName).toBe('boooop'); + }); }); describe('getLazyBuildConditionals', () => { @@ -144,5 +154,45 @@ describe('build-conditionals', () => { const bc = getLazyBuildConditionals(config, cmps); expect(bc.hydrateClientSide).toBe(true); }); + + it('hydratedSelectorName', () => { + userConfig.hydratedFlag = { + name: 'boooop', + }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const bc = getLazyBuildConditionals(config, cmps); + expect(bc.hydratedSelectorName).toBe('boooop'); + }); + }); + + describe('getHydrateBuildConditionals', () => { + it('hydratedSelectorName', () => { + userConfig.hydratedFlag = { + name: 'boooop', + }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const bc = getHydrateBuildConditionals(config, cmps); + expect(bc.hydratedSelectorName).toBe('boooop'); + }); + + it('should allow setting to use a class for hydration', () => { + userConfig.hydratedFlag = { + selector: 'class', + }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const bc = getHydrateBuildConditionals(config, cmps); + expect(bc.hydratedClass).toBe(true); + expect(bc.hydratedAttribute).toBe(false); + }); + + it('should allow setting to use an attr for hydration', () => { + userConfig.hydratedFlag = { + selector: 'attribute', + }; + const { config } = validateConfig(userConfig, mockLoadConfigInit()); + const bc = getHydrateBuildConditionals(config, cmps); + expect(bc.hydratedClass).toBe(false); + expect(bc.hydratedAttribute).toBe(true); + }); }); }); diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index b09a1f90550..137beec3f8a 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -180,6 +180,7 @@ export interface BuildConditionals extends Partial { cloneNodeFix?: boolean; hydratedAttribute?: boolean; hydratedClass?: boolean; + hydratedSelectorName?: string; initializeNextTick?: boolean; // TODO(STENCIL-1305): remove this option scriptDataOpts?: boolean; diff --git a/src/runtime/update-component.ts b/src/runtime/update-component.ts index de1eecb5ee9..3220c0390d2 100644 --- a/src/runtime/update-component.ts +++ b/src/runtime/update-component.ts @@ -456,11 +456,17 @@ const emitLifecycleEvent = (elm: EventTarget, lifecycleName: string) => { } }; +/** + * Set the hydrated flag on a DOM element + * + * @param elm a reference to a DOM element + * @returns undefined + */ const addHydratedFlag = (elm: Element) => BUILD.hydratedClass - ? elm.classList.add('hydrated') + ? elm.classList.add(BUILD.hydratedSelectorName ?? 'hydrated') : BUILD.hydratedAttribute - ? elm.setAttribute('hydrated', '') + ? elm.setAttribute(BUILD.hydratedSelectorName ?? 'hydrated', '') : undefined; const serverSideConnected = (elm: any) => {