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

bug/issue 1197 ensure Lit SSR hydration script loads after importmaps #1289

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
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
Loading