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

Feature/issue 935 native custom element ssr #943

Merged
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
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@
"remark-rehype": "^7.0.0",
"rollup": "^2.58.0",
"rollup-plugin-terser": "^7.0.0",
"unified": "^9.2.0"
"unified": "^9.2.0",
"wc-compiler": "~0.3.1"
},
"devDependencies": {
"@babel/runtime": "^7.10.4",
Expand Down
21 changes: 15 additions & 6 deletions packages/cli/src/lib/ssr-route-worker.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
// https://github.com/nodejs/modules/issues/307#issuecomment-858729422
import { pathToFileURL } from 'url';
import { workerData, parentPort } from 'worker_threads';
import { renderToString } from 'wc-compiler';

async function executeRouteModule({ modulePath, compilation, route, label, id }) {
const { getTemplate = null, getBody = null, getFrontmatter = null } = await import(pathToFileURL(modulePath)).then(module => module);
const moduleURL = pathToFileURL(modulePath);
const module = await import(moduleURL).then(module => module);
const { getFrontmatter = null, getBody = null, getTemplate = null } = module;
const parsedCompilation = JSON.parse(compilation);
const data = {
template: null,
body: null,
frontmatter: null
};

if (getFrontmatter) {
data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id);
}

if (getTemplate) {
data.template = await getTemplate(parsedCompilation, route);
}

if (getBody) {
data.body = await getBody(parsedCompilation, route);
}
if (module.default) {
const { html } = await renderToString(moduleURL);

if (getFrontmatter) {
data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id);
data.body = html;
} else {
if (getBody) {
data.body = await getBody(parsedCompilation, route);
}
}

parentPort.postMessage(data);
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/lifecycles/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ const bundleCompilation = async (compilation) => {

return new Promise(async (resolve, reject) => {
try {
compilation.graph = compilation.graph.filter(page => !page.isSSR);

// https://rollupjs.org/guide/en/#differences-to-the-javascript-api
if (compilation.graph.length > 0) {
const rollupConfigs = await getRollupConfig(compilation);
const rollupConfigs = await getRollupConfig({
...compilation,
graph: compilation.graph.filter(page => !page.isSSR)
});
const bundle = await rollup(rollupConfigs[0]);

await bundle.write(rollupConfigs[0].output);
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/lifecycles/serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,10 @@ async function getHybridServer(compilation) {
await fs.promises.mkdir(path.join(compilation.context.scratchDir, url), { recursive: true });
await fs.promises.writeFile(path.join(compilation.context.scratchDir, url, 'index.html'), body);

compilation.graph = compilation.graph.filter(page => page.isSSR && page.route === url);

const rollupConfigs = await getRollupConfig(compilation);
const rollupConfigs = await getRollupConfig({
...compilation,
graph: [matchingRoute]
});
const bundle = await rollup(rollupConfigs[0]);
await bundle.write(rollupConfigs[0].output);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const greenwoodPluginRendererString = {
const greenwoodPluginRendererDefault = {
type: 'renderer',
name: 'plugin-renderer-string',
name: 'plugin-renderer-default',
provider: () => {
return {
workerUrl: new URL('../../lib/ssr-route-worker.js', import.meta.url)
};
}
};

export { greenwoodPluginRendererString };
export { greenwoodPluginRendererDefault };
162 changes: 63 additions & 99 deletions packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
* footer.js
* pages/
* artists.js
* index.md
* users.js
* templates/
* app.html
*/
import chai from 'chai';
import fs from 'fs';
import { JSDOM } from 'jsdom';
import path from 'path';
import { getSetupFiles, getDependencyFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import { getSetupFiles, getOutputTeardownFiles } from '../../../../../test/utils.js';
import request from 'request';
import { runSmokeTest } from '../../../../../test/smoke-test.js';
import { Runner } from 'gallinago';
Expand All @@ -49,82 +51,7 @@ describe('Build Greenwood With: ', function() {
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 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 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/`
);

await runner.setup(outputPath, [
...getSetupFiles(outputPath),
...lit,
...litPackageJson,
...litDirectives,
...litDecorators,
...litElementPackageJson,
...litElement,
...litElementDecorators,
...litHtmlPackageJson,
...litHtml,
...litHtmlDirectives,
...trustedTypes,
...litReactiveElement,
...litReactiveElementDecorators,
...litReactiveElementPackageJson
]);
await runner.setup(outputPath, getSetupFiles(outputPath));

return new Promise(async (resolve) => {
setTimeout(() => {
Expand All @@ -138,12 +65,13 @@ describe('Build Greenwood With: ', function() {
runSmokeTest(['public', 'index'], LABEL);

let response = {};
let dom;
let artistsPageDom;
let usersPageDom;
let artistsPageGraphData;

before(async function() {
const graph = JSON.parse(await fs.promises.readFile(path.join(outputPath, 'public/graph.json'), 'utf-8'));

artistsPageGraphData = graph.filter(page => page.route === '/artists/')[0];

return new Promise((resolve, reject) => {
Expand All @@ -154,14 +82,28 @@ describe('Build Greenwood With: ', function() {

response = res;
response.body = body;
dom = new JSDOM(body);

artistsPageDom = new JSDOM(body);

resolve();
});
});
});

describe('Serve command with HTML route response', function() {
before(async function() {
return new Promise((resolve, reject) => {
request.get(`${hostname}/users/`, (err, res, body) => {
if (err) {
reject();
}
usersPageDom = new JSDOM(body);

resolve();
});
});
});

describe('Serve command with HTML route response for page using "get" functions', function() {

it('should return a 200 status', function(done) {
expect(response.statusCode).to.equal(200);
Expand All @@ -179,53 +121,45 @@ describe('Build Greenwood With: ', function() {
});

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

it('should have one style tags', function() {
const styles = dom.window.document.querySelectorAll('head > style');
const styles = artistsPageDom.window.document.querySelectorAll('head > style');

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

it('should have three script tags', function() {
const scripts = dom.window.document.querySelectorAll('head > script');
it('should have the expected number of <script> tags in the <head>', function() {
const scripts = artistsPageDom.window.document.querySelectorAll('head > script');

expect(scripts.length).to.equal(3);
expect(scripts.length).to.equal(2);
});

it('should have expected SSR content from the non module script tag', function() {
const scripts = Array.from(dom.window.document.querySelectorAll('head > script'))
const scripts = Array.from(artistsPageDom.window.document.querySelectorAll('head > script'))
.filter(tag => !tag.getAttribute('type'));

expect(scripts.length).to.equal(1);
expect(scripts[0].textContent).to.contain('console.log');
});

it('should have a bundled script for the footer component', function() {
const footerScript = Array.from(dom.window.document.querySelectorAll('head > script[type]'))
.filter(script => (/footer.*[a-z0-9].js/).test(script.src));

expect(footerScript.length).to.be.equal(1);
expect(footerScript[0].type).to.be.equal('module');
});

it('should have the expected number of table rows of content', function() {
const rows = dom.window.document.querySelectorAll('body > table tr');
const rows = artistsPageDom.window.document.querySelectorAll('body > table tr');

expect(rows.length).to.equal(11);
});

it('should have the expected <title> content in the <head>', function() {
const title = dom.window.document.querySelectorAll('head > title');
const title = artistsPageDom.window.document.querySelectorAll('head > title');

expect(title.length).to.equal(1);
expect(title[0].textContent).to.equal('/artists/');
});

it('should have custom metadata in the <head>', function() {
const metaDescription = Array.from(dom.window.document.querySelectorAll('head > meta'))
const metaDescription = Array.from(artistsPageDom.window.document.querySelectorAll('head > meta'))
.filter((tag) => tag.getAttribute('name') === 'description');

expect(metaDescription.length).to.equal(1);
Expand All @@ -248,13 +182,43 @@ describe('Build Greenwood With: ', function() {

it('should append the expected <script> tag for a frontmatter import <x-counter> component', function() {
const componentName = 'counter';
const counterScript = Array.from(dom.window.document.querySelectorAll('head > script[src]'))
const counterScript = Array.from(artistsPageDom.window.document.querySelectorAll('head > script[src]'))
.filter((tag) => tag.getAttribute('src').indexOf(`/${componentName}.`) === 0);

expect(artistsPageGraphData.imports[0]).to.equal(`/components/${componentName}.js`);
expect(counterScript.length).to.equal(1);
});
});

describe('Prerender an HTML route response for page exporting an HTMLElement as default export', function() {
it('the response body should be valid HTML from JSDOM', function(done) {
expect(usersPageDom).to.not.be.undefined;
done();
});

it('should have the expected <h1> text in the <body>', function() {
const heading = usersPageDom.window.document.querySelectorAll('body > h1');
const userLength = parseInt(heading[0].querySelector('span').textContent, 10);

expect(heading.length).to.be.equal(1);
expect(heading[0].textContent).to.contain('List of Users:');
expect(userLength).to.greaterThan(0);
});

it('should have the expected number of <wc-card> tags in the <head>', function() {
const cards = usersPageDom.window.document.querySelectorAll('body > wc-card template[shadowroot="open"]');

expect(cards.length).to.be.greaterThan(0);
});

xit('should have a bundled <script> for the card component', function() {
const cardScript = Array.from(usersPageDom.window.document.querySelectorAll('head > script[type]'))
.filter(script => (/card.*[a-z0-9].js/).test(script.src));

expect(cardScript.length).to.be.equal(1);
expect(cardScript[0].type).to.be.equal('module');
});
});
});

after(function() {
Expand Down
6 changes: 0 additions & 6 deletions packages/cli/test/cases/build.default.ssr/package.json

This file was deleted.

Loading