├── .DS_Store
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── global.css
├── logo.svg
└── robots.txt
├── routes
├── .DS_Store
├── about
│ └── index.svelte
├── blog
│ ├── [slug].svelte
│ └── index.svelte
└── index.svelte
├── sapper
├── README.md
└── runtime.js
├── shared
└── components
│ └── Nav.svelte
├── snowpack
├── client.config.json
└── server.config.json
└── tasks
├── SnowpackLoader.js
└── dev.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rich-Harris/snowpack-svelte-ssr/26aae8a3efa3950f9bb38892eb768efe5026edda/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build
3 | /build
4 | /web_modules
5 | /node_modules
6 | /scratch
7 | /sapper/routes.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Fred K. Schott
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated
2 |
3 | This project has been entirely replaced by Svelte Kit:
4 | * https://github.com/sveltejs/kit
5 | * https://www.npmjs.com/package/@sveltejs/kit
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "node tasks/dev",
4 | "build": "snowpack build --config snowpack/server.config.json && snowpack build --config snowpack/client.config.json",
5 | "test": "jest"
6 | },
7 | "dependencies": {
8 | "svelte": "^3.24.0"
9 | },
10 | "devDependencies": {
11 | "@snowpack/app-scripts-svelte": "^1.8.4",
12 | "@testing-library/jest-dom": "^5.5.0",
13 | "@testing-library/svelte": "^3.0.0",
14 | "chokidar": "^3.4.2",
15 | "http-proxy": "^1.18.1",
16 | "httpie": "^1.1.2",
17 | "jest": "^26.2.2",
18 | "magic-string": "^0.25.7",
19 | "meriyah": "^2.1.1",
20 | "snowpack": "^2.11.0",
21 | "svelte-check": "^1.0.0",
22 | "tiny-glob": "^0.2.6"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rich-Harris/snowpack-svelte-ssr/26aae8a3efa3950f9bb38892eb768efe5026edda/public/favicon.ico
--------------------------------------------------------------------------------
/public/global.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif
3 | }
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/routes/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Rich-Harris/snowpack-svelte-ssr/26aae8a3efa3950f9bb38892eb768efe5026edda/routes/.DS_Store
--------------------------------------------------------------------------------
/routes/about/index.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
about
8 | this is the about page
--------------------------------------------------------------------------------
/routes/blog/[slug].svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 | {slug}
10 | if this were a more fleshed-out example, this component would export a `preload` method that turned `slug` into some data
--------------------------------------------------------------------------------
/routes/blog/index.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | blog
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/routes/index.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | Home
8 |
9 | This is an example of using Snowpack for SSR. See this gist for more information
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sapper/README.md:
--------------------------------------------------------------------------------
1 | In non-demo circumstances, the `runtime.js` file would live inside `node_modules`. Not sure where `routes.js` would go (not in `node_modules`, as its contents are determined by the contents of the `routes` directory.)
--------------------------------------------------------------------------------
/sapper/runtime.js:
--------------------------------------------------------------------------------
1 | import routes from '/_sapper/routes';
2 |
3 | let component;
4 |
5 | const select_route = pathname => routes.find(r => r.pattern.test(pathname));
6 |
7 | async function load_page(route) {
8 | const match = route.pattern.exec(location.pathname);
9 | const page = await route.load();
10 |
11 | const props = {};
12 | route.params.forEach((name, i) => {
13 | props[name] = match[i + 1];
14 | });
15 |
16 | return { page, props };
17 | }
18 |
19 | async function start() {
20 | const route = select_route(window.location.pathname);
21 |
22 | if (route) {
23 | const { page, props } = await load_page(route);
24 |
25 | // TODO handle preload
26 |
27 | component = new page.default({
28 | target: document.body,
29 | hydrate: true,
30 | props
31 | });
32 | }
33 |
34 | // TODO make this less extremely crude
35 | document.addEventListener('click', async e => {
36 | let node = e.target;
37 |
38 | while (node) {
39 | if (node.tagName === 'A') {
40 | const route = select_route(new URL(node.href).pathname);
41 |
42 | if (route) {
43 | e.preventDefault();
44 |
45 | history.pushState({}, null, node.href);
46 |
47 | const { page, props } = await load_page(route);
48 |
49 | component.$destroy();
50 | component = new page.default({
51 | target: document.body,
52 | props
53 | });
54 | }
55 |
56 | return;
57 | }
58 |
59 | node = node.parentNode;
60 | }
61 | });
62 |
63 | window.addEventListener('popstate', async () => {
64 | const route = select_route(window.location.pathname);
65 |
66 | if (route) {
67 | const { page, props } = await load_page(route);
68 |
69 | component.$destroy();
70 | component = new page.default({
71 | target: document.body,
72 | props
73 | });
74 | }
75 | });
76 | }
77 |
78 | start();
79 |
80 | if (import.meta.hot) {
81 | import.meta.hot.accept();
82 | import.meta.hot.dispose(() => {
83 | component.$destroy();
84 | });
85 | }
--------------------------------------------------------------------------------
/shared/components/Nav.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
10 |
11 |
--------------------------------------------------------------------------------
/snowpack/client.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "install": [
3 | "svelte"
4 | ],
5 | "plugins": [
6 | ["../snowpack/plugins/plugin-svelte"]
7 | ],
8 | "devOptions": {
9 | "port": 3002,
10 | "out": "build/client",
11 | "open": "none"
12 | },
13 | "mount": {
14 | "routes": "/_routes/",
15 | "sapper": "/_sapper/",
16 | "shared": "/shared/",
17 | "public": "/"
18 | },
19 | "alias": {
20 | "@shared": "./shared"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/snowpack/server.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "install": [
3 | "svelte"
4 | ],
5 | "plugins": [
6 | ["@snowpack/plugin-svelte", {
7 | "generate": "ssr"
8 | }]
9 | ],
10 | "devOptions": {
11 | "port": 3001,
12 | "out": "build/server",
13 | "open": "none",
14 | "hmr": false,
15 | "secure": false
16 | },
17 | "mount": {
18 | "routes": "/_routes",
19 | "shared": "/shared"
20 | },
21 | "alias": {
22 | "@shared": "./shared"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tasks/SnowpackLoader.js:
--------------------------------------------------------------------------------
1 | const meriyah = require('meriyah');
2 | const MagicString = require('magic-string');
3 |
4 | // This class makes it possible to load modules from the 'server'
5 | // snowpack server, for the sake of SSR
6 | module.exports = class SnowpackLoader {
7 | constructor({loadByUrl}) {
8 | this.loadByUrl = loadByUrl;
9 | this.cache = new Map();
10 | }
11 |
12 | async load(url) {
13 | if (url.endsWith('.css.proxy.js')) {
14 | // bit of a hack, but we need to squelch these as they
15 | // assume we're in the DOM
16 | return null;
17 | }
18 |
19 | let data;
20 |
21 | try {
22 | ( data = await this.loadByUrl(url, {isSSR: true}));
23 | } catch (err) {
24 | console.error('>>> error fetching ', url);
25 | throw err;
26 | }
27 |
28 | const cached = this.cache.get(url);
29 | const hash = get_hash(data);
30 |
31 | if (cached && cached.hash === hash) {
32 | return cached.exports;
33 | }
34 |
35 | const code = new MagicString(data);
36 | let ast;
37 |
38 | try {
39 | ast = meriyah.parseModule(data, {
40 | ranges: true
41 | });
42 | } catch (err) {
43 | console.error('>>> error parsing ', url);
44 | console.log(data);
45 | throw err;
46 | }
47 |
48 | const imports = [];
49 |
50 | const export_from_identifiers = new Map();
51 | let uid = 1;
52 |
53 | ast.body.forEach(node => {
54 | if (node.type === 'ImportDeclaration') {
55 | imports.push(node);
56 | code.remove(node.start, node.end);
57 | }
58 |
59 | if (node.type === 'ExportAllDeclaration') {
60 | if (!export_from_identifiers.has(node.source)) {
61 | export_from_identifiers.set(node.source, `__import${uid++}`);
62 | }
63 |
64 | code.overwrite(node.start, node.end, `Object.assign(exports, ${export_from_identifiers.get(node.source)})`)
65 | imports.push(node);
66 | }
67 |
68 | if (node.type === 'ExportDefaultDeclaration') {
69 | code.overwrite(node.start, node.declaration.start, 'exports.default = ');
70 | }
71 |
72 | if (node.type === 'ExportNamedDeclaration') {
73 | if (node.source) {
74 | imports.push(node);
75 |
76 | if (!export_from_identifiers.has(node.source)) {
77 | export_from_identifiers.set(node.source, `__import${uid++}`);
78 | }
79 | }
80 |
81 | if (node.specifiers && node.specifiers.length > 0) {
82 | code.remove(node.start, node.specifiers[0].start);
83 |
84 | node.specifiers.forEach(specifier => {
85 | const lhs = `exports.${specifier.exported.name}`;
86 | const rhs = node.source
87 | ? `${export_from_identifiers.get(node.source)}.${specifier.local.name}`
88 | : specifier.local.name;
89 |
90 | code.overwrite(specifier.start, specifier.end, `${lhs} = ${rhs}`)
91 | });
92 |
93 | code.remove(node.specifiers[node.specifiers.length - 1].end, node.end);
94 | }
95 |
96 | else {
97 | throw new Error(`TODO ${url}`);
98 | }
99 | }
100 | });
101 |
102 | const deps = [];
103 | imports.forEach(node => {
104 | const resolved = new URL(node.source.value, `http://localhost${url}`);
105 | const promise = this.load(resolved.pathname);
106 |
107 | if (node.type === 'ExportAllDeclaration' || node.type === 'ExportNamedDeclaration') {
108 | // `export * from './other.js'` or `export { foo } from './other.js'`
109 | deps.push({
110 | name: export_from_identifiers.get(node.source),
111 | promise
112 | });
113 | }
114 |
115 | else if (node.specifiers.length === 0) {
116 | // bare import
117 | deps.push({
118 | name: null,
119 | promise
120 | });
121 | }
122 |
123 | else if (node.specifiers[0].type === 'ImportNamespaceSpecifier') {
124 | deps.push({
125 | name: node.specifiers[0].local.name,
126 | promise
127 | });
128 | }
129 |
130 | else {
131 | deps.push(...node.specifiers.map(specifier => ({
132 | name: specifier.local.name,
133 | promise: promise.then(exports => exports[specifier.imported ? specifier.imported.name : 'default'])
134 | })));
135 | }
136 | });
137 |
138 | deps.sort((a, b) => !!a.name !== !!b.name ? a.name ? -1 : 1 : 0);
139 |
140 | code.append(`\n//# sourceURL=${url}`);
141 |
142 | const fn = new Function('exports', ...deps.map(d => d.name).filter(Boolean), code.toString());
143 | const values = await Promise.all(deps.map(d => d.promise));
144 |
145 | const exports = {};
146 | fn(exports, ...values);
147 |
148 | this.cache.set(url, { hash, exports });
149 |
150 | // {
151 | // // for debugging
152 | // const { pathname } = new URL(url);
153 | // const file = `.tmp${pathname}`;
154 | // const dir = path.dirname(file);
155 | // try {
156 | // fs.mkdirSync(dir, { recursive: true });
157 | // } catch {}
158 |
159 | // fs.writeFileSync(file, code.toString());
160 | // }
161 |
162 | return exports;
163 | }
164 | }
165 |
166 | function get_hash(str) {
167 | let hash = 5381;
168 | let i = str.length;
169 |
170 | while(i) hash = (hash * 33) ^ str.charCodeAt(--i);
171 | return hash >>> 0;
172 | }
--------------------------------------------------------------------------------
/tasks/dev.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const fs = require('fs');
3 | const http_proxy = require('http-proxy');
4 | const chokidar = require('chokidar');
5 | const glob = require('tiny-glob/sync');
6 | const SnowpackLoader = require('./SnowpackLoader');
7 |
8 | // run the Snowpack dev server
9 | const snowpack = require('../../snowpack/snowpack/pkg');
10 | const pkgManifest = require('../package.json');
11 | const config_file_client = 'snowpack/client.config.json';
12 | const config = snowpack.unstable__loadAndValidateConfig({config: config_file_client}, pkgManifest);
13 |
14 | // SNOWPACK LOGGING, USEFUL FOR DEBUGGING SNOWPACK ISSUES
15 | // childProcess.stdout.setEncoding('utf8');
16 | // childProcess.stdout.on('data', function(data) {
17 | // console.log('stdout: ' + data);
18 | // });
19 | // childProcess.stderr.setEncoding('utf8');
20 | // childProcess.stderr.on('data', function(data) {
21 | // console.log('stderr: ' + data);
22 | // });
23 |
24 | // proxy requests for assets (i.e. not page requests, which are SSR'd)
25 | // to the 'client' snowpack server
26 | const proxy = http_proxy.createProxyServer();
27 |
28 | // create and update a route manifest. this is a super basic version
29 | // of what Sapper does
30 | const watcher = chokidar.watch('routes', { ignoreInitial: true });
31 |
32 | let manifest;
33 |
34 | const update_manifest = () => {
35 | const files = glob('**/*.svelte', { cwd: 'routes' });
36 | manifest = files
37 | .filter(file => file.split('/').every(part => !part.startsWith('_')))
38 | .map(file => {
39 | const segments = file.split('/');
40 | const last = segments.pop();
41 |
42 | if (last.startsWith('index.')) {
43 | segments[segments.length - 1] += last.slice(5, -7);
44 | } else {
45 | segments.push(last.slice(0, -7));
46 | }
47 |
48 | const params = [];
49 | const pattern_string = segments.join('/').replace(/\[([^\]]+)\]/g, (m, name) => {
50 | params.push(name);
51 | return '([^/]+)';
52 | });
53 |
54 | return {
55 | file,
56 | pattern: new RegExp(`^/${pattern_string}${segments.length ? '/?' : ''}$`), // TODO query string
57 | params
58 | };
59 | });
60 |
61 | const client_routes = manifest.map(r => {
62 | const load = `() => import('/_routes/${r.file.replace('.svelte', '.js')}')`; // TODO why do we need to replace the extension?
63 | return `{ pattern: ${r.pattern}, params: ${JSON.stringify(r.params)}, load: ${load} }`;
64 | });
65 |
66 | fs.writeFileSync('sapper/routes.js', `export default [\n\t${client_routes.join(',\n\t')}\n];`);
67 | };
68 |
69 | watcher.on('add', update_manifest);
70 | watcher.on('unlink', update_manifest);
71 | update_manifest();
72 |
73 | // this is our version of a shell index.html file
74 | const template = ({ html, head, css }) => `
75 |
76 |
77 |
78 |
79 |
80 | ${head}
81 |
82 |
83 |
84 |
85 | ${html}
86 |
87 |
88 |
89 | `;
90 |
91 | (async () => {
92 | const {requestHandler: snowpackMiddleware, loadByUrl} = await snowpack.unstable__startServer({
93 | cwd: process.cwd(),
94 | config,
95 | lockfile: null,
96 | pkgManifest,
97 | });
98 |
99 | // create a loader that will request files from the 'server' snowpack
100 | // server, transform them, and evaluate them
101 | const loader = new SnowpackLoader({loadByUrl});
102 |
103 | http.createServer(async (req, res) => {
104 | const route = manifest.find(r => r.pattern.test(req.url));
105 |
106 | // if this is an SSR request (URL matches one of the routes),
107 | // load the module in question and render the page...
108 | if (route && req.headers.upgrade !== 'websocket') {
109 | const mod = await loader.load(`/_routes/${route.file.replace('.svelte', '.js')}`);
110 |
111 | const match = route.pattern.exec(req.url);
112 | const props = {};
113 | route.params.forEach((name, i) => {
114 | props[name] = match[i + 1];
115 | });
116 | const rendered = template(mod.default.render(props));
117 | res.setHeader('Content-Type', 'text/html');
118 | res.end(rendered);
119 |
120 | return;
121 | }
122 |
123 | // ...otherwise defer to Snowpack middleware
124 | return snowpackMiddleware(req, res);
125 | }).listen(3000);
126 | })();
--------------------------------------------------------------------------------