├── .babelrc
├── .editorconfig
├── .gitignore
├── .npmignore
├── .prettierrc
├── .travis.yml
├── .vscode
└── settings.json
├── README.md
├── jsconfig.json
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
└── index.js
└── test
├── helpers.js
├── it-emits-html-file
├── App.svelte
└── expected.html
├── it-puts-styles-before-html-by-default
├── App.svelte
└── expected.html
├── it-respects-configure-export-option
├── App.svelte
└── expected.html
├── it-respects-preprocess-html-css-options
├── App.svelte
└── expected.html
├── it-respects-props
├── App.svelte
└── expected.html
├── it-skips-emit
└── App.svelte
├── it-throws-error-for-non-cjs-format
└── App.svelte
├── it-throws-error-if-no-filename-passed
└── App.svelte
├── it-uses-filename-function
└── App.svelte
└── tests.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env"],
3 | "plugins": ["@babel/plugin-transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | insert_final_newline = true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | test/**/dist
3 | dist
4 | examples
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .babelrc
2 | .editorconfig
3 | .prettierrc
4 | jsconfig.jsconfig
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "useTabs": false,
5 | "tabWidth": 2,
6 | "singleQuote": false,
7 | "printWidth": 100
8 | }
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "lts/*"
5 |
6 | notifications:
7 | email: true
8 |
9 | install:
10 | - npm ci
11 |
12 | stages:
13 | - test
14 | - release
15 |
16 | jobs:
17 | include:
18 | - stage: test
19 | name: "tests"
20 | script:
21 | - npm run test
22 | - stage: release
23 | name: "release"
24 | if: branch = master
25 | script:
26 | - npm run build
27 | - npm run semantic-release
28 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.renderWhitespace": "all",
4 | "editor.rulers": [80, 100]
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rollup-plugin-svelte-ssr
2 |
3 | [](https://travis-ci.org/akaSybe/rollup-plugin-svelte-ssr)
4 | [](https://github.com/semantic-release/semantic-release)
5 | [](http://commitizen.github.io/cz-cli/)
6 |
7 | Server-side rendering of Svelte app at build-time using Rollup plugin
8 |
9 | ## Basic example
10 |
11 | Let's assume that we have basic svelte component `src/App.svelte`:
12 |
13 | ```svelte
14 |
17 |
18 |
{name}
19 |
20 |
25 | ```
26 |
27 | Let's use `rollup-plugin-svelte-ssr` in `rollup.config.js`:
28 |
29 | ```js
30 | // ... other imports
31 |
32 | import ssr from "rollup-plugin-svelte-ssr";
33 |
34 | export default {
35 | input: "src/App.svelte",
36 | output: {
37 | format: "cjs",
38 | file: "dist/ssr.js"
39 | },
40 | plugins: [
41 | svelte({
42 | generate: "ssr"
43 | }),
44 |
45 | // ... other plugins
46 |
47 | ssr({
48 | fileName: 'ssr.html',
49 | props: {
50 | name: 'Hello',
51 | }
52 | })
53 | ]
54 | }
55 | ```
56 |
57 | In `dist` directory we get `ssr.html` that contains SSR-ed app:
58 |
59 | ```html
60 | Hello
61 | ```
62 |
63 | ## Options
64 |
65 | ```js
66 | ssr({
67 | // allow to set output file name
68 | fileName: 'ssr.html',
69 | // or
70 | // where entry is Rollup entry
71 | fileName: function(entry) {
72 | return "ssr.html"
73 | }
74 | // root component props
75 | props: {
76 | name: 'Hello',
77 | },
78 | // allow to skip emit of js file
79 | skipEmit: false,
80 | // allow to preprocess html
81 | preprocessHtml: function(html) {
82 | return html;
83 | },
84 | // allow to preprocess css
85 | preprocessCss: function(css) {
86 | return css;
87 | },
88 | // customize output
89 | configureExport: function(html, css) {
90 | return `${html}`;
91 | }
92 | })
93 | ```
94 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "typeAcquisition": {
3 | "include": ["jest"]
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-svelte-ssr",
3 | "version": "1.0.3",
4 | "description": "Server-side rendering of Svelte app at build-time using Rollup plugin",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "build": "rollup -c",
8 | "watch": "rollup -c -w",
9 | "test": "jest",
10 | "semantic-release": "semantic-release",
11 | "cz": "git-cz"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/akaSybe/rollup-plugin-svelte-ssr.git"
16 | },
17 | "keywords": [
18 | "svelte",
19 | "ssr",
20 | "rollup-plugin"
21 | ],
22 | "author": "Aleksandr Shestakov",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/akaSybe/rollup-plugin-svelte-ssr/issues"
26 | },
27 | "homepage": "https://github.com/akaSybe/rollup-plugin-svelte-ssr#readme",
28 | "dependencies": {
29 | "@semantic-release/git": "^7.0.16"
30 | },
31 | "devDependencies": {
32 | "@babel/core": "^7.6.4",
33 | "@babel/plugin-transform-runtime": "^7.6.2",
34 | "@babel/preset-env": "^7.6.3",
35 | "@babel/runtime": "^7.6.3",
36 | "@types/jest": "^24.0.18",
37 | "cz-conventional-changelog": "^3.0.2",
38 | "del": "^5.1.0",
39 | "husky": "^3.0.9",
40 | "jest": "^24.9.0",
41 | "prettier": "^1.18.2",
42 | "rollup": "^1.23.1",
43 | "rollup-plugin-commonjs": "^10.1.0",
44 | "rollup-plugin-filesize": "^6.2.0",
45 | "rollup-plugin-node-resolve": "^5.2.0",
46 | "rollup-plugin-progress": "^1.1.1",
47 | "rollup-plugin-svelte": "^5.1.0",
48 | "semantic-release": "^15.13.24",
49 | "svelte": "^3.12.1"
50 | },
51 | "files": [
52 | "dist"
53 | ],
54 | "jest": {
55 | "testMatch": [
56 | "/test/**/tests.js"
57 | ],
58 | "moduleFileExtensions": [
59 | "js"
60 | ]
61 | },
62 | "config": {
63 | "commitizen": {
64 | "path": "./node_modules/cz-conventional-changelog"
65 | }
66 | },
67 | "husky": {
68 | "hooks": {
69 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook"
70 | }
71 | },
72 | "release": {
73 | "plugins": [
74 | "@semantic-release/commit-analyzer",
75 | "@semantic-release/release-notes-generator",
76 | "@semantic-release/npm",
77 | "@semantic-release/github",
78 | [
79 | "@semantic-release/git",
80 | {
81 | "assets": [
82 | "package.json"
83 | ],
84 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
85 | }
86 | ]
87 | ]
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from "rollup-plugin-node-resolve";
2 | import filesize from "rollup-plugin-filesize";
3 | import progress from "rollup-plugin-progress";
4 | import commonjs from "rollup-plugin-commonjs";
5 |
6 | export default {
7 | input: "src/index.js",
8 | output: {
9 | file: "dist/index.js",
10 | format: "cjs",
11 | },
12 | external: ["fs", "path", "vm"],
13 | plugins: [progress(), resolve(), commonjs(), filesize()],
14 | };
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import vm from "vm";
2 | import fs from "fs";
3 | import path from "path";
4 |
5 | function wrapModuleExports(code) {
6 | return `
7 | function getModuleExports() {
8 | const module = {};
9 |
10 | ${code}
11 |
12 | return module.exports;
13 | }
14 | `;
15 | }
16 |
17 | function defaultExport(html, css) {
18 | const style = css ? `` : "";
19 | return `${style}${html}`;
20 | }
21 |
22 | const defaultOptions = {
23 | /** do not emit SSR bundle */
24 | skipEmit: false,
25 | configureExport: defaultExport,
26 | };
27 |
28 | /** */
29 | export default function ssr(options = {}) {
30 | const pluginOptions = {
31 | ...defaultOptions,
32 | ...options,
33 | };
34 |
35 | if (!pluginOptions.fileName) {
36 | throw new Error("options.fileName should be string or function");
37 | }
38 |
39 | return {
40 | name: "svelte-ssr",
41 | async generateBundle(config, bundle, isWrite) {
42 | if (config.format !== "cjs") {
43 | throw new Error("rollup-plugin-svelte-ssr can only be used with 'cjs'-format");
44 | }
45 |
46 | const destPath = path.relative("./", config.file);
47 | const destDir = destPath.slice(0, destPath.lastIndexOf(path.sep));
48 |
49 | Object.keys(bundle).forEach(async key => {
50 | const entry = bundle[key];
51 |
52 | const sandbox = {
53 | ssr: {
54 | html: "",
55 | css: "",
56 | },
57 | };
58 |
59 | try {
60 | const props = JSON.stringify(pluginOptions.props, null, 2);
61 | const generateSsrScript = `
62 | ${wrapModuleExports(entry.code)}
63 | const App = getModuleExports();
64 | const { html, css } = App.render(${props});
65 | ssr.html = html;
66 | ssr.css = css.code;
67 | `;
68 | const script = new vm.Script(generateSsrScript);
69 | script.runInNewContext(sandbox);
70 | } catch (e) {
71 | throw e;
72 | }
73 |
74 | const html =
75 | typeof pluginOptions.preprocessHtml === "function"
76 | ? pluginOptions.preprocessHtml(sandbox.ssr.html)
77 | : sandbox.ssr.html;
78 |
79 | const css =
80 | typeof pluginOptions.preprocessCss === "function"
81 | ? pluginOptions.preprocessCss(sandbox.ssr.css)
82 | : sandbox.ssr.css;
83 |
84 | const fileName =
85 | typeof pluginOptions.fileName === "function"
86 | ? pluginOptions.fileName(entry)
87 | : pluginOptions.fileName;
88 |
89 | const destination = path.resolve(destDir, fileName);
90 | fs.mkdirSync(path.dirname(destination), { recursive: true });
91 | fs.writeFileSync(destination, pluginOptions.configureExport(html, css));
92 |
93 | if (pluginOptions.skipEmit) {
94 | // You can prevent files from being emitted by deleting them from the bundle object.
95 | delete bundle[key];
96 | }
97 | });
98 | },
99 | };
100 | }
101 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | import { rollup } from "rollup";
4 | import svelte from "rollup-plugin-svelte";
5 | import resolve from "rollup-plugin-node-resolve";
6 | import commonjs from "rollup-plugin-commonjs";
7 |
8 | export function resolvePath(testName, fileName) {
9 | return path.resolve(__dirname, testName, fileName);
10 | }
11 |
12 | export function resolveExpectedFile(testName) {
13 | return resolvePath(testName, "expected.html");
14 | }
15 |
16 | export async function bundleWithRollup({ plugin, pluginOptions, testName, output, format = "cjs" }) {
17 | const bundle = await rollup({
18 | input: resolvePath(testName, "App.svelte"),
19 | plugins: [
20 | svelte({
21 | generate: "ssr",
22 | }),
23 | resolve({
24 | browser: true,
25 | dedupe: importee => importee === "svelte" || importee.startsWith("svelte/"),
26 | }),
27 | commonjs(),
28 | plugin(pluginOptions),
29 | ],
30 | });
31 |
32 | await bundle.write({
33 | format,
34 | file: output,
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/test/it-emits-html-file/App.svelte:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/test/it-emits-html-file/expected.html:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/test/it-puts-styles-before-html-by-default/App.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | Hello
8 |
--------------------------------------------------------------------------------
/test/it-puts-styles-before-html-by-default/expected.html:
--------------------------------------------------------------------------------
1 | Hello
--------------------------------------------------------------------------------
/test/it-respects-configure-export-option/App.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | Hello
8 |
--------------------------------------------------------------------------------
/test/it-respects-configure-export-option/expected.html:
--------------------------------------------------------------------------------
1 | Hello
--------------------------------------------------------------------------------
/test/it-respects-preprocess-html-css-options/App.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | Hello
8 |
--------------------------------------------------------------------------------
/test/it-respects-preprocess-html-css-options/expected.html:
--------------------------------------------------------------------------------
1 | replaced
--------------------------------------------------------------------------------
/test/it-respects-props/App.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | Hello, {name}
6 |
--------------------------------------------------------------------------------
/test/it-respects-props/expected.html:
--------------------------------------------------------------------------------
1 | Hello, world
2 |
--------------------------------------------------------------------------------
/test/it-skips-emit/App.svelte:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/test/it-throws-error-for-non-cjs-format/App.svelte:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/test/it-throws-error-if-no-filename-passed/App.svelte:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/test/it-uses-filename-function/App.svelte:
--------------------------------------------------------------------------------
1 | Hello
2 |
--------------------------------------------------------------------------------
/test/tests.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import del from "del";
3 |
4 | import { bundleWithRollup, resolvePath, resolveExpectedFile } from "./helpers";
5 |
6 | import plugin from "../src";
7 |
8 | function readFile(fileName) {
9 | return fs.readFileSync(fileName, { encoding: "utf-8" });
10 | }
11 |
12 | function expectHtmlEqual(actual, expected) {
13 | expect(actual.trim()).toEqual(expected.trim());
14 | }
15 |
16 | describe("plugin tests", () => {
17 | it("should emit html file", async () => {
18 | const testName = "it-emits-html-file";
19 | const outputFileName = `dist/${testName}.html`;
20 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
21 | const ssrOutputFile = resolvePath(testName, outputFileName);
22 |
23 | const pluginOptions = {
24 | fileName: ssrOutputFile,
25 | };
26 |
27 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
28 |
29 | expect(fs.existsSync(ssrBundleFile)).toBeTruthy();
30 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
31 |
32 | const expected = readFile(resolveExpectedFile(testName));
33 | const actual = readFile(ssrOutputFile);
34 |
35 | expectHtmlEqual(actual, expected);
36 |
37 | // cleanup
38 | await del(resolvePath(testName, "dist"));
39 | });
40 |
41 | it("should respect props", async () => {
42 | const testName = "it-respects-props";
43 | const outputFileName = `dist/${testName}.html`;
44 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
45 | const ssrOutputFile = resolvePath(testName, outputFileName);
46 |
47 | const pluginOptions = {
48 | fileName: ssrOutputFile,
49 | props: {
50 | name: "world",
51 | },
52 | };
53 |
54 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
55 |
56 | expect(fs.existsSync(ssrBundleFile)).toBeTruthy();
57 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
58 |
59 | const expected = readFile(resolveExpectedFile(testName));
60 | const actual = readFile(ssrOutputFile);
61 |
62 | expectHtmlEqual(actual, expected);
63 |
64 | // cleanup
65 | await del(resolvePath(testName, "dist"));
66 | });
67 |
68 | it("should skip emit bundle if skipEmit=true", async () => {
69 | const testName = "it-skips-emit";
70 | const outputFileName = `dist/${testName}.html`;
71 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
72 | const ssrOutputFile = resolvePath(testName, outputFileName);
73 |
74 | const pluginOptions = {
75 | fileName: ssrOutputFile,
76 | skipEmit: true,
77 | };
78 |
79 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
80 |
81 | expect(fs.existsSync(ssrBundleFile)).toBeFalsy();
82 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
83 |
84 | // cleanup
85 | await del(resolvePath(testName, "dist"));
86 | });
87 |
88 | it("should use filename function option", async () => {
89 | const testName = "it-uses-filename-function";
90 | const outputFileName = `dist/test.html`;
91 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
92 | const ssrOutputFile = resolvePath(testName, outputFileName);
93 |
94 | const pluginOptions = {
95 | fileName: function(file) {
96 | return ssrOutputFile;
97 | },
98 | };
99 |
100 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
101 |
102 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
103 |
104 | // cleanup
105 | await del(resolvePath(testName, "dist"));
106 | });
107 |
108 | it("should place styles before markup by default", async () => {
109 | const testName = "it-puts-styles-before-html-by-default";
110 | const outputFileName = `dist/test.html`;
111 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
112 | const ssrOutputFile = resolvePath(testName, outputFileName);
113 |
114 | const pluginOptions = {
115 | fileName: ssrOutputFile,
116 | };
117 |
118 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
119 |
120 | expect(fs.existsSync(ssrBundleFile)).toBeTruthy();
121 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
122 |
123 | const expected = readFile(resolveExpectedFile(testName));
124 | const actual = readFile(ssrOutputFile);
125 |
126 | expectHtmlEqual(actual, expected);
127 |
128 | // cleanup
129 | await del(resolvePath(testName, "dist"));
130 | });
131 |
132 | it("should use configureExport option", async () => {
133 | const testName = "it-respects-configure-export-option";
134 | const outputFileName = `dist/test.html`;
135 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
136 | const ssrOutputFile = resolvePath(testName, outputFileName);
137 |
138 | const pluginOptions = {
139 | fileName: ssrOutputFile,
140 | configureExport: function(html, css) {
141 | return `${html}`;
142 | },
143 | };
144 |
145 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
146 |
147 | expect(fs.existsSync(ssrBundleFile)).toBeTruthy();
148 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
149 |
150 | const expected = readFile(resolveExpectedFile(testName));
151 | const actual = readFile(ssrOutputFile);
152 |
153 | expectHtmlEqual(actual, expected);
154 |
155 | // cleanup
156 | await del(resolvePath(testName, "dist"));
157 | });
158 |
159 | it("should use preprocessHtml/preprocessCss options", async () => {
160 | const testName = "it-respects-preprocess-html-css-options";
161 | const outputFileName = `dist/test.html`;
162 | const ssrBundleFile = resolvePath(testName, "dist/ssr.js");
163 | const ssrOutputFile = resolvePath(testName, outputFileName);
164 |
165 | const pluginOptions = {
166 | fileName: ssrOutputFile,
167 | preprocessHtml: function(html) {
168 | return "replaced
";
169 | },
170 | preprocessCss: function(css) {
171 | return "body { opacity: 0; }";
172 | },
173 | };
174 |
175 | await bundleWithRollup({ plugin, pluginOptions, testName, output: ssrBundleFile });
176 |
177 | expect(fs.existsSync(ssrBundleFile)).toBeTruthy();
178 | expect(fs.existsSync(ssrOutputFile)).toBeTruthy();
179 |
180 | const expected = readFile(resolveExpectedFile(testName));
181 | const actual = readFile(ssrOutputFile);
182 |
183 | expectHtmlEqual(actual, expected);
184 |
185 | // cleanup
186 | await del(resolvePath(testName, "dist"));
187 | });
188 |
189 | it("should throw error for non-cjs format", async () => {
190 | const pluginOptions = {
191 | fileName: "doesnt-matter.html",
192 | skipEmit: true,
193 | };
194 |
195 | await expect(
196 | bundleWithRollup({
197 | format: "esm",
198 | plugin,
199 | pluginOptions,
200 | testName: "it-throws-error-for-non-cjs-format",
201 | output: "doesnt-matter.html",
202 | }),
203 | ).rejects.toThrow(new Error("rollup-plugin-svelte-ssr can only be used with 'cjs'-format"));
204 | });
205 |
206 | it("should throw error if no filename option has been passed", async () => {
207 | const pluginOptions = {
208 | skipEmit: true,
209 | };
210 |
211 | let error;
212 | try {
213 | await bundleWithRollup({
214 | plugin,
215 | pluginOptions,
216 | testName: "it-throws-error-if-no-filename-passed",
217 | output: "doesnt-matter.html",
218 | });
219 | } catch (e) {
220 | error = e;
221 | }
222 | expect(error).toEqual(new Error("options.fileName should be string or function"));
223 | });
224 | });
225 |
--------------------------------------------------------------------------------