diff --git a/packages/cli/package.json b/packages/cli/package.json index acaafa2c4..216057b34 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/lib/ssr-route-worker.js b/packages/cli/src/lib/ssr-route-worker.js index 9f859575b..3a30e0b8b 100644 --- a/packages/cli/src/lib/ssr-route-worker.js +++ b/packages/cli/src/lib/ssr-route-worker.js @@ -1,9 +1,12 @@ // 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, @@ -11,16 +14,22 @@ async function executeRouteModule({ modulePath, compilation, route, label, id }) 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); diff --git a/packages/cli/src/lifecycles/bundle.js b/packages/cli/src/lifecycles/bundle.js index 3326d9df2..4b4229b86 100644 --- a/packages/cli/src/lifecycles/bundle.js +++ b/packages/cli/src/lifecycles/bundle.js @@ -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); diff --git a/packages/cli/src/lifecycles/serve.js b/packages/cli/src/lifecycles/serve.js index bf3e42528..9964c0a39 100644 --- a/packages/cli/src/lifecycles/serve.js +++ b/packages/cli/src/lifecycles/serve.js @@ -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); diff --git a/packages/cli/src/plugins/renderer/plugin-renderer-string.js b/packages/cli/src/plugins/renderer/plugin-renderer-default.js similarity index 54% rename from packages/cli/src/plugins/renderer/plugin-renderer-string.js rename to packages/cli/src/plugins/renderer/plugin-renderer-default.js index df120d32a..16203beb9 100644 --- a/packages/cli/src/plugins/renderer/plugin-renderer-string.js +++ b/packages/cli/src/plugins/renderer/plugin-renderer-default.js @@ -1,6 +1,6 @@ -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) @@ -8,4 +8,4 @@ const greenwoodPluginRendererString = { } }; -export { greenwoodPluginRendererString }; \ No newline at end of file +export { greenwoodPluginRendererDefault }; \ No newline at end of file diff --git a/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js b/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js index c8adae81a..f1d9f769a 100644 --- a/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js +++ b/packages/cli/test/cases/build.default.ssr/build.default.ssr.spec.js @@ -17,6 +17,8 @@ * footer.js * pages/ * artists.js + * index.md + * users.js * templates/ * app.html */ @@ -24,7 +26,7 @@ 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'; @@ -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(() => { @@ -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) => { @@ -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); @@ -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 - \ No newline at end of file diff --git a/packages/plugin-renderer-lit/README.md b/packages/plugin-renderer-lit/README.md index b7d085785..011926fd0 100644 --- a/packages/plugin-renderer-lit/README.md +++ b/packages/plugin-renderer-lit/README.md @@ -48,51 +48,48 @@ export default { } ``` -Now, you can write some [SSR routes](/docs/server-rendering/) using Lit! The below example even uses the standard [SimpleGreeting](https://lit.dev/playground/) component from the Lit docs. +Now, you can write some [SSR routes](/docs/server-rendering/) using Lit including all the [available APIs](docs/server-rendering/#api). The below example uses the standard [SimpleGreeting](https://lit.dev/playground/) component from the Lit docs by also using a LitElement as the `default export`! ```js -import fetch from 'node-fetch'; -import { html } from 'lit'; -import '../components/greeting.js'; - -async function getBody() { - const artists = await fetch('http://www.mydomain.com/api/artists').then(resp => resp.json()); - - return html` -

Lit SSR response

- - - - - - - - +import { html, LitElement } from 'lit'; +import './path/to/greeting.js'; + +export default class ArtistsPage extends LitElement { + + constructor() { + super(); + this.artists = [{ /* ... */ }]; + } + + render() { + const { artists } = this; + + return html` ${ artists.map((artist) => { - const { id, name, bio, imageUrl } = artist; + const { id, name, imageUrl } = artist; return html` - - - - - - - + + + + + + +
`; }) } -
IDNameDescriptionMessagePicture
${id}${name}${bio} - - - -
- `; + `; + } } -export { getBody }; +// for now these are needed for the Lit specific implementations +customElements.define('artists-page', ArtistsPage); +export const tagName = 'artists-page'; ``` +> **Note**: _Lit SSR [**only** renders into declarative shadow roots](https://github.com/lit/lit/issues/3080#issuecomment-1165158794) so you will have to keep browser support and polyfill usage in mind depending on your use case_. + ## Options ### Prerender (experimental) diff --git a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js b/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js index dedd61883..14696f78e 100644 --- a/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js +++ b/packages/plugin-renderer-lit/src/ssr-route-worker-lit.js @@ -41,20 +41,28 @@ async function executeRouteModule({ modulePath, compilation, route, label, id, p data.html = await getTemplateResultString(templateResult); } else { - const { getTemplate = null, getBody = null, getFrontmatter = null } = await import(pathToFileURL(modulePath)).then(module => module); + const module = await import(pathToFileURL(modulePath)).then(module => module); + const { getTemplate = null, getBody = null, getFrontmatter = null } = module; - if (getTemplate) { - const templateResult = await getTemplate(parsedCompilation, route); - - data.template = await getTemplateResultString(templateResult); - } + if (module.default && module.tagName) { + const { tagName } = module; + const templateResult = html` + ${unsafeHTML(`<${tagName}>`)} + `; - if (getBody) { + data.body = await getTemplateResultString(templateResult); + } else if (getBody) { const templateResult = await getBody(parsedCompilation, route); data.body = await getTemplateResultString(templateResult); } + if (getTemplate) { + const templateResult = await getTemplate(parsedCompilation, route); + + data.template = await getTemplateResultString(templateResult); + } + if (getFrontmatter) { data.frontmatter = await getFrontmatter(parsedCompilation, route, label, id); } diff --git a/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js b/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js index 8cd6fdce9..a31503280 100644 --- a/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js +++ b/packages/plugin-renderer-lit/test/cases/build.default/build.default.spec.js @@ -19,6 +19,7 @@ * greeting.js * pages/ * artists.js + * users.js * templates/ * app.html */ @@ -139,6 +140,8 @@ describe('Build Greenwood With: ', function() { let response = {}; let artists = []; let dom; + let usersPageDom; + let usersPageHtml; let aboutPageGraphData; before(async function() { @@ -157,12 +160,21 @@ describe('Build Greenwood With: ', function() { response.body = body; dom = new JSDOM(body); - resolve(); + request.get(`${hostname}/users/`, (err, res, body) => { + if (err) { + reject(); + } + + usersPageHtml = body; + usersPageDom = new JSDOM(body); + + resolve(); + }); }); }); }); - describe('Serve command with HTML route response', function() { + describe('Serve command with HTML route response using getBody, getTemplate and getFrontmatter', function() { it('should return a 200 status', function(done) { expect(response.statusCode).to.equal(200); @@ -243,6 +255,25 @@ describe('Build Greenwood With: ', function() { expect(aboutPageGraphData.data.date).to.equal('01-01-2021'); }); }); + + describe('Serve command with HTML route response using LitElement 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

text in the ', function() { + expect(usersPageHtml).to.contain('Users Page'); + }); + + it('should have the expected users length text in the ', function() { + expect(usersPageHtml).to.contain(`
${artists.length}
`); + }); + + it('should have the expected content in the ', function() { + expect(usersPageHtml).to.contain('