Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: start moving scripts into manifest.json
Browse files Browse the repository at this point in the history
digitalsadhu committed Nov 13, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 4f86c0f commit add5179
Showing 9 changed files with 313 additions and 153 deletions.
27 changes: 25 additions & 2 deletions api/build.js
Original file line number Diff line number Diff line change
@@ -63,13 +63,13 @@ export async function build({ state, config, cwd = process.cwd() }) {
if (existsSync(CONTENT_SRC_FILEPATH)) {
writeFileSync(
CONTENT_ENTRY,
`import "@lit-labs/ssr-client/lit-element-hydrate-support.js";import Component from "${CONTENT_SRC_FILEPATH}";customElements.define("${NAME}-content",Component);`,
`import Component from "${CONTENT_SRC_FILEPATH}";customElements.define("${NAME}-content",Component);`,
);
}
if (existsSync(FALLBACK_SRC_FILEPATH)) {
writeFileSync(
FALLBACK_ENTRY,
`import "@lit-labs/ssr-client/lit-element-hydrate-support.js";import Component from "${FALLBACK_SRC_FILEPATH}";customElements.define("${NAME}-fallback",Component);`,
`import Component from "${FALLBACK_SRC_FILEPATH}";customElements.define("${NAME}-fallback",Component);`,
);
}

@@ -135,6 +135,29 @@ export async function build({ state, config, cwd = process.cwd() }) {
sourcemap: false,
});

// build hydration support script
await esbuild.build({
entryPoints: [

// TODO: can't use require.resolve here as it will resolve in a Node context and get the wrong script
// probably need to create a file that does import "@lit-labs/ssr-client/lit-element-hydrate-support.js";
// and use that as input to entryPoints
require.resolve(
'@lit-labs/ssr-client/lit-element-hydrate-support.js',
),
],
bundle: true,
format: 'esm',
outfile: join(CLIENT_OUTDIR, 'lit-element-hydrate-support.js'),
minify: true,
target: ['es2017'],
legalComments: `none`,
sourcemap: false,
});

// TODO: Properly build lazy loaded scripts for production mode
// with a lazy load wrapper addEventListener('load',()=>import('${url}'))

