Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(runtime): support declarative shadow DOM #5792

Merged
merged 6 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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