Skip to content

Commit

Permalink
feat(runtime): support declarative shadow DOM (#5792)
Browse files Browse the repository at this point in the history
* feat(runtime): support declarative shadow DOM

feat(runtime): enhance renderToString to support serializeShadowRootAsDeclarativeShadowRoot flag

make esm hydrate script

make test work

add unit test

fix prettier

wip

minor tweaks

apply more changes from #5787

get unit tests working

prettier

remove import

fix test

eslint fix

use dynamic import

minor e2e fixes

prettier

fix cspell

adjust tests

prettier

allow to run headless

make streaming work

fix tests

prettier

remove obsolete file

this should fix pre-render test

prettier

finally get it right

prettier

* Update src/runtime/bootstrap-lazy.ts

Co-authored-by: Tanner Reits <[email protected]>

* fix: strict null checks

* prettier

* remove setTimeout call

---------

Co-authored-by: Tanner Reits <[email protected]>
  • Loading branch information
christian-bromann and tanner-reits authored Jun 21, 2024
1 parent 63491de commit c837063
Show file tree
Hide file tree
Showing 61 changed files with 1,081 additions and 494 deletions.
1 change: 1 addition & 0 deletions cspell-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ runtimes
searchbar
shadowcsshost
shadowcsshostcontext
shadowroot
sourcemaps
specfile
stenciljs
Expand Down
6 changes: 1 addition & 5 deletions src/client/client-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ export const setPlatformHelpers = (helpers: {
Object.assign(plt, helpers);
};

export const supportsShadow =
// TODO(STENCIL-854): Remove code related to legacy shadowDomShim field
BUILD.shadowDomShim && BUILD.shadowDom
? /*@__PURE__*/ (() => (doc.head.attachShadow + '').indexOf('[native') > -1)()
: true;
export const supportsShadow = BUILD.shadowDom;

export const supportsListenerOptions = /*@__PURE__*/ (() => {
let supportsListenerOptions = false;
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/html/canonical-link.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const updateCanonicalLink = (doc: Document, href: string) => {
export const updateCanonicalLink = (doc: Document, href?: string) => {
// https://webmasters.googleblog.com/2009/02/specify-your-canonical.html
// <link rel="canonical" href="http://www.example.com/product.php?item=swedish-fish" />
let canonicalLinkElm = doc.head.querySelector('link[rel="canonical"]');
Expand All @@ -20,7 +20,7 @@ export const updateCanonicalLink = (doc: Document, href: string) => {
// but there is a canonical link in the head so let's remove it
const existingHref = canonicalLinkElm.getAttribute('href');
if (!existingHref) {
canonicalLinkElm.parentNode.removeChild(canonicalLinkElm);
canonicalLinkElm.parentNode?.removeChild(canonicalLinkElm);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type * as d from '@stencil/core/declarations';
import { catchError, createOnWarnFn, generatePreamble, join, loadRollupDiagnostics } from '@utils';
import MagicString from 'magic-string';
import { RollupOptions } from 'rollup';
import { rollup } from 'rollup';
import { rollup, type RollupBuild } from 'rollup';

import type * as d from '../../../declarations';
import {
STENCIL_HYDRATE_FACTORY_ID,
STENCIL_INTERNAL_HYDRATE_ID,
Expand All @@ -14,6 +14,24 @@ import { HYDRATE_FACTORY_INTRO, HYDRATE_FACTORY_OUTRO } from './hydrate-factory-
import { updateToHydrateComponents } from './update-to-hydrate-components';
import { writeHydrateOutputs } from './write-hydrate-outputs';

const buildHydrateAppFor = async (
format: 'esm' | 'cjs',
rollupBuild: RollupBuild,
config: d.ValidatedConfig,
compilerCtx: d.CompilerCtx,
buildCtx: d.BuildCtx,
outputTargets: d.OutputTargetHydrate[],
) => {
const file = format === 'esm' ? 'index.mjs' : 'index.js';
const rollupOutput = await rollupBuild.generate({
banner: generatePreamble(config),
format,
file,
});

await writeHydrateOutputs(config, compilerCtx, buildCtx, outputTargets, rollupOutput);
};

/**
* Generate and build the hydrate app and then write it to disk
*
Expand All @@ -35,6 +53,7 @@ export const generateHydrateApp = async (

const rollupOptions: RollupOptions = {
...config.rollupConfig.inputOptions,
external: ['stream'],

input,
inlineDynamicImports: true,
Expand Down Expand Up @@ -63,13 +82,10 @@ export const generateHydrateApp = async (
};

const rollupAppBuild = await rollup(rollupOptions);
const rollupOutput = await rollupAppBuild.generate({
banner: generatePreamble(config),
format: 'cjs',
file: 'index.js',
});

await writeHydrateOutputs(config, compilerCtx, buildCtx, outputTargets, rollupOutput);
await Promise.all([
buildHydrateAppFor('cjs', rollupAppBuild, config, compilerCtx, buildCtx, outputTargets),
buildHydrateAppFor('esm', rollupAppBuild, config, compilerCtx, buildCtx, outputTargets),
]);
} catch (e: any) {
if (!buildCtx.hasError) {
// TODO(STENCIL-353): Implement a type guard that balances using our own copy of Rollup types (which are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@ const writeHydrateOutput = async (
const hydratePackageName = await getHydratePackageName(config, compilerCtx);

const hydrateAppDirPath = outputTarget.dir;
if (!hydrateAppDirPath) {
throw new Error(`outputTarget config missing the "dir" property`);
}

const hydrateCoreIndexPath = join(hydrateAppDirPath, 'index.js');
const hydrateCoreIndexPathESM = join(hydrateAppDirPath, 'index.mjs');
const hydrateCoreIndexDtsFilePath = join(hydrateAppDirPath, 'index.d.ts');

const pkgJsonPath = join(hydrateAppDirPath, 'package.json');
const pkgJsonCode = getHydratePackageJson(
config,
hydrateCoreIndexPath,
hydrateCoreIndexPathESM,
hydrateCoreIndexDtsFilePath,
hydratePackageName,
);
Expand All @@ -62,28 +67,37 @@ const writeHydrateOutput = async (

const getHydratePackageJson = (
config: d.ValidatedConfig,
hydrateAppFilePath: string,
hydrateAppFilePathCJS: string,
hydrateAppFilePathESM: string,
hydrateDtsFilePath: string,
hydratePackageName: string,
) => {
const pkg: d.PackageJsonData = {
name: hydratePackageName,
description: `${config.namespace} component hydration app.`,
main: basename(hydrateAppFilePath),
main: basename(hydrateAppFilePathCJS),
types: basename(hydrateDtsFilePath),
exports: {
'.': {
require: `./${basename(hydrateAppFilePathCJS)}`,
import: `./${basename(hydrateAppFilePathESM)}`,
},
},
};
return JSON.stringify(pkg, null, 2);
};

const getHydratePackageName = async (config: d.ValidatedConfig, compilerCtx: d.CompilerCtx) => {
const directoryName = basename(config.rootDir);
try {
const rootPkgFilePath = join(config.rootDir, 'package.json');
const pkgStr = await compilerCtx.fs.readFile(rootPkgFilePath);
const pkgData = JSON.parse(pkgStr) as d.PackageJsonData;
return `${pkgData.name}/hydrate`;
const scope = pkgData.name || directoryName;
return `${scope}/hydrate`;
} catch (e) {}

return `${config.fsNamespace}/hydrate`;
return `${config.fsNamespace || directoryName}/hydrate`;
};

const copyHydrateRunnerDts = async (
Expand Down
26 changes: 7 additions & 19 deletions src/compiler/prerender/prerender-template-html.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createDocument, serializeNodeToHtml } from '@stencil/core/mock-doc';
import { catchError, isFunction, isPromise, isString } from '@utils';
import { catchError, isFunction, isString } from '@utils';

import type * as d from '../../declarations';
import {
Expand All @@ -21,18 +21,14 @@ export const generateTemplateHtml = async (
manager: d.PrerenderManager,
) => {
try {
if (!isString(srcIndexHtmlPath)) {
if (!isString(srcIndexHtmlPath) && outputTarget.indexHtml) {
srcIndexHtmlPath = outputTarget.indexHtml;
}

let templateHtml: string;
if (isFunction(prerenderConfig.loadTemplate)) {
const loadTemplateResult = prerenderConfig.loadTemplate(srcIndexHtmlPath);
if (isPromise(loadTemplateResult)) {
templateHtml = await loadTemplateResult;
} else {
templateHtml = loadTemplateResult;
}
templateHtml = await loadTemplateResult;
} else {
templateHtml = await config.sys.readFile(srcIndexHtmlPath);
}
Expand All @@ -56,7 +52,7 @@ export const generateTemplateHtml = async (

doc.documentElement.classList.add('hydrated');

if (hydrateOpts.inlineExternalStyleSheets && !isDebug) {
if (hydrateOpts.inlineExternalStyleSheets && !isDebug && outputTarget.appDir) {
try {
await inlineExternalStyleSheets(config.sys, outputTarget.appDir, doc);
} catch (e: any) {
Expand All @@ -72,7 +68,7 @@ export const generateTemplateHtml = async (
}
}

if (hydrateOpts.minifyStyleElements && !isDebug) {
if (hydrateOpts.minifyStyleElements && !isDebug && outputTarget.baseUrl) {
try {
const baseUrl = new URL(outputTarget.baseUrl, manager.devServerHostUrl);
await minifyStyleElements(config.sys, outputTarget.appDir, doc, baseUrl, true);
Expand All @@ -83,22 +79,14 @@ export const generateTemplateHtml = async (

if (isFunction(prerenderConfig.beforeSerializeTemplate)) {
const beforeSerializeResults = prerenderConfig.beforeSerializeTemplate(doc);
if (isPromise(beforeSerializeResults)) {
doc = await beforeSerializeResults;
} else {
doc = beforeSerializeResults;
}
doc = await beforeSerializeResults;
}

let html = serializeNodeToHtml(doc);

if (isFunction(prerenderConfig.afterSerializeTemplate)) {
const afterSerializeResults = prerenderConfig.afterSerializeTemplate(html);
if (isPromise(afterSerializeResults)) {
html = await afterSerializeResults;
} else {
html = afterSerializeResults;
}
html = await afterSerializeResults;
}

return {
Expand Down
18 changes: 6 additions & 12 deletions src/compiler/prerender/prerender-worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { catchError, isFunction, isPromise, isRootPath, join, normalizePath } from '@utils';
import { catchError, isFunction, isRootPath, join, normalizePath } from '@utils';
import { dirname } from 'path';

import type * as d from '../../declarations';
Expand Down Expand Up @@ -70,18 +70,15 @@ export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d

if (typeof prerenderConfig.beforeHydrate === 'function') {
try {
const rtn = prerenderConfig.beforeHydrate(doc, url);
if (isPromise(rtn)) {
await rtn;
}
await prerenderConfig.beforeHydrate(doc, url);
} catch (e: any) {
catchError(results.diagnostics, e);
}
}

// parse the html to dom nodes, hydrate the components, then
// serialize the hydrated dom nodes back to into html
const hydrateResults = (await hydrateApp.hydrateDocument(doc, hydrateOpts)) as d.HydrateResults;
const hydrateResults: d.HydrateResults = await hydrateApp.hydrateDocument(doc, hydrateOpts);
results.diagnostics.push(...hydrateResults.diagnostics);

if (typeof prerenderConfig.filePath === 'function') {
Expand All @@ -104,7 +101,7 @@ export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d
}

if (hydrateOpts.addModulePreloads) {
if (!prerenderRequest.isDebug) {
if (!prerenderRequest.isDebug && componentGraph) {
addModulePreloads(doc, hydrateOpts, hydrateResults, componentGraph);
}
} else {
Expand Down Expand Up @@ -148,10 +145,7 @@ export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d

if (typeof prerenderConfig.afterHydrate === 'function') {
try {
const rtn = prerenderConfig.afterHydrate(doc, url, results);
if (isPromise(rtn)) {
await rtn;
}
await prerenderConfig.afterHydrate(doc, url, results);
} catch (e: any) {
catchError(results.diagnostics, e);
}
Expand All @@ -164,7 +158,7 @@ export const prerenderWorker = async (sys: d.CompilerSystem, prerenderRequest: d
return results;
}

const html = hydrateApp.serializeDocumentToString(doc, hydrateOpts);
const html = await hydrateApp.serializeDocumentToString(doc, hydrateOpts);

prerenderEnsureDir(sys, prerenderCtx, results.filePath);

Expand Down
23 changes: 12 additions & 11 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1137,30 +1137,30 @@ export interface HostElement extends HTMLElement {

['s-p']?: Promise<void>[];

componentOnReady?: () => Promise<this>;
componentOnReady?: () => Promise<this> | undefined;
}

export interface HydrateResults {
buildId: string;
diagnostics: Diagnostic[];
url: string;
host: string;
hostname: string;
href: string;
port: string;
pathname: string;
search: string;
hash: string;
html: string;
host: string | null;
hostname: string | null;
href: string | null;
port: string | null;
pathname: string | null;
search: string | null;
hash: string | null;
html: string | null;
components: HydrateComponent[];
anchors: HydrateAnchorElement[];
imgs: HydrateImgElement[];
scripts: HydrateScriptElement[];
styles: HydrateStyleElement[];
staticData: HydrateStaticData[];
title: string;
title: string | null;
hydratedCount: number;
httpStatus: number;
httpStatus: number | null;
}

export interface HydrateComponent {
Expand Down Expand Up @@ -1960,6 +1960,7 @@ export interface PackageJsonData {
name?: string;
version?: string;
main?: string;
exports?: { [key: string]: string | { [key: string]: string } };
description?: string;
bin?: { [key: string]: string };
browser?: string;
Expand Down
13 changes: 13 additions & 0 deletions src/declarations/stencil-public-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,19 @@ export interface SerializeDocumentOptions extends HydrateDocumentOptions {
* Remove HTML comments. Defaults to `true`.
*/
removeHtmlComments?: boolean;
/**
* If set to `false` Stencil will ignore the fact that a component has a `shadow: true`
* flag and serializes it as a scoped component. If set to `true` the component will
* be rendered within a Declarative Shadow DOM.
* @default false
*/
serializeShadowRoot?: boolean;
/**
* The `fullDocument` flag determines the format of the rendered output. Set it to true to
* generate a complete HTML document, or false to render only the component.
* @default true
*/
fullDocument?: boolean;
}

export interface HydrateFactoryOptions extends SerializeDocumentOptions {
Expand Down
12 changes: 5 additions & 7 deletions src/hydrate/platform/h-async.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { consoleDevError } from '@platform';
import { h } from '@runtime';
import { isPromise } from '@utils';

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

export const hAsync = (nodeName: any, vnodeData: any, ...children: d.ChildType[]) => {
if (Array.isArray(children) && children.length > 0) {
// only return a promise if we have to
const flatChildren = children.flat(Infinity);
if (flatChildren.some(isPromise)) {
// has children and at least one of them is async
// wait on all of them to be resolved
// has children and at least one of them is async
// wait on all of them to be resolved
if (flatChildren.some((child) => child instanceof Promise)) {
return Promise.all(flatChildren)
.then((resolvedChildren) => {
return h(nodeName, vnodeData, ...resolvedChildren);
Expand All @@ -20,9 +19,8 @@ export const hAsync = (nodeName: any, vnodeData: any, ...children: d.ChildType[]
return h(nodeName, vnodeData);
});
}

// no async children, return sync
return h(nodeName, vnodeData, ...children);
// no async children, just return sync
return h(nodeName, vnodeData, ...flatChildren);
}

// no children, return sync
Expand Down
Loading

0 comments on commit c837063

Please sign in to comment.