mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 19:45:26 +00:00
initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
90
README.md
Normal file
90
README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# sapper
|
||||
|
||||
Combat-ready apps, engineered by Svelte.
|
||||
|
||||
## This is not a thing yet
|
||||
|
||||
If you visit this README in a few weeks, hopefully it will have blossomed into the app development framework we deserve. Right now, it's just a set of ideas.
|
||||
|
||||
---
|
||||
|
||||
[Next.js](https://github.com/zeit/next.js/) introduced a beautiful idea — that you should be able to build your app as universal React components in a special `pages` directory, and the framework should take care of routing and rendering on both client and server. What if we did the same thing for Svelte?
|
||||
|
||||
High-level goals:
|
||||
|
||||
* Extreme ease of development
|
||||
* Code-splitting and HMR out of the box (probably via webpack)
|
||||
* Best-in-class performance
|
||||
* As little magic as possible. Anyone should be able to understand how everything fits together, and e.g. make changes to the webpack config
|
||||
|
||||
|
||||
## Design
|
||||
|
||||
A Sapper app is just an Express app (conventionally, `server.js`) that uses the `sapper` middleware:
|
||||
|
||||
```js
|
||||
const app = require('express')();
|
||||
const sapper = require('sapper');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(sapper());
|
||||
|
||||
const { PORT = 3000 } = process.env;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`listening on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
The middleware serves pages that match files in the `routes` directory, and assets generated by webpack. In development mode, the middleware once activated watches `routes` to keep the app up-to-date.
|
||||
|
||||
|
||||
## Routing
|
||||
|
||||
Like Next, routes are defined by the project directory structure, but with some crucial differences:
|
||||
|
||||
* Files with an `.html` extension are treated as Svelte components. The `routes/about.html` (or `routes/about/index.html`) would create the `/about` route.
|
||||
* Files with a `.js` or `.mjs` extension are more generic route handlers. These files should export functions corresponding to the HTTP methods they support (example below).
|
||||
* Instead of route masking, we embed parameters in the filename. For example `post/%id%.html` maps to `/post/:id`, and the component will be rendered with the appropriate parameter.
|
||||
* Nested routes (read [this article](https://joshduff.com/2015-06-why-you-need-a-state-router.md)) can be handled by creating a file that matches the subroute — for example, `routes/app/settings/%submenu%.html` would match `/app/settings/profile` *and* `app/settings`, but in the latter case the `submenu` parameter would be `null`.
|
||||
|
||||
An example of a generic route:
|
||||
|
||||
```js
|
||||
// routes/api/post/%id%.js
|
||||
export async function get(req, res) {
|
||||
try {
|
||||
const data = await getPostFromDatabase(req.params.id);
|
||||
const json = JSON.stringify(data);
|
||||
|
||||
res.set({
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': json.length
|
||||
});
|
||||
|
||||
res.send(json);
|
||||
} catch (err) {
|
||||
res.status(500).send(err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or, if you omit the `res` argument, it can use the return value:
|
||||
|
||||
```js
|
||||
// routes/api/post/%id%.js
|
||||
export async function get(req, res) {
|
||||
return await getPostFromDatabase(req.params.id);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Things to figure out
|
||||
|
||||
* How to customise the overall page template
|
||||
* An equivalent of `getInitialProps`
|
||||
* Critical CSS
|
||||
* `store` integration
|
||||
* Route transitions
|
||||
* Equivalent of `next export`
|
||||
* ...and lots of other things that haven't occurred to me yet.
|
||||
67
connect.js
Normal file
67
connect.js
Normal file
@@ -0,0 +1,67 @@
|
||||
require('svelte/ssr/register');
|
||||
const esm = require('@std/esm');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const create_matchers = require('./utils/create_matchers.js');
|
||||
|
||||
require = esm(module, {
|
||||
esm: 'all'
|
||||
});
|
||||
|
||||
module.exports = function connect(opts = {}) {
|
||||
const routes = path.resolve('routes');
|
||||
const out = path.resolve('.sapper');
|
||||
|
||||
let pages = glob.sync('**/*.html', { cwd: routes });
|
||||
let page_matchers = create_matchers(pages);
|
||||
|
||||
let server_routes = glob.sync('**/*.+(js|mjs)', { cwd: routes });
|
||||
let server_route_matchers = create_matchers(server_routes);
|
||||
|
||||
return async function(req, res, next) {
|
||||
const url = req.url.replace(/\?.+/, '');
|
||||
|
||||
for (let i = 0; i < page_matchers.length; i += 1) {
|
||||
const matcher = page_matchers[i];
|
||||
if (matcher.test(url)) {
|
||||
const params = matcher.exec(url);
|
||||
const Component = require(`${routes}/${matcher.file}`);
|
||||
|
||||
res.end(Component.render({
|
||||
params,
|
||||
query: req.query
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < server_route_matchers.length; i += 1) {
|
||||
const matcher = server_route_matchers[i];
|
||||
if (matcher.test(url)) {
|
||||
req.params = matcher.exec(url);
|
||||
const route = require(`${routes}/${matcher.file}`);
|
||||
|
||||
const handler = route[req.method.toLowerCase()];
|
||||
if (handler) {
|
||||
if (handler.length === 2) {
|
||||
handler(req, res);
|
||||
} else {
|
||||
const data = await handler(req);
|
||||
|
||||
// TODO headers, error handling
|
||||
if (typeof data === 'string') {
|
||||
res.end(data);
|
||||
} else {
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
2
mocha.opts
Normal file
2
mocha.opts
Normal file
@@ -0,0 +1,2 @@
|
||||
--recursive
|
||||
utils/**/*.test.js
|
||||
205
package-lock.json
generated
Normal file
205
package-lock.json
generated
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@std/esm": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@std/esm/-/esm-0.18.0.tgz",
|
||||
"integrity": "sha512-oeHSSVp/WxC08ngpKgyYR4LcI0+EBwZiJcB58jvIqyJnOGxudSkxTgAQKsVfpNsMXfOoILgu9PWhuzIZ8GQEjw=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
||||
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"browser-stdout": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
|
||||
"integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
|
||||
"dev": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"diff": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
|
||||
"integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"dev": true
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "1.0.0",
|
||||
"inflight": "1.0.6",
|
||||
"inherits": "2.0.3",
|
||||
"minimatch": "3.0.4",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "1.0.1"
|
||||
}
|
||||
},
|
||||
"growl": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
|
||||
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
|
||||
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
|
||||
"dev": true
|
||||
},
|
||||
"he": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
|
||||
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
|
||||
"dev": true
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "1.4.0",
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.8"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
},
|
||||
"mocha": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz",
|
||||
"integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"browser-stdout": "1.3.0",
|
||||
"commander": "2.11.0",
|
||||
"debug": "3.1.0",
|
||||
"diff": "3.3.1",
|
||||
"escape-string-regexp": "1.0.5",
|
||||
"glob": "7.1.2",
|
||||
"growl": "1.10.3",
|
||||
"he": "1.1.1",
|
||||
"mkdirp": "0.5.1",
|
||||
"supports-color": "4.4.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
|
||||
"dev": true
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1.0.2"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
|
||||
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "2.0.0"
|
||||
}
|
||||
},
|
||||
"svelte": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-1.47.0.tgz",
|
||||
"integrity": "sha512-+RrPJXW3BDbbfSWFd2G2kAtUJs3cre0b4bWWe4mIkqprzL0FqVNBgWoSQNKmDngbCDbeXXvYC6ldkPp8bNXlRA=="
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "sapper",
|
||||
"version": "0.0.1",
|
||||
"description": "Combat-ready apps, engineered by Svelte",
|
||||
"main": "connect.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@std/esm": "^0.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^4.0.1",
|
||||
"svelte": "^1.47.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "mocha --opts mocha.opts"
|
||||
},
|
||||
"repository": "https://github.com/sveltejs/sapper",
|
||||
"keywords": [
|
||||
"svelte",
|
||||
"isomorphic",
|
||||
"universal",
|
||||
"template",
|
||||
"express"
|
||||
],
|
||||
"author": "Rich Harris",
|
||||
"license": "LIL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/sapper/issues"
|
||||
},
|
||||
"homepage": "https://github.com/sveltejs/sapper#readme",
|
||||
"@std/esm": {
|
||||
"esm": "js"
|
||||
}
|
||||
}
|
||||
52
utils/create_matchers.js
Normal file
52
utils/create_matchers.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function create_matchers(files) {
|
||||
return files
|
||||
.map(file => {
|
||||
const parts = file.replace(/\.(html|js|mjs)$/, '').split(path.sep);
|
||||
if (parts[parts.length - 1] === 'index') parts.pop();
|
||||
|
||||
const dynamic = parts
|
||||
.filter(part => part[0] === '%')
|
||||
.map(part => part.slice(1, -1));
|
||||
|
||||
const pattern = new RegExp(
|
||||
`^\\/${parts.map(p => p[0] === '%' ? '([^/]+)' : p).join('\\/')}$`
|
||||
);
|
||||
|
||||
const test = url => pattern.test(url);
|
||||
|
||||
const exec = url => {
|
||||
const match = pattern.exec(url);
|
||||
if (!match) return;
|
||||
|
||||
const params = {};
|
||||
dynamic.forEach((param, i) => {
|
||||
params[param] = match[i + 1];
|
||||
});
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
return {
|
||||
file,
|
||||
test,
|
||||
exec,
|
||||
parts,
|
||||
dynamic
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
(a.dynamic.length - b.dynamic.length) || // match static paths first
|
||||
(b.parts.length - a.parts.length) // match longer paths first
|
||||
);
|
||||
})
|
||||
.map(matcher => {
|
||||
return {
|
||||
file: matcher.file,
|
||||
test: matcher.test,
|
||||
exec: matcher.exec
|
||||
}
|
||||
});
|
||||
}
|
||||
39
utils/create_matchers.test.js
Normal file
39
utils/create_matchers.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const create_matchers = require('./create_matchers.js');
|
||||
|
||||
describe('create_matchers', () => {
|
||||
it('sorts routes correctly', () => {
|
||||
const matchers = create_matchers(['index.html', 'about.html', '%wildcard%.html', 'post/%id%.html']);
|
||||
|
||||
assert.deepEqual(
|
||||
matchers.map(m => m.file),
|
||||
[
|
||||
'about.html',
|
||||
'index.html',
|
||||
'post/%id%.html',
|
||||
'%wildcard%.html'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('generates params', () => {
|
||||
const matchers = create_matchers(['index.html', 'about.html', '%wildcard%.html', 'post/%id%.html']);
|
||||
|
||||
let file;
|
||||
let params;
|
||||
for (let i = 0; i < matchers.length; i += 1) {
|
||||
const matcher = matchers[i];
|
||||
if (params = matcher.exec('/post/123')) {
|
||||
file = matcher.file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(file, 'post/%id%.html');
|
||||
assert.deepEqual(params, {
|
||||
id: '123'
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user