Skip to content

Commit

Permalink
feat(runtime): support declarative shadow DOM
Browse files Browse the repository at this point in the history
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
  • Loading branch information
christian-bromann committed Jun 12, 2024
1 parent 0a11aac commit 532557e
Show file tree
Hide file tree
Showing 54 changed files with 1,028 additions and 443 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
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 @@ -31,12 +31,14 @@ const writeHydrateOutput = async (
const hydrateAppDirPath = outputTarget.dir;

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 +64,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
20 changes: 4 additions & 16 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 Down Expand Up @@ -28,11 +28,7 @@ export const generateTemplateHtml = async (
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 Down Expand Up @@ -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
16 changes: 5 additions & 11 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 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
1 change: 1 addition & 0 deletions src/declarations/stencil-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,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 @@ -933,6 +933,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
6 changes: 3 additions & 3 deletions src/hydrate/platform/hydrate-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function hydrateApp(
let ranCompleted = false;

function hydratedComplete() {
global.clearTimeout(tmrId);
globalThis.clearTimeout(tmrId);
createdElements.clear();
connectedElements.clear();

Expand Down Expand Up @@ -91,7 +91,7 @@ export function hydrateApp(
registerHost(elm, Cstr.cmpMeta);

// proxy the host element with the component's metadata
proxyHostElement(elm, Cstr.cmpMeta);
proxyHostElement(elm, Cstr.cmpMeta, opts);
}
}
}
Expand Down Expand Up @@ -148,7 +148,7 @@ export function hydrateApp(
} as (typeof window)['document']['createElementNS'];

// ensure we use NodeJS's native setTimeout, not the mocked hydrate app scoped one
tmrId = global.setTimeout(timeoutExceeded, opts.timeout);
tmrId = globalThis.setTimeout(timeoutExceeded, opts.timeout);

plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', doc.baseURI).href;

Expand Down
3 changes: 2 additions & 1 deletion src/hydrate/platform/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BUILD } from '@app-data';
import { addHostEventListeners } from '@runtime';

import type * as d from '../../declarations';
Expand Down Expand Up @@ -120,7 +121,7 @@ export const setPlatformHelpers = (helpers: {
Object.assign(plt, helpers);
};

export const supportsShadow = false;
export const supportsShadow = BUILD.shadowDom;

export const supportsListenerOptions = false;

Expand Down
Loading

0 comments on commit 532557e

Please sign in to comment.