UNPKG

@febinrasheed/prerender-loader

Version:

Painless universal prerendering for Webpack 5. Works great with html-webpack-plugin.

206 lines (180 loc) 8.18 kB
/** * Copyright 2018 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ import path from 'path'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import { compile, compileToHtml, compileMultipleToHtml, readFile } from './_helpers'; const PRERENDER_LOADER = path.resolve(__dirname, '../src'); const configure = prerenderLoaderOptions => config => { let template = path.resolve(config.context, 'index.html'); if (prerenderLoaderOptions === true) { template = `!!${PRERENDER_LOADER}?string!${template}`; } else if (prerenderLoaderOptions) { template = `!!${PRERENDER_LOADER}?${JSON.stringify(prerenderLoaderOptions)}!${template}`; } else { template = `!!raw-loader!${template}`; } config.plugins.push( new HtmlWebpackPlugin({ template, filename: 'index.html', compile: false, inject: true, minify: { collapseWhitespace: true, preserveLineBreaks: true } }) ); }; const withoutPrerender = configure(false); const withPrerender = configure(true); describe('webpack compilation smoke test (no prerendering)', () => { it('should compile', async () => { const info = await compile('fixtures/basic/index.js', withoutPrerender); expect(info.assets).toHaveLength(2); const html = await readFile('fixtures/basic/dist/index.html'); expect(html).toMatchSnapshot(); }); it('should compile named entries', async () => { const info = await compile('fixtures/basic/index.js', config => { config = withoutPrerender(config) || config; config.entry = { app: config.entry }; return config; }); expect(info.assets).toHaveLength(2); const html = await readFile('fixtures/basic/dist/index.html'); expect(html).toMatchSnapshot(); }); it('should compile array entries', async () => { const info = await compile('fixtures/basic/index.js', config => { config = withoutPrerender(config) || config; config.entry = [config.entry]; return config; }); expect(info.assets).toHaveLength(2); const html = await readFile('fixtures/basic/dist/index.html'); expect(html).toMatchSnapshot(); }); it('should propagate child compiler errors', async () => { await expect(compile('fixtures/failure/index.js', withPrerender)).rejects.toMatch(/does-not-exist/); }); }); describe('prerender-loader!x.html', () => { describe('?disabled', async () => { it('should not serialize HTML', async () => { const { html } = await compileToHtml('basic', configure({ disabled: true, string: true })); expect(html).not.toMatch(/<div>this counts as SSR<\/div>/); }); }); describe('Imperative DOM, no {{prerender}} field', () => { it('should return serialized HTML', async () => { const { html, document } = await compileToHtml('basic', withPrerender); // verify that our DOM-generated content has been prerendered into the static HTML: expect(html).toMatch(/<div>this counts as SSR<\/div>/); // ... and that there's no extra children: expect(document.body.children).toHaveLength(2); // ... and that html-webpack-plugin was able to inject scripts after the content: expect(document.body.firstElementChild).toHaveProperty('outerHTML', '<div>this counts as SSR</div>'); expect(html).toMatchSnapshot(); }); }); describe('default export function, no {{prerender}} field', () => { it('should inject returned HTML into <body>', async () => { const { html, document } = await compileToHtml('factory', withPrerender); // verify that our DOM-generated content has been prerendered into the static HTML: expect(html).toMatch(/<div>some returned HTML<\/div>/); // ... and that there's no extra children: expect(document.body.children).toHaveLength(2); // ... and that html-webpack-plugin was able to inject scripts after the content: expect(document.body.firstElementChild).toHaveProperty('outerHTML', '<div>some returned HTML</div>'); expect(html).toMatchSnapshot(); }); }); describe('default export function, with {{prerender}} field', () => { // <div>some returned HTML</div> it('should inject returned HTML in place of {{prerender}}', async () => { const { html, document } = await compileToHtml('with-field', withPrerender); // verify that our DOM-generated content has been prerendered into the static HTML: expect(html).toMatch(/<div>more returned HTML<\/div>/); expect(document.body.children).toHaveLength(2); // verify our the wrapper element that contained {{prerender}} is still present: expect(document.body.firstElementChild).toHaveProperty('id', 'wrapper'); // verify the wrapper element contains all of the prerendered content: expect(document.getElementById('wrapper').innerHTML).toMatch(/^\s*<div>more returned HTML<\/div>\s*$/); expect(html).toMatchSnapshot(); }); }); describe('named export function', () => { it('should invoke the only named export', async () => { const { html } = await compileToHtml('named-export-fn', withPrerender); expect(html).toMatch(/<div>some HTML returned from prerender\(\)<\/div>/); }); }); describe('exported value', () => { it('should invoke the only named export', async () => { const { html } = await compileToHtml('value-export', withPrerender); expect(html).toMatch(/<div>this is the resolved value of PRERENDERED_HTML<\/div>/); }); }); describe('exported function with no return value', () => { it('should serialize the whole document', async () => { const { html, document } = await compileToHtml('function-export-dom', withPrerender); expect(html).toMatch(/<div>content injected into the dom<\/div>/); expect(document.body.children).toHaveLength(2); expect(html).toMatchSnapshot(); }); }); const DOCUMENT_URL = 'http://localhost/page'; describe(`?documentUrl=${DOCUMENT_URL}`, () => { it('should set the value returned by window.location', async () => { const { document } = await compileToHtml('document-url', configure({ string: true, documentUrl: DOCUMENT_URL })); // verify that our DOM-generated content has been prerendered into the static HTML: expect(document.body.firstElementChild).toHaveProperty('outerHTML', `<div>${DOCUMENT_URL}</div>`); }); }); }); describe('multiple entry points', () => { const configureMultiple = config => { const template = path.resolve(config.context, 'index.html'); Object.keys(config.entry).forEach(entryKey => { const options = { string: true, entry: `./${entryKey}.js` }; config.plugins.push(new HtmlWebpackPlugin({ template: `!!${PRERENDER_LOADER}?${JSON.stringify(options)}!${template}`, filename: entryKey + '.html', compile: false, inject: true, minify: { collapseWhitespace: true, preserveLineBreaks: true } })); }); }; it('should inject returned HTML in place of {{prerender}}', async () => { const { html } = await compileMultipleToHtml('multiple-with-field', configureMultiple); expect(html).toHaveProperty('home'); expect(html.home).toMatch(/<div>returned home HTML<\/div>/); expect(html.home).not.toMatch(/<div>returned other HTML<\/div>/); expect(html).toHaveProperty('other'); expect(html.other).toMatch(/<div>returned other HTML<\/div>/); expect(html.other).not.toMatch(/<div>returned home HTML<\/div>/); const bothHtml = html.home + '\n\n' + html.other; expect(bothHtml).toMatchSnapshot(); }); });