From a09490913ef71a269792f9caaedb49b513949233 Mon Sep 17 00:00:00 2001
From: Jason Miller
Date: Wed, 30 May 2018 14:04:07 -0400
Subject: [PATCH] Initial release!
---
.editorconfig | 9 +
.gitignore | 3 +
CONTRIBUTING.md | 28 ++
LICENSE | 202 +++++++++++++
README.md | 270 ++++++++++++++++++
package.json | 97 +++++++
src/index.js | 247 ++++++++++++++++
src/util.js | 62 ++++
test/__snapshots__/index.test.js.snap | 48 ++++
test/_helpers.js | 70 +++++
test/fixtures/basic/dist/bundle.js | 1 +
test/fixtures/basic/dist/index.html | 5 +
test/fixtures/basic/index.html | 8 +
test/fixtures/basic/index.js | 3 +
test/fixtures/factory/dist/bundle.js | 1 +
test/fixtures/factory/dist/index.html | 5 +
test/fixtures/factory/index.html | 8 +
test/fixtures/factory/index.js | 1 +
test/fixtures/failure/index.html | 8 +
test/fixtures/failure/index.js | 1 +
.../function-export-dom/dist/bundle.js | 1 +
.../function-export-dom/dist/index.html | 5 +
test/fixtures/function-export-dom/index.html | 8 +
test/fixtures/function-export-dom/index.js | 9 +
test/fixtures/named-export-fn/dist/bundle.js | 1 +
test/fixtures/named-export-fn/dist/index.html | 5 +
test/fixtures/named-export-fn/index.html | 8 +
test/fixtures/named-export-fn/index.js | 3 +
test/fixtures/value-export/dist/bundle.js | 1 +
test/fixtures/value-export/dist/index.html | 5 +
test/fixtures/value-export/index.html | 8 +
test/fixtures/value-export/index.js | 1 +
test/fixtures/with-field/dist/bundle.js | 1 +
test/fixtures/with-field/dist/index.html | 8 +
test/fixtures/with-field/index.html | 11 +
test/fixtures/with-field/index.js | 1 +
test/index.test.js | 135 +++++++++
37 files changed, 1288 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitignore
create mode 100644 CONTRIBUTING.md
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 package.json
create mode 100644 src/index.js
create mode 100644 src/util.js
create mode 100644 test/__snapshots__/index.test.js.snap
create mode 100644 test/_helpers.js
create mode 100644 test/fixtures/basic/dist/bundle.js
create mode 100644 test/fixtures/basic/dist/index.html
create mode 100644 test/fixtures/basic/index.html
create mode 100644 test/fixtures/basic/index.js
create mode 100644 test/fixtures/factory/dist/bundle.js
create mode 100644 test/fixtures/factory/dist/index.html
create mode 100644 test/fixtures/factory/index.html
create mode 100644 test/fixtures/factory/index.js
create mode 100644 test/fixtures/failure/index.html
create mode 100644 test/fixtures/failure/index.js
create mode 100644 test/fixtures/function-export-dom/dist/bundle.js
create mode 100644 test/fixtures/function-export-dom/dist/index.html
create mode 100644 test/fixtures/function-export-dom/index.html
create mode 100644 test/fixtures/function-export-dom/index.js
create mode 100644 test/fixtures/named-export-fn/dist/bundle.js
create mode 100644 test/fixtures/named-export-fn/dist/index.html
create mode 100644 test/fixtures/named-export-fn/index.html
create mode 100644 test/fixtures/named-export-fn/index.js
create mode 100644 test/fixtures/value-export/dist/bundle.js
create mode 100644 test/fixtures/value-export/dist/index.html
create mode 100644 test/fixtures/value-export/index.html
create mode 100644 test/fixtures/value-export/index.js
create mode 100644 test/fixtures/with-field/dist/bundle.js
create mode 100644 test/fixtures/with-field/dist/index.html
create mode 100644 test/fixtures/with-field/index.html
create mode 100644 test/fixtures/with-field/index.js
create mode 100644 test/index.test.js
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c6c8b36
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..24ca935
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+/node_modules
+/dist
+/package-lock.json
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..bb70059
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f107611
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2018 Google Inc.
+
+ 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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6687a4a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,270 @@
+
+
+
+ prerender-loader
+
+
+
+
+Painless universal prerendering for Webpack. Works great with
+[html-webpack-plugin].
+
+> 🧐 **What is Prerendering?**
+>
+>Pre-rendering describes the process of rendering a client-side application at
+>build time, producing useful static HTML that can be sent to the browser
+>instead of an empty bootstrapping page.
+>
+>Pre-rendering is like Server-Side Rendering, just done at build time to produce
+>static files. Both techniques help get meaningful content onto the user's
+>screen faster.
+
+## Features
+
+- Works entirely within Webpack
+- Integrates with [html-webpack-plugin]
+- Works with `webpack-dev-server` / `webpack serve`
+- Supports both DOM and String prerendering
+- Asynchronous rendering via async/await or Promises
+
+---
+
+
+
+- [Features](#features)
+- [How does it work?](#how-does-it-work)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [DOM Prerendering](#dom-prerendering)
+ - [String Prerendering](#string-prerendering)
+ - [Injecting content into the HTML](#injecting-content-into-the-html)
+ - [Prerendering JavaScript Files](#prerendering-javascript-files)
+- [Options](#options)
+- [License](#license)
+
+
+
+## How does it work?
+
+`prerender-loader` renders your web application within Webpack during builds,
+producing static HTML. When the loader is applied to an HTML file, it creates a
+DOM structure from that HTML, compiles the application, runs it within the DOM
+and serializes the result back to HTML.
+
+---
+
+## Installation
+
+First, install `prerender-loader` as a development dependency:
+
+```sh
+npm i -D prerender-loader
+```
+
+---
+
+## Usage
+
+In most cases, you'll want to apply the loader to your `html-webpack-plugin`
+template option:
+
+```diff
+// webpack.config.js
+module.exports = {
+ plugins: [
+ new HtmlWebpackPlugin({
+- template: 'index.html',
++ template: '!!prerender-loader?string!index.html',
+
+ // any other options you'd normally set are still supported:
+ compile: false,
+ inject: true
+ })
+ ]
+}
+```
+
+What does all that punctuation mean? Let's breaking the whole loader string
+down:
+
+> In Webpack, a module identifier beginning with `!!` will bypass any configured
+>loaders from `module.rules` - here we're saying "don't do anything to
+>`index.html` except what I've defined here
+>
+>The `?string` parameter tells `prerender-loader` to output an ES module
+>exporting the prerendered HTML string, rather than returning the HTML directly.
+>
+>Finally, everything up to the last `!` in a module identifier is the inline
+>loader definition (the transforms to apply to a given module). The filename of
+>the module to load comes after the `!`.
+>
+>**Note:** If you've already set up `html-loader` or `raw-loader` to handle
+>`.html` files, you can skip both options and simply pass a `template` value of
+>`"prerender-loader!index.html"`!
+
+As with any loader, it is also possible to apply `prerender-loader` on-the-fly
+:
+
+```js
+const html = require('prerender-loader?!./app.html');
+```
+
+... or in your Webpack configuration's `module.rules` section:
+
+```js
+module.exports = {
+ module: {
+ rules: [
+ {
+ test: 'src/index.html',
+ loader: 'prerender-loader?string'
+ }
+ ]
+ }
+}
+```
+
+
+Once you have `prerender-loader` in-place, prerendering is now turned on. During
+your build, the app will be executed, with any modifications it makes to
+`index.html` will be saved to disk. This is fine for the needs of many apps,
+but you can also take more explicit control over your prerendering: either using
+the DOM or by rendering to a String.
+
+### DOM Prerendering
+
+During prerendering, your application gets compiled and run directly under
+NodeJS, but within a [JSDOM] container so that you can use the familiar browser
+globals like `document` and `window`.
+
+Here's an example `entry` module that uses DOM prerendering:
+
+```js
+import { render } from 'fancy-dom-library';
+import App from './app';
+
+export default () => {
+ render(, document.body);
+};
+```
+
+In all cases, asynchronous functions and callbacks are supported:
+
+```js
+import { mount } from 'other-fancy-library';
+import app from './app';
+
+export default async function prerender() {
+ let res = await fetch('https://example.com');
+ let data = await res.json();
+ mount(app(data), document.getElementById('app'));
+}
+```
+
+### String Prerendering
+
+It's also possible to export a function from your Webpack entry module, which
+gives you full control over prerendering: `prerender-loader` will call the
+function and its return value will be used as the static HTML. If the exported
+function returns a Promise, it will be awaited and the resolved value will be
+used.
+
+```js
+import { renderToString } from 'react-dom';
+import App from './app';
+
+export default () => {
+ const html = renderToString();
+ // returned HTML will be injected into :
+ return html;
+};
+```
+
+In addition to DOM and String prerendering, it's also possible to use a
+combination of the two. If an application's Webpack entry exports a prerender
+function that doens't return a value, the default DOM serialization will kick
+in, just like in DOM prerendering. This means you can use your exported
+prerender function to trigger DOM manipulation ("client-side" rendering), and
+then just let `prerender-loader` handle generating the static HTML for whatever
+got rendered.
+
+Here's an example that renders a [Preact] application and waits for DOM
+rendering to settle down before allowing prerender-loader to serialize the
+document to static HTML:
+
+```js
+import { h, options } from 'preact';
+import { renderToString } from 'preact';
+import App from './app';
+
+// we're done when there are no renders for 50ms:
+const IDLE_TIMEOUT = 50;
+
+export default () => new Promise(resolve => {
+ let timer;
+ // each time preact re-renders, reset our idle timer:
+ options.debounceRendering = commit => {
+ clearTimeout(timer);
+ timer = setTimeout(resolve, IDLE_TIMEOUT);
+ commit();
+ };
+
+ // render into using normal client-side rendering:
+ render(, document.body);
+});
+```
+
+### Injecting content into the HTML
+
+When applied to a `.html` file, `prerender-loader` will inject prerendered
+content at the end of `` by default. If you want to place the content
+somewhere else, you can add a `{{prerender}}` field:
+
+```html
+
+
+
+
+ {{prerender}}
+
+
+
+```
+
+This works well if you intend to provide a prerender function that only returns
+your application's HTML structure, not the full document's HTML.
+
+### Prerendering JavaScript Files
+
+In addition to processing `.html` files, the loader can also directly pre-render
+`.js` scripts. The only difference is that the DOM used for prerender will be
+initially empty:
+
+```js
+const prerenderedHtml = require('!prerender-loader?string!./app.js');
+```
+
+---
+
+## Options
+
+All options are ... optional.
+
+| Option | Type | Default | Description |
+|------------|---------|---------|-------------|
+| `string` | boolean | false | Output a JS module exporting an HTML String instead of the HTML itself
+| `disabled` | boolean | false | Bypass the loader entirely (but still respect `options.string`)
+| `params` | object | null | Options to pass to your prerender function
+
+
+---
+
+## License
+
+[Apache 2.0](LICENSE)
+
+This is not an official Google product.
+
+[html-webpack-plugin]: https://github.com/jantimon/html-webpack-plugin
+[JSDOM]: https://github.com/jsdom/jsdom
+[Preact]: https://preactjs.com
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..4a4979d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,97 @@
+{
+ "name": "prerender-loader",
+ "version": "1.0.0",
+ "description": "Webpack plugin to inline critical CSS and lazy-load the rest.",
+ "main": "dist/prerender-loader.js",
+ "source": "src/index.js",
+ "license": "Apache-2.0",
+ "author": "The Chromium Authors",
+ "contributors": [
+ {
+ "name": "Jason Miller",
+ "email": "developit@google.com"
+ }
+ ],
+ "keywords": [
+ "pre-render",
+ "prerendering",
+ "webpack plugin",
+ "SSR",
+ "performance",
+ "first contentful paint",
+ "FCP"
+ ],
+ "repository": "GoogleChromeLabs/prerender-loader",
+ "scripts": {
+ "build": "microbundle -f cjs --no-compress --external all",
+ "docs": "documentation readme -q --no-markdown-toc -a public -s Usage --sort-order alpha src",
+ "test": "jest --coverage"
+ },
+ "babel": {
+ "presets": [
+ "env"
+ ]
+ },
+ "jest": {
+ "testEnvironment": "node",
+ "coverageReporters": [
+ "text"
+ ],
+ "collectCoverageFrom": [
+ "src/**/*"
+ ],
+ "watchPathIgnorePatterns": [
+ "node_modules",
+ "dist"
+ ]
+ },
+ "devDependencies": {
+ "babel-core": "^6.26.3",
+ "babel-jest": "^23.0.0",
+ "babel-preset-env": "^1.7.0",
+ "documentation": "^7.0.0",
+ "eslint": "^4.19.1",
+ "eslint-config-standard": "^11.0.0",
+ "eslint-plugin-import": "^2.12.0",
+ "eslint-plugin-jest": "^21.15.2",
+ "eslint-plugin-node": "^6.0.1",
+ "eslint-plugin-promise": "^3.8.0",
+ "eslint-plugin-standard": "^3.1.0",
+ "file-loader": "^1.1.11",
+ "html-webpack-plugin": "^3.2.0",
+ "jest": "^23.0.0",
+ "memory-fs": "^0.4.1",
+ "microbundle": "^0.4.4",
+ "raw-loader": "^0.5.1",
+ "webpack": "^4.8.3"
+ },
+ "dependencies": {
+ "jsdom": "^11.11.0",
+ "loader-utils": "^1.1.0"
+ },
+ "peerDependencies": {
+ "webpack": "*",
+ "memory-fs": "*"
+ },
+ "eslintConfig": {
+ "extends": [
+ "standard",
+ "plugin:jest/recommended"
+ ],
+ "env": {
+ "browser": false,
+ "node": true
+ },
+ "rules": {
+ "indent": [
+ 2,
+ 2
+ ],
+ "semi": [
+ 2,
+ "always"
+ ],
+ "prefer-const": 1
+ }
+ }
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..7df7ad5
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,247 @@
+/**
+ * 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 os from 'os';
+import path from 'path';
+import jsdom from 'jsdom';
+import loaderUtils from 'loader-utils';
+import LibraryTemplatePlugin from 'webpack/lib/LibraryTemplatePlugin';
+import NodeTemplatePlugin from 'webpack/lib/node/NodeTemplatePlugin';
+import NodeTargetPlugin from 'webpack/lib/node/NodeTargetPlugin';
+import SingleEntryPlugin from 'webpack/lib/SingleEntryPlugin';
+import { DefinePlugin } from 'webpack';
+import MemoryFs from 'memory-fs';
+import { runChildCompiler, getRootCompiler, getBestModuleExport, stringToModule } from './util';
+
+// Used to annotate this plugin's hooks in Tappable invocations
+const PLUGIN_NAME = 'prerender-loader';
+
+// Internal name used for the output bundle (never written to disk)
+const FILENAME = 'ssr-bundle.js';
+
+// Searches for fields of the form {{prerender}} or {{prerender:./some/module}}
+const PRERENDER_REG = /\{\{prerender(?::\s*([^}]+?)\s*)?\}\}/;
+
+/**
+ * prerender-loader can be applied to any HTML or JS file with the given options.
+ * @public
+ * @param {Options} options Options to control how Critters inlines CSS.
+ *
+ * @example
+ * // webpack.config.js
+ * module.exports = {
+ * plugins: [
+ * new HtmlWebpackPlugin({
+ * // `!!` tells webpack to skip any configured loaders for .html files
+ * // `?string` tells prerender-loader output a JS module exporting the HTML string
+ * template: '!!prerender-loader?string!index.html'
+ * })
+ * ]
+ * }
+ *
+ * @example
+ * // inline demo: assumes you have html-loader set up:
+ * import prerenderedHtml from '!prerender-loader!./file.html';
+ */
+export default function PrerenderLoader (content) {
+ const options = loaderUtils.getOptions(this) || {};
+ const outputFilter = options.as === 'string' || options.string ? stringToModule : String;
+
+ if (options.disabled === true) {
+ return outputFilter(content);
+ }
+
+ // When applied to HTML, attempts to inject into a specified {{prerender}} field.
+ // @note: this is only used when the entry module exports a String or function
+ // that resolves to a String, otherwise the whole document is serialized.
+ let inject = false;
+ if (!this.request.match(/.(js|ts)x?$/i)) {
+ const matches = content.match(PRERENDER_REG);
+ if (matches) {
+ inject = true;
+ options.entry = matches[1];
+ }
+ options.templateContent = content;
+ }
+
+ const callback = this.async();
+
+ prerender(this._compilation, this.request, options, inject, this)
+ .then(output => {
+ callback(null, outputFilter(output));
+ })
+ .catch(err => {
+ // console.error(err);
+ callback(err);
+ });
+}
+
+async function prerender (parentCompilation, request, options, inject, loader) {
+ const parentCompiler = getRootCompiler(parentCompilation.compiler);
+ const context = parentCompiler.options.context || process.cwd();
+ const entry = './' + ((options.entry && [].concat(options.entry).pop().trim()) || path.relative(context, parentCompiler.options.entry));
+
+ const outputOptions = {
+ // fix: some plugins ignore/bypass outputfilesystem, so use a temp directory and ignore any writes.
+ path: os.tmpdir(),
+ filename: FILENAME
+ };
+
+ // Only copy over mini-extract-text-plugin (excluding it breaks extraction entirely)
+ const plugins = (parentCompiler.options.plugins || []).filter(c => /MiniCssExtractPlugin/i.test(c.constructor.name));
+
+ // Compile to an in-memory filesystem since we just want the resulting bundled code as a string
+ const compiler = parentCompilation.createChildCompiler('prerender', outputOptions, plugins);
+ compiler.context = parentCompiler.context;
+ compiler.outputFileSystem = new MemoryFs();
+
+ // Define PRERENDER to be true within the SSR bundle
+ new DefinePlugin({
+ PRERENDER: 'true'
+ }).apply(compiler);
+
+ // ... then define PRERENDER to be false within the client bundle
+ new DefinePlugin({
+ PRERENDER: 'false'
+ }).apply(parentCompiler);
+
+ // Compile to CommonJS to be executed by Node
+ new NodeTemplatePlugin(outputOptions).apply(compiler);
+ new NodeTargetPlugin().apply(compiler);
+
+ new LibraryTemplatePlugin('PRERENDER_RESULT', 'var').apply(compiler);
+
+ // Kick off compilation at our entry module (either the parent compiler's entry or a custom one defined via `{{prerender:entry.js}}`)
+ new SingleEntryPlugin(context, entry, undefined).apply(compiler);
+
+ // Set up cache inheritance for the child compiler
+ const subCache = 'subcache ' + request;
+ function addChildCache (compilation, data) {
+ if (compilation.cache) {
+ if (!compilation.cache[subCache]) compilation.cache[subCache] = {};
+ compilation.cache = compilation.cache[subCache];
+ }
+ }
+ if (compiler.hooks) {
+ compiler.hooks.compilation.tap(PLUGIN_NAME, addChildCache);
+ } else {
+ compiler.plugin('compilation', addChildCache);
+ }
+
+ const compilation = await runChildCompiler(compiler);
+ let result;
+ let dom, window, injectParent, injectNextSibling;
+
+ if (compilation.assets[compilation.options.output.filename]) {
+ // Get the compiled main bundle
+ const output = compilation.assets[compilation.options.output.filename].source();
+
+ // @TODO: provide a non-DOM option to allow turning off JSDOM entirely.
+
+ const tpl = options.templateContent || '';
+ dom = new jsdom.JSDOM(tpl.replace(PRERENDER_REG, ''), {
+ // suppress console-proxied eval() errors, but keep console proxying
+ virtualConsole: new jsdom.VirtualConsole({ omitJSDOMErrors: false }).sendTo(console),
+
+ // don't track source locations for performance reasons
+ includeNodeLocations: false,
+
+ // don't allow inline event handlers & script tag exec
+ runScripts: 'outside-only'
+ });
+ window = dom.window;
+
+ // Find the placeholder node for injection & remove it
+ const injectPlaceholder = window.document.getElementById('PRERENDER_INJECT');
+ if (injectPlaceholder) {
+ injectParent = injectPlaceholder.parentNode;
+ injectNextSibling = injectPlaceholder.nextSibling;
+ injectPlaceholder.remove();
+ }
+
+ // These are missing from JSDOM
+ let counter = 0;
+ window.requestAnimationFrame = () => ++counter;
+ window.cancelAnimationFrame = () => { };
+
+ // Inject a require shim
+ window.require = moduleId => {
+ const asset = compilation.assets[moduleId.replace(/^\.?\//g, '')];
+ if (!asset) {
+ throw Error(`Error: Module not found. attempted require("${moduleId}")`);
+ }
+ const mod = { exports: {} };
+ window.eval(`(function(exports, module, require){\n${asset.source()}\n})`)(mod.exports, mod, window.require);
+ return mod.exports;
+ };
+
+ // Invoke the SSR bundle within the JSDOM document and grab the exported/returned result
+ result = window.eval(output + '\nPRERENDER_RESULT');
+ }
+
+ // Deal with ES Module exports (just use the best guess):
+ if (result && typeof result === 'object') {
+ result = getBestModuleExport(result);
+ }
+
+ if (typeof result === 'function') {
+ result = result(options.params || null);
+ }
+
+ // The entry can export or return a Promise in order to perform fully async prerendering:
+ if (result && result.then) {
+ result = await result;
+ }
+
+ // Returning or resolving to `null` / `undefined` defaults to serializing the whole document.
+ // Note: this pypasses `inject` because the document is already derived from the template.
+ if (result !== undefined && options.templateContent) {
+ const template = window.document.createElement('template');
+ template.innerHTML = result || '';
+ const content = template.content || template;
+ const parent = injectParent || window.document.body;
+ let child;
+ while ((child = content.firstChild)) {
+ parent.insertBefore(child, injectNextSibling || null);
+ }
+ } else if (inject) {
+ // Otherwise inject the prerendered HTML into the template
+ return options.templateContent.replace(PRERENDER_REG, result || '');
+ }
+
+ // dom.serialize() doesn't properly serialize HTML appended to document.body.
+ return dom.serialize();
+ // return `${window.document.documentElement.outerHTML}`;
+
+ // // Returning or resolving to `null` / `undefined` defaults to serializing the whole document.
+ // // Note: this pypasses `inject` because the document is already derived from the template.
+ // if (result == null && dom) {
+ // // result = dom.serialize();
+ // } else if (inject) {
+ // // @TODO determine if this is really worthwhile/necessary for the string return case
+ // if (injectParent || options.templateContent) {
+ // console.log(injectParent.outerHTML);
+ // (injectParent || document.body).insertAdjacentHTML('beforeend', result || '');
+ // // result = dom.serialize();
+ // } else {
+ // // Otherwise inject the prerendered HTML into the template
+ // return options.templateContent.replace(PRERENDER_REG, result || '');
+ // }
+ // }
+
+ // return dom.serialize();
+ // return result;
+}
diff --git a/src/util.js b/src/util.js
new file mode 100644
index 0000000..5d0a3eb
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,62 @@
+/**
+ * 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.
+ */
+
+/**
+ * Promisified version of compiler.runAsChild() with error hoisting and isolated output/assets.
+ * (runAsChild() merges assets into the parent compilation, we don't want that)
+ */
+export function runChildCompiler (compiler) {
+ return new Promise((resolve, reject) => {
+ compiler.compile((err, compilation) => {
+ // still allow the parent compiler to track execution of the child:
+ compiler.parentCompilation.children.push(compilation);
+ if (err) return reject(err);
+
+ // Bubble stat errors up and reject the Promise:
+ if (compilation.errors && compilation.errors.length) {
+ const errorDetails = compilation.errors.map(error => error.details).join('\n');
+ return reject(Error('Child compilation failed:\n' + errorDetails));
+ }
+
+ resolve(compilation);
+ });
+ });
+}
+
+/** Crawl up the compiler tree and return the outermost compiler instance */
+export function getRootCompiler (compiler) {
+ while (compiler.parentCompilation && compiler.parentCompilation.compiler) {
+ compiler = compiler.parentCompilation.compiler;
+ }
+ return compiler;
+}
+
+/** Find the best possible export for an ES Module. Returns `undefined` for no exports. */
+export function getBestModuleExport (exports) {
+ if (exports.default) {
+ return exports.default;
+ }
+ for (const prop in exports) {
+ if (prop !== '__esModule') {
+ return exports[prop];
+ }
+ }
+}
+
+/** Wrap a String up into an ES Module that exports it */
+export function stringToModule (str) {
+ return 'export default ' + JSON.stringify(str);
+}
diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000..0e6853f
--- /dev/null
+++ b/test/__snapshots__/index.test.js.snap
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`prerender-loader!x.html Imperative DOM, no {{prerender}} field should return serialized HTML 1`] = `
+"
+Basic Demo
+
+
+this counts as SSR
"
+`;
+
+exports[`prerender-loader!x.html default export function, no {{prerender}} field should inject returned HTML into 1`] = `
+"
+export factory returning HTML
+
+
+some returned HTML
"
+`;
+
+exports[`prerender-loader!x.html default export function, with {{prerender}} field should inject returned HTML in place of {{prerender}} 1`] = `
+"
+prerender field demo
+
+
+
+"
+`;
+
+exports[`prerender-loader!x.html exported function with no return value should serialize the whole document 1`] = `
+"
+function export with default DOM serialization
+
+
+content injected into the dom
"
+`;
+
+exports[`webpack compilation smoke test (no prerendering) should compile 1`] = `
+"
+
+
+Basic Demo
+
+
+
+
+"
+`;
diff --git a/test/_helpers.js b/test/_helpers.js
new file mode 100644
index 0000000..3388e09
--- /dev/null
+++ b/test/_helpers.js
@@ -0,0 +1,70 @@
+/**
+ * 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 { promisify } from 'util';
+import jsdom from 'jsdom';
+import fs from 'fs';
+import path from 'path';
+import webpack from 'webpack';
+
+const DOMParser = new jsdom.JSDOM(``, { includeNodeLocations: false }).window.DOMParser;
+
+// parse a string into a JSDOM Document
+export const parseDom = html => new DOMParser().parseFromString(html, 'text/html');
+
+// returns a promise resolving to the contents of a file
+export const readFile = file => promisify(fs.readFile)(path.resolve(__dirname, file), 'utf8');
+
+// invoke webpack on a given entry module, optionally mutating the default configuration
+export function compile (entry, configDecorator) {
+ return new Promise((resolve, reject) => {
+ const context = path.dirname(path.resolve(__dirname, entry));
+ entry = path.basename(entry);
+ let config = {
+ context,
+ entry: path.resolve(context, entry),
+ output: {
+ path: path.resolve(__dirname, path.resolve(context, 'dist')),
+ filename: 'bundle.js',
+ chunkFilename: '[name].chunk.js'
+ },
+ resolveLoader: {
+ modules: [path.resolve(__dirname, '../node_modules')]
+ },
+ module: {
+ rules: []
+ },
+ plugins: []
+ };
+ if (configDecorator) {
+ config = configDecorator(config) || config;
+ }
+ webpack(config, (err, stats) => {
+ if (err) return reject(err);
+ const info = stats.toJson();
+ if (stats.hasErrors()) return reject(info.errors.join('\n'));
+ resolve(info);
+ });
+ });
+}
+
+// invoke webpack via compile(), injecting `html` and `document` properties into the webpack build info.
+export async function compileToHtml (fixture, configDecorator, crittersOptions = {}) {
+ const info = await compile(`fixtures/${fixture}/index.js`, configDecorator);
+ info.html = await readFile(`fixtures/${fixture}/dist/index.html`);
+ info.document = parseDom(info.html);
+ return info;
+}
diff --git a/test/fixtures/basic/dist/bundle.js b/test/fixtures/basic/dist/bundle.js
new file mode 100644
index 0000000..33a1d83
--- /dev/null
+++ b/test/fixtures/basic/dist/bundle.js
@@ -0,0 +1 @@
+!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){var n=document.createElement("div");n.appendChild(document.createTextNode("this counts as SSR")),document.body.appendChild(n)}]);
\ No newline at end of file
diff --git a/test/fixtures/basic/dist/index.html b/test/fixtures/basic/dist/index.html
new file mode 100644
index 0000000..47871f9
--- /dev/null
+++ b/test/fixtures/basic/dist/index.html
@@ -0,0 +1,5 @@
+
+Basic Demo
+
+
+this counts as SSR
\ No newline at end of file
diff --git a/test/fixtures/basic/index.html b/test/fixtures/basic/index.html
new file mode 100644
index 0000000..45e9abe
--- /dev/null
+++ b/test/fixtures/basic/index.html
@@ -0,0 +1,8 @@
+
+
+
+ Basic Demo
+
+
+
+
diff --git a/test/fixtures/basic/index.js b/test/fixtures/basic/index.js
new file mode 100644
index 0000000..2e1e3d3
--- /dev/null
+++ b/test/fixtures/basic/index.js
@@ -0,0 +1,3 @@
+var div = document.createElement('div');
+div.appendChild(document.createTextNode('this counts as SSR'));
+document.body.appendChild(div);
diff --git a/test/fixtures/factory/dist/bundle.js b/test/fixtures/factory/dist/bundle.js
new file mode 100644
index 0000000..e990e8b
--- /dev/null
+++ b/test/fixtures/factory/dist/bundle.js
@@ -0,0 +1 @@
+!function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r),r.default=(()=>"some returned HTML
")}]);
\ No newline at end of file
diff --git a/test/fixtures/factory/dist/index.html b/test/fixtures/factory/dist/index.html
new file mode 100644
index 0000000..67f5509
--- /dev/null
+++ b/test/fixtures/factory/dist/index.html
@@ -0,0 +1,5 @@
+
+export factory returning HTML
+
+
+some returned HTML
\ No newline at end of file
diff --git a/test/fixtures/factory/index.html b/test/fixtures/factory/index.html
new file mode 100644
index 0000000..791aa9a
--- /dev/null
+++ b/test/fixtures/factory/index.html
@@ -0,0 +1,8 @@
+
+
+
+ export factory returning HTML
+
+
+
+
diff --git a/test/fixtures/factory/index.js b/test/fixtures/factory/index.js
new file mode 100644
index 0000000..3e6a67d
--- /dev/null
+++ b/test/fixtures/factory/index.js
@@ -0,0 +1 @@
+export default () => `some returned HTML
`;
diff --git a/test/fixtures/failure/index.html b/test/fixtures/failure/index.html
new file mode 100644
index 0000000..f6e7990
--- /dev/null
+++ b/test/fixtures/failure/index.html
@@ -0,0 +1,8 @@
+
+
+
+ Failure Demo
+
+
+
+
diff --git a/test/fixtures/failure/index.js b/test/fixtures/failure/index.js
new file mode 100644
index 0000000..1efa30b
--- /dev/null
+++ b/test/fixtures/failure/index.js
@@ -0,0 +1 @@
+export { default } from './does-not-exist';
diff --git a/test/fixtures/function-export-dom/dist/bundle.js b/test/fixtures/function-export-dom/dist/bundle.js
new file mode 100644
index 0000000..86aa688
--- /dev/null
+++ b/test/fixtures/function-export-dom/dist/bundle.js
@@ -0,0 +1 @@
+!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t),t.default=(()=>new Promise(e=>{var t=document.createElement("div");t.appendChild(document.createTextNode("content injected into the dom")),document.body.appendChild(t),setTimeout(()=>{e()},10)}))}]);
\ No newline at end of file
diff --git a/test/fixtures/function-export-dom/dist/index.html b/test/fixtures/function-export-dom/dist/index.html
new file mode 100644
index 0000000..704919f
--- /dev/null
+++ b/test/fixtures/function-export-dom/dist/index.html
@@ -0,0 +1,5 @@
+
+function export with default DOM serialization
+
+
+content injected into the dom
\ No newline at end of file
diff --git a/test/fixtures/function-export-dom/index.html b/test/fixtures/function-export-dom/index.html
new file mode 100644
index 0000000..cf1ab03
--- /dev/null
+++ b/test/fixtures/function-export-dom/index.html
@@ -0,0 +1,8 @@
+
+
+
+ function export with default DOM serialization
+
+
+
+
diff --git a/test/fixtures/function-export-dom/index.js b/test/fixtures/function-export-dom/index.js
new file mode 100644
index 0000000..9651efc
--- /dev/null
+++ b/test/fixtures/function-export-dom/index.js
@@ -0,0 +1,9 @@
+export default () => new Promise(resolve => {
+ var div = document.createElement('div');
+ div.appendChild(document.createTextNode('content injected into the dom'));
+ document.body.appendChild(div);
+
+ setTimeout(() => {
+ resolve();
+ }, 10);
+});
diff --git a/test/fixtures/named-export-fn/dist/bundle.js b/test/fixtures/named-export-fn/dist/bundle.js
new file mode 100644
index 0000000..f88ebd3
--- /dev/null
+++ b/test/fixtures/named-export-fn/dist/bundle.js
@@ -0,0 +1 @@
+!function(e){var r={};function n(t){if(r[t])return r[t].exports;var o=r[t]={i:t,l:!1,exports:{}};return e[t].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=r,n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},n.p="",n(n.s=0)}([function(e,r,n){"use strict";function t(){return"some HTML returned from prerender()
"}n.r(r),n.d(r,"prerender",function(){return t})}]);
\ No newline at end of file
diff --git a/test/fixtures/named-export-fn/dist/index.html b/test/fixtures/named-export-fn/dist/index.html
new file mode 100644
index 0000000..eecda0a
--- /dev/null
+++ b/test/fixtures/named-export-fn/dist/index.html
@@ -0,0 +1,5 @@
+
+named export factory
+
+
+some HTML returned from prerender()
\ No newline at end of file
diff --git a/test/fixtures/named-export-fn/index.html b/test/fixtures/named-export-fn/index.html
new file mode 100644
index 0000000..e2cb338
--- /dev/null
+++ b/test/fixtures/named-export-fn/index.html
@@ -0,0 +1,8 @@
+
+
+
+ named export factory
+
+
+
+
diff --git a/test/fixtures/named-export-fn/index.js b/test/fixtures/named-export-fn/index.js
new file mode 100644
index 0000000..98dae02
--- /dev/null
+++ b/test/fixtures/named-export-fn/index.js
@@ -0,0 +1,3 @@
+export function prerender () {
+ return `some HTML returned from prerender()
`;
+}
diff --git a/test/fixtures/value-export/dist/bundle.js b/test/fixtures/value-export/dist/bundle.js
new file mode 100644
index 0000000..3985ac8
--- /dev/null
+++ b/test/fixtures/value-export/dist/bundle.js
@@ -0,0 +1 @@
+!function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r),t.d(r,"PRERENDERED_HTML",function(){return n});const n=Promise.resolve("this is the resolved value of PRERENDERED_HTML
")}]);
\ No newline at end of file
diff --git a/test/fixtures/value-export/dist/index.html b/test/fixtures/value-export/dist/index.html
new file mode 100644
index 0000000..553270d
--- /dev/null
+++ b/test/fixtures/value-export/dist/index.html
@@ -0,0 +1,5 @@
+
+value export
+
+
+this is the resolved value of PRERENDERED_HTML
\ No newline at end of file
diff --git a/test/fixtures/value-export/index.html b/test/fixtures/value-export/index.html
new file mode 100644
index 0000000..6087486
--- /dev/null
+++ b/test/fixtures/value-export/index.html
@@ -0,0 +1,8 @@
+
+
+
+ value export
+
+
+
+
diff --git a/test/fixtures/value-export/index.js b/test/fixtures/value-export/index.js
new file mode 100644
index 0000000..d2293fe
--- /dev/null
+++ b/test/fixtures/value-export/index.js
@@ -0,0 +1 @@
+export const PRERENDERED_HTML = Promise.resolve(`this is the resolved value of PRERENDERED_HTML
`);
diff --git a/test/fixtures/with-field/dist/bundle.js b/test/fixtures/with-field/dist/bundle.js
new file mode 100644
index 0000000..0b2b4aa
--- /dev/null
+++ b/test/fixtures/with-field/dist/bundle.js
@@ -0,0 +1 @@
+!function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";t.r(r),r.default=(()=>"more returned HTML
")}]);
\ No newline at end of file
diff --git a/test/fixtures/with-field/dist/index.html b/test/fixtures/with-field/dist/index.html
new file mode 100644
index 0000000..be9eec4
--- /dev/null
+++ b/test/fixtures/with-field/dist/index.html
@@ -0,0 +1,8 @@
+
+prerender field demo
+
+
+
+
\ No newline at end of file
diff --git a/test/fixtures/with-field/index.html b/test/fixtures/with-field/index.html
new file mode 100644
index 0000000..6ea056b
--- /dev/null
+++ b/test/fixtures/with-field/index.html
@@ -0,0 +1,11 @@
+
+
+
+ prerender field demo
+
+
+
+ {{prerender}}
+
+
+
diff --git a/test/fixtures/with-field/index.js b/test/fixtures/with-field/index.js
new file mode 100644
index 0000000..60694e9
--- /dev/null
+++ b/test/fixtures/with-field/index.js
@@ -0,0 +1 @@
+export default () => `more returned HTML
`;
diff --git a/test/index.test.js b/test/index.test.js
new file mode 100644
index 0000000..241297f
--- /dev/null
+++ b/test/index.test.js
@@ -0,0 +1,135 @@
+/**
+ * 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, 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({
+ filename: 'index.html',
+ template,
+ 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 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 () => {
+ const { html } = await compileToHtml('basic', configure({ disabled: true, string: true }));
+ expect(html).not.toMatch(/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(/
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', '
this counts as SSR
');
+ expect(html).toMatchSnapshot();
+ });
+ });
+
+ describe('default export function, no {{prerender}} field', () => {
+ it('should inject returned HTML into ', async () => {
+ const { html, document } = await compileToHtml('factory', withPrerender);
+
+ // verify that our DOM-generated content has been prerendered into the static HTML:
+ expect(html).toMatch(/
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', '
some returned HTML
');
+ expect(html).toMatchSnapshot();
+ });
+ });
+
+ describe('default export function, with {{prerender}} field', () => {
+ //
some returned HTML
+ 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(/
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*
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(/
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(/
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(/
content injected into the dom<\/div>/);
+ expect(document.body.children).toHaveLength(2);
+ expect(html).toMatchSnapshot();
+ });
+ });
+});