start generating client-side bundle. WIP

This commit is contained in:
Rich Harris
2017-12-11 13:08:25 -05:00
parent ad8a410ba4
commit a22e28a11f
9 changed files with 4990 additions and 39 deletions

View File

@@ -27,8 +27,6 @@ A Sapper app is just an Express app (conventionally, `server.js`) that uses the
const app = require('express')();
const sapper = require('sapper');
const app = express();
app.use(sapper());
const { PORT = 3000 } = process.env;
@@ -74,7 +72,7 @@ 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) {
export async function get(req) {
return await getPostFromDatabase(req.params.id);
}
```

View File

@@ -1,14 +1,20 @@
require('svelte/ssr/register');
const esm = require('@std/esm');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const tmp = require('tmp');
const create_matchers = require('./utils/create_matchers.js');
const create_app = require('./utils/create_app.js');
const create_webpack_compiler = require('./utils/create_webpack_compiler.js');
require = esm(module, {
const esmRequire = esm(module, {
esm: 'all'
});
module.exports = function connect(opts = {}) {
const dir = tmp.dirSync({ unsafeCleanup: true });
module.exports = function connect(opts) {
const routes = path.resolve('routes');
const out = path.resolve('.sapper');
@@ -18,19 +24,40 @@ module.exports = function connect(opts = {}) {
let server_routes = glob.sync('**/*.+(js|mjs)', { cwd: routes });
let server_route_matchers = create_matchers(server_routes);
// create_app(routes, dir.name, page_matchers, opts.dev);
create_app(routes, out, page_matchers, opts.dev);
const webpack_compiler = create_webpack_compiler(
path.join(out, 'main.js'),
path.resolve('.sapper/webpack'),
opts.dev
);
return async function(req, res, next) {
const url = req.url.replace(/\?.+/, '');
if (url.startsWith('/webpack/')) {
fs.createReadStream(path.resolve('.sapper' + url)).pipe(res);
return;
}
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
}));
const app = await webpack_compiler.app;
const page = opts.template({
app,
html: Component.render({
params,
query: req.query
})
});
res.end(page);
return;
}
}
@@ -39,7 +66,7 @@ module.exports = function connect(opts = {}) {
const matcher = server_route_matchers[i];
if (matcher.test(url)) {
req.params = matcher.exec(url);
const route = require(`${routes}/${matcher.file}`);
const route = esmRequire(`${routes}/${matcher.file}`);
const handler = route[req.method.toLowerCase()];
if (handler) {

2738
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,14 @@
"test": "test"
},
"dependencies": {
"@std/esm": "^0.18.0"
"@std/esm": "^0.18.0",
"extract-text-webpack-plugin": "^3.0.2",
"tmp": "0.0.33",
"webpack": "^3.10.0"
},
"devDependencies": {
"mocha": "^4.0.1",
"svelte": "^1.47.0"
"svelte": "^1.47.1"
},
"scripts": {
"test": "mocha --opts mocha.opts"

38
templates/main.js Normal file
View File

@@ -0,0 +1,38 @@
window.addEventListener('click', event => {
let a = event.target;
while (a && a.nodeName !== 'A') a = a.parentNode;
if (!a) return;
if (navigate(new URL(a.href))) event.preventDefault();
});
const target = document.querySelector('main');
let component;
function navigate(url) {
if (url.origin !== window.location.origin) return;
let match;
let params = {};
const query = {};
function render(mod) {
if (component) {
component.destroy();
} else {
target.innerHTML = '';
}
component = new mod.default({
target,
data: { query, params },
hydrate: !!component
});
}
// ROUTES
return true;
}
navigate(window.location);

27
utils/create_app.js Normal file
View File

@@ -0,0 +1,27 @@
const fs = require('fs');
const path = require('path');
const tmp = require('tmp');
const template = fs.readFileSync(path.resolve(__dirname, '../templates/main.js'), 'utf-8');
module.exports = function create_app(routes, dest, matchers, dev) {
// TODO in dev mode, watch files
const code = matchers
.map(matcher => {
const condition = matcher.dynamic.length === 0 ?
`url.pathname === '/${matcher.parts.join('/')}'` :
`${matcher.pattern}.test(url.pathname)`;
return `
if (${condition}) {
import('../routes/${matcher.file}').then(render);
}
`.replace(/^\t{3}/gm, '').trim();
})
.join(' else ');
const main = template.replace('// ROUTES', code);
fs.writeFileSync(path.join(dest, 'main.js'), main);
};

View File

@@ -30,6 +30,7 @@ module.exports = function create_matchers(files) {
return {
file,
pattern,
test,
exec,
parts,
@@ -41,12 +42,5 @@ module.exports = function create_matchers(files) {
(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
}
});
}

View File

@@ -0,0 +1,81 @@
const fs = require('fs');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
// TODO make the webpack config, err, configurable
module.exports = function create_webpack_compiler(main, dest, dev) {
const compiler = {};
const _ = webpack({
entry: {
main
},
resolve: {
extensions: ['.js', '.html']
},
output: {
path: dest,
filename: '[name].[hash].js',
chunkFilename: '[name].[id].js',
publicPath: '/webpack/'
},
module: {
rules: [
{
test: /\.html$/,
exclude: /node_modules/,
use: {
loader: 'svelte-loader',
options: {
emitCss: true,
cascade: false,
store: true
}
}
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{ loader: 'css-loader', options: { sourceMap: dev } }]
})
}
]
},
plugins: [
new ExtractTextPlugin('main.css'),
!dev && new webpack.optimize.ModuleConcatenationPlugin(),
!dev && new UglifyJSPlugin()
].filter(Boolean),
devtool: dev ? 'inline-source-map' : false
});
if (false) { // TODO watch in dev
// TODO how can we invalidate compiler.app when watcher restarts?
compiler.app = new Promise((fulfil, reject) => {
_.watch({}, (err, stats) => {
if (err || stats.hasErrors()) {
// TODO handle errors
}
const filename = stats.toJson().assetsByChunkName.main;
fulfil(`/webpack/${filename}`);
});
});
} else {
compiler.app = new Promise((fulfil, reject) => {
_.run((err, stats) => {
if (err || stats.hasErrors()) {
// TODO handle errors
}
const filename = stats.toJson().assetsByChunkName.main;
fulfil(`/webpack/${filename}`);
});
});
}
return compiler;
};

2085
yarn.lock Normal file

File diff suppressed because it is too large Load Diff