chore: support for FF/Safari by loading needed polyfills

This commit is contained in:
Thomas Allmer
2022-03-10 00:19:29 +01:00
parent e17671c448
commit fa41908d0e
15 changed files with 311 additions and 85 deletions

View File

@@ -1,6 +1,5 @@
## Features
- ssr render can just be a string concat
- recursive rendering of lit / html / and markdown
- "import" markdown with frontmatter
- mdjs update to unified v10 AND go esm only (only cjs pkg we have now)

View File

@@ -1,28 +1,15 @@
import { Readable } from 'stream';
import { render } from '@lit-labs/ssr/lib/render-with-global-dom-shim.js';
/**
* @param {Readable} stream
* @returns {Promise<string>}
*/
function streamToString(stream) {
/** @type {Uint8Array[]} */
const chunks = [];
return new Promise((resolve, reject) => {
stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
stream.on('error', err => reject(err));
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
}
/**
* @param {string} templateResult
* @returns {Promise<string>}
*/
export async function litServerRender(templateResult) {
const ssrResult = render(templateResult);
const stream = Readable.from(ssrResult);
let outputString = await streamToString(stream);
let outputString = '';
for (const chunk of ssrResult) {
outputString += chunk;
}
// TODO: lit-ssr has a bug where it does not handle dynamic content in <title>, <body>, <html>, ...
// https://github.com/lit/lit/issues/2441
// we are now writing <body-server-only> in the template and before we write the file we remove the `-server-only` part
@@ -32,7 +19,7 @@ export async function litServerRender(templateResult) {
outputString = outputString.replace(/<(.*?)-server-only/g, '<$1');
// TODO: remove workaround once https://github.com/lit/lit/issues/2470 is fixed
// we remove <?> from the source code as it's invalid html and sax-wasm can not handle it
// we remove <?> from the source code as it's html that requires error handling and some versions of sax-wasm can not handle it
outputString = outputString.replace(/<\?>/g, '<!---->');
return outputString;

View File

@@ -53,6 +53,7 @@
"@rocket/cli": "^0.20.0-alpha.10",
"@rocket/drawer": "^0.1.3",
"@rocket/engine": "^0.1.0-alpha.10",
"@webcomponents/template-shadowroot": "^0.1.0",
"lit": "^2.0.0",
"workbox-window": "^6.1.5"
},

View File

@@ -54,6 +54,10 @@ export class LayoutSidebar extends Layout {
*/
options = {
...this.options,
bodyClasses: {
...this.options.bodyClasses,
'dsd-pending': true,
},
siteName: 'Rocket',
logoSrc: '/icon.svg',
logoAlt: 'Rocket Logo',
@@ -146,6 +150,14 @@ export class LayoutSidebar extends Layout {
<link rel="stylesheet" href="resolve:@rocket/launch/css/style.css" />
`,
head__50: html`
<style>
body[dsd-pending] {
display: none;
}
</style>
`,
header__10: html`
<a class="logo-link" href="/">
<img src="/icon.svg" alt="${this.options.logoAlt}" />
@@ -194,6 +206,14 @@ export class LayoutSidebar extends Layout {
sidebar__100: data =>
this.options.pageTree.renderMenu(new IndexMenu(), data.sourceRelativeFilePath),
top__10: () => html`
<script>
if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
document.body.removeAttribute('dsd-pending');
}
</script>
`,
content__600: data => html`
<div class="content-previous-next">
${this.options.pageTree.renderMenu(new PreviousMenu(), data.sourceRelativeFilePath)}
@@ -250,6 +270,20 @@ export class LayoutSidebar extends Layout {
}
return nothing;
},
bottom__70: () => html`
<script type="module">
(async () => {
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
const { hydrateShadowRoots } = await import(
'@webcomponents/template-shadowroot/template-shadowroot.js'
);
hydrateShadowRoots(document.body);
document.body.removeAttribute('dsd-pending');
}
})();
</script>
`,
};
}

View File

@@ -21,6 +21,8 @@ Web component only live within the html body. For content within the head or a f
Rocket uses [lit](https://lit.dev) for layouts so using it for components as well makes it easy to switch back and forth.
However any web component code/library should work.
## Defining a component
A lit component typically looks like this:
👉 `site/src/components/TwoColumns.js`
@@ -56,6 +58,30 @@ import { TwoColumns } from './TwoColumns.js';
customElements.define('two-columns', TwoColumns);
```
With the element being define you can now use it in markdown, html or JavaScript.
```md
# Hello World
<two-columns><div slot="left">
This is **left** content
</div><div slot="right">
And here comes the **right** content
</div></two-columns>
```
<inline-notification>
Note the empty lines between html & markdown. They are necessary as this is how the markdown parser separates unprocessed html from markdown.
</inline-notification>
## Loading Components
For each component you then need to decide if you want to render server or client side.
You decide this by where you are executing the element definition.

View File

@@ -136,11 +136,12 @@ const layoutB =
Partial html is not supported in [lit](http://lit.dev) as it uses the browser build in html parser which try to "auto correct" your html by closing tags.
e.g. this
<!-- prettier-ignore-start -->
```js
const mainStart = html`
<main>
<p>Always at the top</p>
</main>
`;
const mainEnd = html`
@@ -154,15 +155,19 @@ const layout = () => html`
`;
```
<!-- prettier-ignore-end -->
will result in
```html
<main>
<p>Always at the top</p>
<p>Hello World</p>
</main>
<p>Hello World</p>
```
Which is most likely not what you want.
</inline-notification>
## Class based layouts

View File

@@ -8,11 +8,11 @@ export { html, layout, setupUnifiedPlugins };
# Routing
Rocket uses **file-based routing** to generate your build URLs based on the file layout of your project `docs` directory. When a file is added to the `docs` directory of your project, it is automatically available as a route based on its filename.
Rocket uses **file-based routing** to generate your build URLs based on the file layout of your project `site/pages` directory. When a file is added to the `site/pages` directory of your project, it is automatically available as a route based on its filename.
## Static routes
Rocket Pages as Markdown (`rocket.md`), HTML (`rocket.html`) and JavaScript (`rocket.js`) in the `docs` directory become pages on your website. Each pages route is decided based on its filename and path within the `docs` directory. This means that there is no separate "routing config" to maintain in an Rocket project.
Rocket Pages are Markdown (`rocket.md`), HTML (`rocket.html`) and JavaScript (`rocket.js`) in the `site/pages` directory become pages on your website. Each pages route is decided based on its filename and path within the `site/pages` directory. This means that there is no separate "routing config" to maintain in an Rocket project.
```bash
# Example: Static routes

View File

@@ -110,7 +110,7 @@ The url `mysite/basics/` now works just fine but in a navigation most people do
In order to not confuse people if `Basics` is clickable or not - of if there is valuable content or not you can say this page is not clickable.
You can do so by
You can do so by exporting a flag in `20--basics/index.rocket.js` like so:
```js
export const menuNoLink = true;

View File

@@ -4,8 +4,6 @@ export const sourceRelativeFilePath = '10--docs/20--basics/80--hydration.rocket.
import { html, layout, setupUnifiedPlugins } from '../../recursive.data.js';
export { html, layout, setupUnifiedPlugins };
/* END - Rocket auto generated - do not touch */
export const menuOrder = 12;
```
# Hydration

View File

@@ -4,15 +4,24 @@ export const sourceRelativeFilePath = '10--docs/20--basics/90--assets.rocket.md'
import { html, layout, setupUnifiedPlugins } from '../../recursive.data.js';
export { html, layout, setupUnifiedPlugins };
/* END - Rocket auto generated - do not touch */
import '@rocket/launch/inline-notification/define';
```
# Assets
One of the best aspects of Rocket is it is *pretty much* HTML, CSS, Markdown, and JS. This means using assets (images, videos, CSS files, GIFs, etc.) is as easy as it normally is in the "vanilla" context of each of those languages.
One of the best aspects of Rocket is it is _pretty much_ HTML, CSS, Markdown, and JS. This means using assets (images, videos, CSS files, GIFs, etc.) is as easy as it normally is in the "vanilla" context of each of those languages.
<inline-notification>
During development Rocket does not move any of your assets around but it changes the paths in the output HTML to reference to the original assets location.
This means that relative paths within assets (like css, js) will always work.
</inline-notification>
## Source Assets
### HTML
### HTML
```html
<img src="../path/to/your/image.jpg" alt="My Image" />
@@ -37,24 +46,57 @@ One of the best aspects of Rocket is it is *pretty much* HTML, CSS, Markdown, an
This would be similar to HTML since it would be inside of an html template literal
```js
export default () => html`
<img src="../path/to/your/image.jpg" alt="My Image" />
`
export default () => html`<img src="../path/to/your/image.jpg" alt="My Image" />`;
```
## Static Assets
Oftentimes, you have files such as robots.txt, .htaccess (redirects), favicons, etc. that do not need to be in your `src/` directory. For these types of files that simply need to be copied to the built site you will want to place them in your `site/public/` folder. Anything is this folder will be copied over into the built site completely untouched.
**Note:** Since these files are simply copied over during the build process, anything you place in the `src/public/` directory will not be optimized, bundled, or minified by Rocket.
**Note:** Since these files are simply copied over during the build process, anything you place in the `site/public/` directory will not be optimized, bundled, or minified by Rocket.
## The :resolve Function
## The `:resolve` Function
Rocket includes a special `:resolve` function that will resolve npm imports for assets.
This means any npm package, script, image, video, css file, etc. can be used as the value of the src attribute.
Here is an example of css file being imported using the `:resolve` function
```css
```html
<img src="resolve:@rocket/engine/assets/logo.svg" alt="Rocket Logo" />
<link rel="stylesheet" href="resolve:@example/blog/styles/blog.css" />
```
```
## Node exports
If you package is `"type": "module"` then you can also add exports.
👉 `package.json`
```json
{
"name": "my-package",
"type": "module",
"exports": {
".": "./src/index.js",
"./assets/*": "./src/assets/*"
}
}
```
Once you have an export it also enables to "self" reference your package.
Combine this with the `:resolve` function means that you can reference assets always with the same path.
Completely unrelated to the path of the file itself.
👉 `site/pages/index.rocket.md`
```md
![Logo](resolve:my-package/assets/logo.svg)
```
👉 `site/pages/about/index.rocket.md`
```md
![Logo](resolve:my-package/assets/logo.svg)
```

View File

@@ -1,6 +1,6 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--docs/20--basics/90--configuration.rocket.md';
export const sourceRelativeFilePath = '10--docs/20--basics/95--configuration.rocket.md';
import { html, layout, setupUnifiedPlugins } from '../../recursive.data.js';
export { html, layout, setupUnifiedPlugins };
/* END - Rocket auto generated - do not touch */

View File

@@ -1,11 +0,0 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--docs/30--guides/10--overview.rocket.md';
import { html, layout, setupUnifiedPlugins } from '../../recursive.data.js';
export { html, layout, setupUnifiedPlugins };
/* END - Rocket auto generated - do not touch */
```
# Overview
A few things are usually needed before going live "for real".

View File

@@ -0,0 +1,81 @@
```js server
/* START - Rocket auto generated - do not touch */
export const sourceRelativeFilePath = '10--docs/30--guides/60--cross-browser.rocket.md';
import { html, layout, setupUnifiedPlugins } from '../../recursive.data.js';
export { html, layout, setupUnifiedPlugins };
/* END - Rocket auto generated - do not touch */
import { inlineFile } from '@rocket/engine';
import { createRequire } from 'module';
import path from 'path';
const { resolve } = createRequire(new URL('.', import.meta.url));
```
# Cross Browser
If we are using server side rendering for any of our web components then you are using [declarative shadow dom](https://web.dev/declarative-shadow-dom/).
As it's not yet fully supported by all browsers, we may want to use a polyfill to make it work.
If we are using an existing theme/preset then it's most likely taken care of for us.
If not then we can add this style tag and two script tags to our layout.
```bash
npm i -D @webcomponents/template-shadowroot
```
```html
<!DOCTYPE html>
<html>
<head>
<!-- On browsers that don't yet support native declarative shadow DOM, a
paint can occur after some or all pre-rendered HTML has been parsed,
but before the declarative shadow DOM polyfill has taken effect. This
paint is undesirable because it won't include any component shadow DOM.
To prevent layout shifts that can result from this render, we use a
"dsd-pending" attribute to ensure we only paint after we know
shadow DOM is active. -->
<style>
body[dsd-pending] {
display: none;
}
</style>
</head>
<body dsd-pending>
<script>
if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
// This browser has native declarative shadow DOM support, so we can
// allow painting immediately.
document.body.removeAttribute('dsd-pending');
}
</script>
<!-- The pre-rendered web components -->
<script type="module">
(async () => {
// Check if we require the declarative shadow DOM polyfill. As of
// February 2022, Chrome and Edge have native support, but Firefox
// and Safari don't yet.
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
// Fetch the declarative shadow DOM polyfill.
const { hydrateShadowRoots } = await import(
'@webcomponents/template-shadowroot/template-shadowroot.js'
);
// Apply the polyfill. This is a one-shot operation, so it is important
// it happens after all HTML has been parsed.
hydrateShadowRoots(document.body);
// At this point, browsers without native declarative shadow DOM
// support can paint the initial state of your components!
document.body.removeAttribute('dsd-pending');
}
})();
</script>
</body>
</html>
```
This loading strategy is based on the [eleventy-plugin-lit](https://github.com/lit/lit/tree/main/packages/labs/eleventy-plugin-lit#bootup) bootup strategy.

View File

@@ -291,6 +291,16 @@
"text": "Components",
"id": "components",
"level": 1
},
{
"text": "Defining a component",
"id": "defining-a-component",
"level": 2
},
{
"text": "Loading Components",
"id": "loading-components",
"level": 2
}
],
"name": "Components",
@@ -411,6 +421,23 @@
"sourceRelativeFilePath": "10--docs/20--basics/70--navigation.rocket.md",
"level": 3
},
{
"title": "Hydration | Rocket",
"h1": "Hydration",
"headlinesWithId": [
{
"text": "Hydration",
"id": "hydration",
"level": 1
}
],
"name": "Hydration",
"menuLinkText": "Hydration",
"url": "/docs/basics/hydration/",
"outputRelativeFilePath": "docs/basics/hydration/index.html",
"sourceRelativeFilePath": "10--docs/20--basics/80--hydration.rocket.md",
"level": 3
},
{
"title": "Assets | Rocket",
"h1": "Assets",
@@ -419,6 +446,46 @@
"text": "Assets",
"id": "assets",
"level": 1
},
{
"text": "Source Assets",
"id": "source-assets",
"level": 2
},
{
"text": "HTML",
"id": "html",
"level": 3
},
{
"text": "CSS",
"id": "css",
"level": 3
},
{
"text": "Markdown",
"id": "markdown",
"level": 3
},
{
"text": "Javascript",
"id": "javascript",
"level": 3
},
{
"text": "Static Assets",
"id": "static-assets",
"level": 2
},
{
"text": "The Function",
"id": "the-resolve-function",
"level": 2
},
{
"text": "Node exports",
"id": "node-exports",
"level": 2
}
],
"name": "Assets",
@@ -442,6 +509,11 @@
"id": "adjusting-the-inputoutput-directories",
"level": 2
},
{
"text": "Adding an API proxy",
"id": "adding-an-api-proxy",
"level": 2
},
{
"text": "Adding Rollup Plugins",
"id": "adding-rollup-plugins",
@@ -458,8 +530,13 @@
"level": 2
},
{
"text": "Rollup",
"id": "rollup",
"text": "DevServerOptions (@web/dev-server)",
"id": "devserveroptions-webdev-server",
"level": 3
},
{
"text": "BuildOptions (rollup)",
"id": "buildoptions-rollup",
"level": 3
}
],
@@ -467,26 +544,8 @@
"menuLinkText": "Configuration",
"url": "/docs/basics/configuration/",
"outputRelativeFilePath": "docs/basics/configuration/index.html",
"sourceRelativeFilePath": "10--docs/20--basics/90--configuration.rocket.md",
"sourceRelativeFilePath": "10--docs/20--basics/95--configuration.rocket.md",
"level": 3
},
{
"title": "Hydration | Rocket",
"h1": "Hydration",
"headlinesWithId": [
{
"text": "Hydration",
"id": "hydration",
"level": 1
}
],
"name": "Hydration",
"menuLinkText": "Hydration",
"url": "/docs/basics/hydration/",
"outputRelativeFilePath": "docs/basics/hydration/index.html",
"sourceRelativeFilePath": "10--docs/20--basics/80--hydration.rocket.md",
"level": 3,
"menuOrder": 12
}
]
},
@@ -500,23 +559,6 @@
"sourceRelativeFilePath": "10--docs/30--guides/index.rocket.js",
"level": 2,
"children": [
{
"title": "Overview | Rocket",
"h1": "Overview",
"headlinesWithId": [
{
"text": "Overview",
"id": "overview",
"level": 1
}
],
"name": "Overview",
"menuLinkText": "Overview",
"url": "/docs/guides/overview/",
"outputRelativeFilePath": "docs/guides/overview/index.html",
"sourceRelativeFilePath": "10--docs/30--guides/10--overview.rocket.md",
"level": 3
},
{
"title": "Social Media | Rocket",
"h1": "Social Media",
@@ -659,6 +701,23 @@
"outputRelativeFilePath": "docs/guides/go-live/index.html",
"sourceRelativeFilePath": "10--docs/30--guides/50--go-live.rocket.md",
"level": 3
},
{
"title": "Cross Browser | Rocket",
"h1": "Cross Browser",
"headlinesWithId": [
{
"text": "Cross Browser",
"id": "cross-browser",
"level": 1
}
],
"name": "Cross Browser",
"menuLinkText": "Cross Browser",
"url": "/docs/guides/cross-browser/",
"outputRelativeFilePath": "docs/guides/cross-browser/index.html",
"sourceRelativeFilePath": "10--docs/30--guides/60--cross-browser.rocket.md",
"level": 3
}
]
}

View File

@@ -2724,6 +2724,11 @@
resolved "https://registry.yarnpkg.com/@webcomponents/scoped-custom-element-registry/-/scoped-custom-element-registry-0.0.3.tgz#774591a886b0b0e4914717273ba53fd8d5657522"
integrity sha512-lpSzgDCGbM99dytb3+J3Suo4+Bk1E13MPnWB42JK8GwxSAxFz+tC7TTv2hhDSIE2IirGNKNKCf3m08ecu6eAsQ==
"@webcomponents/template-shadowroot@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@webcomponents/template-shadowroot/-/template-shadowroot-0.1.0.tgz#adb3438d0d9a18e8fced08abc253f56b7eadab00"
integrity sha512-ry84Vft6xtRBbd4M/ptRodbOLodV5AD15TYhyRghCRgIcJJKmYmJ2v2BaaWxygENwh6Uq3zTfGPmlckKT/GXsQ==
"@webcomponents/webcomponentsjs@^2.5.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.5.0.tgz#61b27785a6ad5bfd68fa018201fe418b118cb38d"