// Run code through esbuild first (to apply plugins and strip types) but don't bundle or minify
// use esbuild to resolve imports and then run a build with plugins
await esbuild.build({
131 changes: 50 additions & 81 deletions lib/plugin.js
Original file line number Diff line number Diff line change
@@ -9,22 +9,20 @@ import assetsPn from '../plugins/assets.js';
import compressionPn from '../plugins/compression.js';
import errorsPn from '../plugins/errors.js';
import exceptionsPn from '../plugins/exceptions.js';
import hydratePn from '../plugins/hydrate.js';
import importElementPn from '../plugins/import-element.js';
import liveReloadPn from '../plugins/live-reload.js';
import localePn from '../plugins/locale.js';
import metricsPn from '../plugins/metrics.js';
import podletPn from '../plugins/podlet.js';
import scriptPn from '../plugins/script.js';
import timingPn from '../plugins/timing.js';
import validationPn from '../plugins/validation.js';
import lazyPn from '../plugins/lazy.js';
import scriptsPn from '../plugins/scripts.js';
import ssrPn from '../plugins/ssr.js';
import csrPn from '../plugins/csr.js';
import documentPn from '../plugins/document.js';
import docsPn from '../plugins/docs.js';
import bundlerPn from '../plugins/bundler.js';
import modeSupportPn from '../plugins/mode-support.js';
import hydrateSupportPn from '../plugins/hydrate-support.js';
import { isAbsoluteURL, joinURLPathSegments } from './utils.js';

const defaults = {
@@ -35,7 +33,7 @@ const defaults = {

/**
* create an intersection type out of fastify instance and its decorated properties
* @typedef {import("fastify").FastifyInstance & { podlet: any, metrics: any, schemas: any, importElement: function, readTranslations: function, script: function, hydrate: function, ssr: function, csr: function }} FastifyInstance
* @typedef {import("fastify").FastifyInstance & { podlet: any, metrics: any, schemas: any, importElement: function, readTranslations: function, script: function, serverRender: function, serverRenderAndHydrate: function, clientRender: function }} FastifyInstance
*/

/**
@@ -112,6 +110,12 @@ export default fp(
fallback,
development,
});
await f.register(hydrateSupportPn, {
enabled: mode === 'hydrate',
base,
development,
prefix,
});
await f.register(lazyPn, { enabled: lazy, base, development, prefix });
await f.register(scriptsPn, {
enabled: scripts,
@@ -132,19 +136,12 @@ export default fp(
await f.register(assetsPn, { base, cwd });
await f.register(bundlerPn, { cwd, development, plugins });
await f.register(exceptionsPn, { grace, development });
await f.register(hydratePn, {
appName: name,
base: assetBase,
development,
prefix,
});
await f.register(csrPn, {
await f.register(modeSupportPn, {
appName: name,
base: assetBase,
development,
prefix,
});
await f.register(ssrPn, { appName: name, base, prefix, development });
await f.register(importElementPn, {
appName: name,
development,
@@ -153,7 +150,6 @@ export default fp(
});
await f.register(localePn, { locale, cwd });
await f.register(metricsPn);
await f.register(scriptPn, { development });
await f.register(timingPn, {
timeAllRoutes,
groupStatusCodes,
@@ -175,58 +171,43 @@ export default fp(
// routes
if (existsSync(contentFilePath)) {
const tag = unsafeStatic(`${name}-content`);

// mount content route
f.get(f.podlet.content(), async (request, reply) => {
try {
const contextConfig = /** @type {FastifyContextConfig} */ (
reply.context.config
);
contextConfig.timing = true;

if (mode === 'ssr-only' || mode === 'hydrate') {
// import server side component
await f.importElement(contentFilePath);
}

// use developer provided content state function to generate initial content state
const initialState = JSON.stringify(
// @ts-ignore
(await contentStateFn(request, reply.app.podium.context)) || '',
);

const messages = await f.readTranslations();

const translations = messages ? JSON.stringify(messages) : '';

// includes ${null} hack for SSR. See https://github.com/lit/lit/issues/2246
const template = html`
<${tag} version="${version}" locale='${locale}' translations='${translations}'
initial-state='${initialState}'></${tag}>${null} `;
const hydrateSupport =
mode === 'hydrate'
? f.script(
joinURLPathSegments(
prefix,
'/_/dynamic/modules/@lit-labs/ssr-client/lit-element-hydrate-support.js',
),
{ dev: true },
)
: '';
let markup;
if (mode === 'ssr-only') {
markup = f.ssr('content', template);
} else if (mode === 'csr-only') {
markup = f.csr(
'content',
`<${name}-content version="${version}" locale='${locale}' translations='${translations}' initial-state='${initialState}'></${name}-content>`,
);
} else {
markup = f.hydrate('content', template);
}
const template = html`<${tag} version="${version}" locale='${locale}' translations='${translations}' initial-state='${initialState}'></${tag}>${null} `;

reply
.type('text/html; charset=utf-8')
.send(`${hydrateSupport}${markup}`);
reply.type('text/html; charset=utf-8');

return reply;
if (mode === 'csr-only') {
return reply.send(f.clientRender('content', template));
} else {
// import the custom element for server side use
await f.importElement(contentFilePath);

if (mode === 'ssr-only') {
return reply.send(f.serverRender('content', template));
} else {
return reply.send(
f.serverRenderAndHydrate('content', template),
);
}
}
} catch (err) {
f.log.error(err);
return reply;
@@ -236,56 +217,44 @@ export default fp(

if (existsSync(fallbackFilePath)) {
const tag = unsafeStatic(`${name}-fallback`);

// mount fallback route
f.get(f.podlet.fallback(), async (request, reply) => {
try {
const contextConfig = /** @type {FastifyContextConfig} */ (
reply.context.config
);
contextConfig.timing = true;

if (mode === 'ssr-only' || mode === 'hydrate') {
// import server side component
await f.importElement(fallbackFilePath);
}

// use developer provided fallback state function to generate initial fallback state
const initialState = JSON.stringify(
// @ts-ignore
(await fallbackStateFn(request, reply.app.podium.context)) ||
'',
);

const messages = await f.readTranslations();

const translations = messages ? JSON.stringify(messages) : '';
const template = html`
<${tag} version="${version}" locale='${locale}' translations='${translations}'
initial-state='${initialState}'></${tag}>${null} `;
const hydrateSupport =
mode === 'hydrate'
? f.script(
joinURLPathSegments(
prefix,
'/_/dynamic/modules/@lit-labs/ssr-client/lit-element-hydrate-support.js',
),
{ dev: true },
)
: '';
let markup;
if (mode !== 'ssr-only') {
markup = f.ssr('fallback', template);
} else if (mode === 'csr-only') {
markup = f.csr(
'fallback',
`<${name}-fallback version="${version}" locale='${locale}' translations='${translations}' initial-state='${initialState}'></${name}-fallback>`,
);

// includes ${null} hack for SSR. See https://github.com/lit/lit/issues/2246
const template = html`<${tag} version="${version}" locale='${locale}' translations='${translations}' initial-state='${initialState}'></${tag}>${null} `;

reply.type('text/html; charset=utf-8');

if (mode === 'csr-only') {
return reply.send(f.clientRender('fallback', template));
} else {
markup = f.hydrate('fallback', template);
}
reply
.type('text/html; charset=utf-8')
.send(`${hydrateSupport}${markup}`);
// import the custom element for server side use
await f.importElement(fallbackFilePath);

return reply;
if (mode === 'ssr-only') {
return reply.send(f.serverRender('fallback', template));
} else {
return reply.send(
f.serverRenderAndHydrate('fallback', template),
);
}
}
} catch (err) {
f.log.error(err);
return reply;
20 changes: 20 additions & 0 deletions plugins/bundler.js
Original file line number Diff line number Diff line change
@@ -60,6 +60,26 @@ export default fp(
},
);

// TODO: add a lazy load wrapper endpoint that wraps the requested file in
// addEventListener('load',()=>import('${url}'))
// fastify.get(
// '/_/dynamic/files/lazyload/:file.js',

// TODO: add a custom element endpoint that takes the path to a custom element and wraps it in a definiton
fastify.get(
'/_/dynamic/element/:type/:name',
async (/** @type {Request} */ request, reply) => {
// @ts-ignore
const { type, name } = request.params;

reply.type('application/javascript').send(`
import El from '/_/dynamic/files/${type}.js';
customElements.define("${name}-${type}",El);
`);
return reply;
},
);

fastify.get(
'/_/dynamic/files/:file.js',
async (/** @type {Request} */ request, reply) => {
19 changes: 19 additions & 0 deletions plugins/hydrate-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import fp from 'fastify-plugin';
import { joinURLPathSegments } from '../lib/utils.js';

export default fp(async (fastify, { enabled, base, development, prefix }) => {
// inject live reload when in dev mode
if (enabled) {
fastify.log.debug('Lit hydrate support enabled');

const url = development
? joinURLPathSegments(
prefix,
'/_/dynamic/modules/@lit-labs/ssr-client/lit-element-hydrate-support.js',
)
: joinURLPathSegments(base, `/client/lit-element-hydrate-support.js`);

// @ts-ignore
fastify.scriptsList.push({ value: url, type: 'module' });
}
});
59 changes: 33 additions & 26 deletions plugins/lazy.js
Original file line number Diff line number Diff line change
@@ -5,33 +5,40 @@ export default fp(
async (fastify, { enabled, prefix = '/', base, development = false }) => {
// inject live reload when in dev mode
if (enabled) {
fastify.addHook(
'onSend',
(request, reply, /** @type {string} */ payload, done) => {
let newPayload = payload;
const contentType = reply.getHeader('content-type') || '';
if (typeof contentType === 'string') {
// only inject lazy if the content type is html
if (contentType.includes('html')) {
// if there is a document, inject before closing body
const url = development
? joinURLPathSegments(prefix, `/_/dynamic/files/lazy.js`)
: joinURLPathSegments(base, `/client/lazy.js`);
const url = development
? joinURLPathSegments(prefix, `/_/dynamic/files/lazyload/lazy.js`)
: joinURLPathSegments(base, `/client/lazy.js`);

if (payload.includes('</body>')) {
newPayload = payload.replace(
'</body>',
`<script type="module">addEventListener('load',()=>import('${url}'))</script></body>`,
);
} else {
// if no document, inject at the end of the payload
newPayload = `${payload}<script type="module">addEventListener('load',()=>import('${url}'))</script>`;
}
}
}
done(null, newPayload);
},
);
// @ts-ignore
fastify.scriptsList.push({ value: url, type: 'module' });

// fastify.addHook(
// 'onSend',
// (request, reply, /** @type {string} */ payload, done) => {
// let newPayload = payload;
// const contentType = reply.getHeader('content-type') || '';
// if (typeof contentType === 'string') {
// // only inject lazy if the content type is html
// if (contentType.includes('html')) {
// // if there is a document, inject before closing body
// const url = development
// ? joinURLPathSegments(prefix, `/_/dynamic/files/lazy.js`)
// : joinURLPathSegments(base, `/client/lazy.js`);

// if (payload.includes('</body>')) {
// newPayload = payload.replace(
// '</body>',
// `<script type="module">addEventListener('load',()=>import('${url}'))</script></body>`,
// );
// } else {
// // if no document, inject at the end of the payload
// newPayload = `${payload}<script type="module">addEventListener('load',()=>import('${url}'))</script>`;
// }
// }
// }
// done(null, newPayload);
// },
// );
}
},
);
118 changes: 118 additions & 0 deletions plugins/mode-support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { render } from '@lit-labs/ssr';
import fp from 'fastify-plugin';
import { joinURLPathSegments } from '../lib/utils.js';

/**
* The purpose of this plugin is to decorate the fastify object with mode based method that can be used to
* server render, client side render or server render and hydrate a template
*/
export default fp(
async (
fastify,
{ appName = '', base = '/', prefix = '', development = false, mode },
) => {
/**
* Constructs a script tag for a given type (content or fallback)
* Takes into account development mode vs production, app mode (ssr-only, csr-only or hydrate) and assets base
* @param {"content" | "fallback"} type
* @returns {string} - constructed script tag
*/
function scriptTag(type) {
let script;
if (mode !== 'ssr-only') {
if (development) {
script = `<script src="/_/dynamic/element/${type}/${appName}" type="module" crossorigin defer></script>`;
} else {
script = `<script src="${base}/client/${type}.js" type="module" crossorigin defer></script>`;
}
}
return script;
}

/**
* Renders a template for a given type (content or fallback)
* Takes into account development mode vs production and assets base and appName
* Scopes the polyfill to the element in question to avoid clashing with other uses of hydration
* on the page
* @param {"content" | "fallback"} type
* @param {import("lit").HTMLTemplateResult} template
* @returns {string} - server rendered component markup with dsd polyfill inlined after
*
* @example
* ```js
* serverRender("content", html`<my-app-content ...etc></my-app-content>`)
* ```
*/
function serverRender(type, template) {
let shadowRootPath = `${base}/client/template-shadowroot.js`;
if (development) {
shadowRootPath = joinURLPathSegments(
prefix,
`/_/dynamic/modules/@webcomponents/template-shadowroot/template-shadowroot.js`,
);
}
// user provided markup, SSR'd
const ssrMarkup = Array.from(render(template)).join('');
// polyfill for browsers that don't support declarative shadow dom
const polyfillMarkup = `
<script type="module">
if (!HTMLTemplateElement.prototype.hasOwnProperty("shadowRoot")) {
const {hydrateShadowRoots} = await import("${shadowRootPath}");
hydrateShadowRoots(document.querySelector("${appName}-${type}"));
}
</script>`;
return `${ssrMarkup}${polyfillMarkup}`;
}

/**
* Mode decorators for ssr-only, csr-only and hydrate
*/

fastify.decorate(
'serverRender',
/**
* Fastify decorator method that creates server side only markup from a type (content or fallback)
* and a template
* @param {"content" | "fallback"} type
* @param {import("lit").HTMLTemplateResult} template
* @returns {string}
*/
(type, template) => {
return `${serverRender(type, template)}`;
},
);

fastify.decorate(
'serverRenderAndHydrate',
/**
* Fastify decorator method that creates server side only markup with additional client side hydration scripts using a type (content or fallback)
* and a template
* @param {"content" | "fallback"} type
* @param {import("lit").HTMLTemplateResult} template
* @returns {string}
*/
(type, template) => {
return `${serverRender(type, template)}${scriptTag(type)}`;
},
);

fastify.decorate(
'clientRender',
/**
* Fastify decorator method that creates server side tag markup for a content or fallback route and adds additional client side scripts to define the component
* once the page has loaded
* @param {"content" | "fallback"} type
* @param {import("lit").HTMLTemplateResult} template
* @returns {string}
*/
(type, template) => {
return `${String.raw(template.strings, ...template.values)}${scriptTag(
type,
)}`.replace('null', '');
},
);
},
);

// Also TODO
// hydrate polyfill is loaded on dom ready but should really happen inline if possible to avoid FOUC in Safari and FF
15 changes: 15 additions & 0 deletions plugins/podlet.js
Original file line number Diff line number Diff line change
@@ -78,5 +78,20 @@ export default fp(
},
);
}

// set up scriptsList for other plugins to add scripts to
// @ts-ignore
if (!fastify.scriptsList) {
fastify.decorate('scriptsList', []);
}

// process metrics streams at the end
fastify.addHook('onReady', async () => {
// @ts-ignore
if (!fastify.scriptsList) return;

// @ts-ignore
podlet.js(fastify.scriptsList);
});
},
);
18 changes: 0 additions & 18 deletions plugins/script.js

This file was deleted.

59 changes: 33 additions & 26 deletions plugins/scripts.js
Original file line number Diff line number Diff line change
@@ -5,32 +5,39 @@ export default fp(async (fastify, { enabled, base, development, prefix }) => {
// inject live reload when in dev mode
if (enabled) {
fastify.log.debug('custom client side scripting enabled');
fastify.addHook(
'onSend',
(request, reply, /** @type {string} */ payload, done) => {
let newPayload = payload;
const contentType = reply.getHeader('content-type') || '';
if (typeof contentType === 'string') {
// only inject live reload if the content type is html
if (contentType.includes('html')) {
// if there is a document, inject before closing body
const url = development
? joinURLPathSegments(prefix, `/_/dynamic/files/scripts.js`)
: joinURLPathSegments(base, `/client/scripts.js`);

if (payload.includes('</body>')) {
newPayload = payload.replace(
'</body>',
`<script type="module" src="${url}"></script></body>`,
);
} else {
// if no document, inject at the end of the payload
newPayload = `${payload}<script type="module" src="${url}"></script>`;
}
}
}
done(null, newPayload);
},
);
const url = development
? joinURLPathSegments(prefix, `/_/dynamic/files/scripts.js`)
: joinURLPathSegments(base, `/client/scripts.js`);

// @ts-ignore
fastify.scriptsList.push({ value: url, type: 'module' });
// fastify.addHook(
// 'onSend',
// (request, reply, /** @type {string} */ payload, done) => {
// let newPayload = payload;
// const contentType = reply.getHeader('content-type') || '';
// if (typeof contentType === 'string') {
// // only inject live reload if the content type is html
// if (contentType.includes('html')) {
// // if there is a document, inject before closing body
// const url = development
// ? joinURLPathSegments(prefix, `/_/dynamic/files/scripts.js`)
// : joinURLPathSegments(base, `/client/scripts.js`);

// if (payload.includes('</body>')) {
// newPayload = payload.replace(
// '</body>',
// `<script type="module" src="${url}"></script></body>`,
// );
// } else {
// // if no document, inject at the end of the payload
// newPayload = `${payload}<script type="module" src="${url}"></script>`;
// }
// }
// }
// done(null, newPayload);
// },
// );
}
});

0 comments on commit add5179

Please sign in to comment.