Skip to content

Commit

Permalink
bug/issue 1197 ensure Lit SSR hydration script loads after importmaps (
Browse files Browse the repository at this point in the history
…#1289)

* ensure Lit hydration script loads after importmaps

* fix attribute selector syntax used in test cases
  • Loading branch information
thescientist13 authored Nov 1, 2024
1 parent 39d0a3c commit 5b75cf4
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 9 deletions.
43 changes: 37 additions & 6 deletions packages/plugin-renderer-lit/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,47 @@ class LitHydrationResource extends ResourceInterface {

async intercept(url, request, response) {
const { importMaps } = this.compilation.config.polyfills;
const importMapType = process.env.__GWD_COMMAND__ === 'develop' && importMaps // eslint-disable-line no-underscore-dangle
const isDevelopment = process.env.__GWD_COMMAND__ === 'develop'; // eslint-disable-line no-underscore-dangle
const importType = isDevelopment && importMaps
? 'module-shim'
: 'module';
const importMapType = isDevelopment && importMaps
? 'importmap-shim'
: 'importmap';
const headSelector = isDevelopment ? `<script type="${importMapType}">` : '<head>';
const hydrationSupportScriptPath = '/node_modules/@lit-labs/ssr-client/lit-element-hydrate-support.js';
let body = await response.text();

// this needs to come first before any userland code
body = body.replace('<head>', `
<head>
<script type="${importMapType}" src="/node_modules/@lit-labs/ssr-client/lit-element-hydrate-support.js"></script>
`);
// this needs to come first before any userland code, but before any import maps
// https://github.com/ProjectEvergreen/greenwood/pull/1289
if (isDevelopment) {
// quick way to find the ending position of the importmap <script> tag
// and append the hydration support <script> right after it
const scriptEndPattern = /<\/script>/g;
const importMapStartPos = body.indexOf(headSelector) ?? '';
let importMapEndPos = 0;
let match;

while ((match = scriptEndPattern.exec(body)) !== null) {
const position = match.index;
if (position > importMapStartPos) {
importMapEndPos = position;
break;
}
}

body = `
${body.slice(0, importMapEndPos)}
</script>
<script type="${importType}" src="${hydrationSupportScriptPath}"></script>
${body.slice(importMapEndPos + 9)}
`;
} else {
body = body.replace(headSelector, `
${headSelector}
<script type="${importType}" src="${hydrationSupportScriptPath}"></script>
`);
}

return new Response(body);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Use Case
* Run Greenwood for development with lit renderer plugin.
*
* User Result
* Should serve a bare bones Greenwood build for developing with Lit+SSR.
*
* User Command
* greenwood develop
*
* User Config
* import { greenwoodPluginRendererLit } from '@greenwood/plugin-renderer-lit';
*
* {
* plugins: [{
* greenwoodPluginRendererLit()
* }]
* }
*
* User Workspace
* src/
* pages/
* index.js
*/
import chai from 'chai';
import { JSDOM } from 'jsdom';
import path from 'path';
import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import { Runner } from 'gallinago';
import { fileURLToPath, URL } from 'url';

const expect = chai.expect;

describe('Develop Greenwood With: ', function() {
const LABEL = 'Lit Renderer';
const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
const outputPath = fileURLToPath(new URL('.', import.meta.url));
const hostname = 'http://127.0.0.1:1984';
let runner;

before(function() {
this.context = {
publicDir: path.join(outputPath, 'public')
};
runner = new Runner();
});

describe(LABEL, function() {

before(async function() {
const lit = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/*.js`,
`${outputPath}/node_modules/lit/`
);
const litDecorators = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/decorators/*.js`,
`${outputPath}/node_modules/lit/decorators/`
);
const litDirectives = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/directives/*.js`,
`${outputPath}/node_modules/lit/directives/`
);
const litPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/lit/package.json`,
`${outputPath}/node_modules/lit/`
);
const litElement = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-element/*.js`,
`${outputPath}/node_modules/lit-element/`
);
const litElementPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-element/package.json`,
`${outputPath}/node_modules/lit-element/`
);
const litElementDecorators = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-element/decorators/*.js`,
`${outputPath}/node_modules/lit-element/decorators/`
);
const litHtml = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/*.js`,
`${outputPath}/node_modules/lit-html/`
);
const litHtmlNode = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/node/*.js`,
`${outputPath}/node_modules/lit-html/node/`
);
const litHtmlNodeDirectives = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/node/directives/*.js`,
`${outputPath}/node_modules/lit-html/node/directives/`
);
const litHtmlPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/package.json`,
`${outputPath}/node_modules/lit-html/`
);
const litHtmlDirectives = await getDependencyFiles(
`${process.cwd()}/node_modules/lit-html/directives/*.js`,
`${outputPath}/node_modules/lit-html/directives/`
);
// lit-html has a dependency on this
// https://github.com/lit/lit/blob/main/packages/lit-html/package.json#L82
const trustedTypes = await getDependencyFiles(
`${process.cwd()}/node_modules/@types/trusted-types/package.json`,
`${outputPath}/node_modules/@types/trusted-types/`
);
const litReactiveElement = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/*.js`,
`${outputPath}/node_modules/@lit/reactive-element/`
);
const litReactiveElementNode = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/node/*.js`,
`${outputPath}/node_modules/@lit/reactive-element/node/`
);
const litReactiveElementDecorators = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/decorators/*.js`,
`${outputPath}/node_modules/@lit/reactive-element/decorators/`
);
const litReactiveElementPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit/reactive-element/package.json`,
`${outputPath}/node_modules/@lit/reactive-element/`
);
const litSsrElementHydrationSupport = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit-labs/ssr-client/lit-element-hydrate-support.js`,
`${outputPath}/node_modules/@lit-labs/ssr-client/`
);
const litSsrHtmlHydrationSupport = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit-labs/ssr-client/lib/*.js`,
`${outputPath}/node_modules/@lit-labs/ssr-client/lib/`
);
const litSsrDomShimPackageJson = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit-labs/ssr-dom-shim/package.json`,
`${outputPath}/node_modules/@lit-labs/ssr-dom-shim/`
);
const litSsrDomShim = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit-labs/ssr-dom-shim/*.js`,
`${outputPath}/node_modules/@lit-labs/ssr-dom-shim/`
);
const litSsrDomShimLibs = await getDependencyFiles(
`${process.cwd()}/node_modules/@lit-labs/ssr-dom-shim/lib/*.js`,
`${outputPath}/node_modules/@lit-labs/ssr-dom-shim/lib/`
);

runner.setup(outputPath, [
...getSetupFiles(outputPath),
...lit,
...litPackageJson,
...litDirectives,
...litDecorators,
...litElementPackageJson,
...litElement,
...litElementDecorators,
...litHtmlPackageJson,
...litHtml,
...litHtmlNode,
...litHtmlDirectives,
...litHtmlNodeDirectives,
...trustedTypes,
...litReactiveElement,
...litReactiveElementNode,
...litReactiveElementDecorators,
...litReactiveElementPackageJson,
...litSsrElementHydrationSupport,
...litSsrHtmlHydrationSupport,
...litSsrDomShim,
...litSsrDomShimPackageJson,
...litSsrDomShimLibs
]);

return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 10000);

runner.runCommand(cliPath, 'develop', { async: true });
});
});

describe('Develop command with expected HTML for the / route', function() {
let response = {};
let dom;
let body;

before(async function() {
response = await fetch(`${hostname}/`);
body = await response.text();
dom = new JSDOM(body);
});

it('should return a 200 status', function(done) {
expect(response.status).to.equal(200);
done();
});

it('should return the correct content type', function(done) {
expect(response.headers.get('content-type')).to.equal('text/html');
done();
});

it('should return a response body', function(done) {
expect(body).to.not.be.undefined;
done();
});

it('the response body should be valid HTML from JSDOM', function(done) {
expect(dom).to.not.be.undefined;
done();
});

it('should have the expected lit hydration script in the <head>', function() {
const scripts = Array.from(dom.window.document.querySelectorAll('script[src*="lit-element-hydrate-support"]'));

expect(scripts.length).to.equal(1);
});

it('should have the expected lit hydration script _after_ any importmaps in the <head>', function() {
// make sure this does NOT come before an importmap
const scripts = Array.from(dom.window.document.querySelectorAll('script[src*="lit-element-hydrate-support"] + script[type="importmap"]'));

expect(scripts.length).to.equal(0);
});
});
});

after(function() {
runner.teardown(getOutputTeardownFiles(outputPath));
runner.stopCommand();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { greenwoodPluginRendererLit } from '../../../src/index.js';

export default {
plugins: [
greenwoodPluginRendererLit()
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "plugin-renderer-lit-develop-default",
"type": "module",
"dependencies": {
"lit": "^3.1.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { html } from 'lit';

export async function getBody() {
return html`<h1>Home Page</h1>`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
*
* {
* plugins: [{
* greenwoodPluginRendererLit({
* prerender: true
* })
* greenwoodPluginRendererLit()
* }]
* }
*
Expand Down
6 changes: 6 additions & 0 deletions test/smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ function commonIndexSpecs(dom, html, label) {
it('should not have any optimization markers left in the HTML', function() {
expect(html.match(/data-gwd-opt=".*[a-z]"/)).to.be.equal(null);
});

it('should not have any module based <script> tags that come _before_ any importmaps in the <head>', function() {
const scripts = Array.from(dom.window.document.querySelectorAll('script[type="module"] + script[type*="importmap"]'));

expect(scripts.length).to.equal(0);
});
});

describe('document <body>', function() {
Expand Down

0 comments on commit 5b75cf4

Please sign in to comment.