Compare commits

..

49 Commits

Author SHA1 Message Date
Rich Harris
1e623dde29 -> v0.6.1 2018-02-03 13:48:30 -05:00
Rich Harris
5104abf329 -> v0.6.0 2018-02-03 13:09:31 -05:00
Rich Harris
6554fc8616 Merge branch 'restructure' 2018-02-03 13:07:36 -05:00
Rich Harris
cd01b7e6db Merge branch 'master' of github.com:sveltejs/sapper 2018-02-03 12:24:59 -05:00
Rich Harris
bfa3da6d3d Merge branch 'issue_109-patch-1' of https://github.com/samhatchett/sapper into samhatchett-issue_109-patch-1 2018-02-03 12:07:53 -05:00
Rich Harris
6ee092f8d4 Merge pull request #107 from lukastaegert/fix-search-param-handling
Fix query param handling
2018-02-03 12:05:57 -05:00
Rich Harris
ac70004f77 remove runtime.js.map (again?) 2018-02-03 12:04:39 -05:00
Rich Harris
3449f1eb37 add test for #105 2018-02-03 12:04:14 -05:00
Rich Harris
16cb1fccc6 Merge pull request #105 from thgh/patch-2
Fix route not found when url is /?
2018-02-03 12:03:36 -05:00
Rich Harris
b20c1c029f Merge branch 'thgh-patch-1' 2018-02-03 11:50:37 -05:00
Rich Harris
7abfb1aab1 Merge branch 'restructure' 2018-02-03 11:50:12 -05:00
Rich Harris
205c2defe4 Merge branch 'patch-1' of https://github.com/thgh/sapper into thgh-patch-1 2018-02-03 11:45:54 -05:00
Rich Harris
09a6eec83e Merge pull request #101 from sveltejs/restructure
Restructure codebase
2018-02-03 11:44:57 -05:00
Rich Harris
2cabf61ea7 ignore runtime.js.map 2018-02-03 11:41:33 -05:00
Sam Hatchett
71cfdd2907 fixes api route default content-type
fixes issue where api routes were being defaulted to text/html. Page routes should be text/html, but api routes could be json, zip files, etc., and express does some type-guessing to assist in case the user code does not specify the content-type.
2018-01-31 11:08:25 -05:00
Lukas Taegert
297f4276de Fix query param handling by
* not using a for-of loop on an iterator that is transpiled wrongly
* not using URL.searchParams which is only supported by rather new
  browsers
2018-01-26 23:29:34 +01:00
Thomas Ghysels
422e31e183 Fix route not found when url is /? 2018-01-26 02:48:56 +01:00
Thomas Ghysels
b53ee061c0 Fix DOMException when location.hash is invalid selector 2018-01-26 02:24:49 +01:00
Rich Harris
8bad37205d convert to typescript 2018-01-21 16:28:02 -05:00
Rich Harris
fd0dd4fe58 and again... 2018-01-21 16:14:16 -05:00
Rich Harris
4940644ae3 more tidying up 2018-01-21 16:11:46 -05:00
Rich Harris
fb8d952eeb more tidying up 2018-01-21 16:02:42 -05:00
Rich Harris
fc631c4866 make route handling more explicit 2018-01-21 15:41:13 -05:00
Rich Harris
03ce2ea998 tidy up a bit 2018-01-21 15:04:22 -05:00
Rich Harris
dd8deb2d8a wip 3 2018-01-21 14:41:11 -05:00
Rich Harris
7d721abb2a wip 2 2018-01-21 11:15:32 -05:00
Rich Harris
39b1fa89ce wip 2018-01-21 11:03:23 -05:00
Rich Harris
7a3506420f Merge pull request #100 from sveltejs/gh-99
return a promise from init
2018-01-20 20:16:46 -05:00
Rich Harris
72ae4a1c64 ugh still need to wait for requests to complete 2018-01-20 19:55:21 -05:00
Rich Harris
a09c33d6a5 return a promise from init - fixes #99 2018-01-20 19:49:41 -05:00
Rich Harris
4590aa313c Merge pull request #96 from mrkishi/master
Identify js and html route clashes
2018-01-20 18:36:50 -05:00
Rich Harris
d11bd954e0 seems tests weren't actually running in appveyor? (#98)
well this was harder than it should have been
2018-01-20 18:34:30 -05:00
Rich Harris
c15959710b huh. not sure where this broke 2018-01-20 13:39:49 -05:00
Rich Harris
bb8ff74f68 remove noise from tests 2018-01-20 13:21:53 -05:00
Rich Harris
2cbbe91490 Merge branch 'master' of github.com:sveltejs/sapper 2018-01-20 12:53:16 -05:00
Rich Harris
faeddd8add work around Svelte hydration failures 2018-01-20 12:53:06 -05:00
Rich Harris
d77722c042 Merge branch 'chexxor-patch-1' 2018-01-20 12:40:35 -05:00
Rich Harris
61daba7a64 add hydration test, add window.init function to make it possible 2018-01-20 12:40:31 -05:00
Rich Harris
54ff8cc2e6 Merge branch 'patch-1' of https://github.com/chexxor/sapper into chexxor-patch-1 2018-01-20 12:37:48 -05:00
Rich Harris
e6fcafe09b Merge pull request #95 from lukeed/fix/express-reliance
Remove Express Helpers
2018-01-20 12:10:43 -05:00
Rich Harris
a305d3cea1 Merge pull request #97 from sveltejs/gh-90
return service worker from generate_asset_cache
2018-01-20 12:09:11 -05:00
Rich Harris
75e70207b8 return service worker from generate_asset_cache - fixes #90 2018-01-20 12:02:32 -05:00
mrkishi
8a8526d9ed Identify js and html route clashes 2018-01-20 01:42:13 -02:00
Luke Edwards
9a76229bb6 test: shim setHeader method 2018-01-19 13:56:01 -08:00
Luke Edwards
f4e46e6e6c replace Express shorthands w/ native counterparts 2018-01-19 13:54:55 -08:00
Alex Berg
90cd347112 Fix typo 2018-01-19 01:25:20 -06:00
Alex Berg
5adfdd6fe0 Hydrate on first load, not destroy 2018-01-19 01:22:08 -06:00
Rich Harris
a6dc61a182 -> v0.5.1 2018-01-16 08:47:50 -05:00
Rich Harris
96666d05ec only write to filesystem in dev mode 2018-01-16 08:47:44 -05:00
35 changed files with 996 additions and 736 deletions

11
.gitignore vendored
View File

@@ -1,6 +1,15 @@
.DS_Store
yarn.lock
node_modules
cypress/screenshots
test/app/.sapper
runtime.js
yarn.lock
runtime.js.map
cli.js
cli.js.map
middleware.js
middleware.js.map
core.js
core.js.map
webpack/config.js
webpack/config.js.map

View File

@@ -1,5 +1,24 @@
# sapper changelog
## 0.6.1
* Fix `pkg.files` and `pkg.bin`
## 0.6.0
* Hydrate on first load, and only on first load ([#93](https://github.com/sveltejs/sapper/pull/93))
* Identify clashes between page and server routes ([#96](https://github.com/sveltejs/sapper/pull/96))
* Remove Express-specific utilities, for compatbility with Polka et al ([#94](https://github.com/sveltejs/sapper/issues/94))
* Return a promise from `init` when first page has rendered ([#99](https://github.com/sveltejs/sapper/issues/99))
* Handle invalid hash links ([#104](https://github.com/sveltejs/sapper/pull/104))
* Avoid `URLSearchParams` ([#107](https://github.com/sveltejs/sapper/pull/107))
* Don't automatically set `Content-Type` for server routes ([#111](https://github.com/sveltejs/sapper/pull/111))
* Handle empty query string routes, e.g. `/?` ([#105](https://github.com/sveltejs/sapper/pull/105))
## 0.5.1
* Only write service-worker.js to filesystem in dev mode ([#90](https://github.com/sveltejs/sapper/issues/90))
## 0.5.0
* Experimental support for `sapper export` ([#9](https://github.com/sveltejs/sapper/issues/9))

View File

@@ -15,3 +15,7 @@ environment:
install:
- ps: Install-Product node $env:nodejs_version
- npm install
test_script:
- node --version && npm --version
- npm test

View File

@@ -1,47 +0,0 @@
process.env.NODE_ENV = 'production';
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const rimraf = require('rimraf');
const { client, server } = require('./utils/compilers.js');
const create_app = require('./utils/create_app.js');
const generate_asset_cache = require('./utils/generate_asset_cache.js');
const { dest } = require('./config.js');
module.exports = () => {
mkdirp.sync(dest);
rimraf.sync(path.join(dest, '**/*'));
// create main.js and server-routes.js
create_app();
return new Promise((fulfil, reject) => {
function handleErrors(err, stats) {
if (err) {
reject(err);
process.exit(1);
}
if (stats.hasErrors()) {
console.error(stats.toString({ colors: true }));
reject(new Error(`Encountered errors while building app`));
}
}
client.run((err, clientStats) => {
handleErrors(err, clientStats);
const clientInfo = clientStats.toJson();
fs.writeFileSync(path.join(dest, 'stats.client.json'), JSON.stringify(clientInfo, null, ' '));
server.run((err, serverStats) => {
handleErrors(err, serverStats);
const serverInfo = serverStats.toJson();
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(serverInfo, null, ' '));
generate_asset_cache(clientInfo, serverInfo);
fulfil();
});
});
});
};

View File

@@ -1,21 +0,0 @@
const path = require('path');
const mkdirp = require('mkdirp');
const rimraf = require('rimraf');
exports.dev = process.env.NODE_ENV !== 'production';
exports.templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
exports.src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
exports.dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
if (exports.dev) {
mkdirp.sync(exports.dest);
rimraf.sync(path.join(exports.dest, '**/*'));
}
exports.entry = {
client: path.resolve(exports.templates, '.main.rendered.js'),
server: path.resolve(exports.dest, 'server-entry.js')
};

View File

@@ -1,30 +0,0 @@
const glob = require('glob');
const create_routes = require('./utils/create_routes.js');
const { src, dev } = require('./config.js');
const callbacks = [];
exports.onchange = fn => {
callbacks.push(fn);
};
function update() {
exports.routes = create_routes(
glob.sync('**/*.+(html|js|mjs)', { cwd: src })
);
callbacks.forEach(fn => fn());
}
update();
if (dev) {
const watcher = require('chokidar').watch(`${src}/**/*.+(html|js|mjs)`, {
ignoreInitial: true,
persistent: false
});
watcher.on('add', update);
watcher.on('change', update);
watcher.on('unlink', update);
}

View File

@@ -1,11 +0,0 @@
const path = require('path');
const relative = require('require-relative');
const webpack = relative('webpack', process.cwd());
exports.client = webpack(
require(path.resolve('webpack.client.config.js'))
);
exports.server = webpack(
require(path.resolve('webpack.server.config.js'))
);

View File

@@ -1,82 +0,0 @@
const fs = require('fs');
const path = require('path');
const route_manager = require('../route_manager.js');
const { src, entry, dev } = require('../config.js');
function posixify(file) {
return file.replace(/[/\\]/g, '/');
}
function create_app() {
const { routes } = route_manager;
function create_client_main() {
const template = fs.readFileSync('templates/main.js', 'utf-8');
const code = `[${
routes
.filter(route => route.type === 'page')
.map(route => {
const params = route.dynamic.length === 0 ?
'{}' :
`{ ${route.dynamic.map((part, i) => `${part}: match[${i + 1}]`).join(', ') } }`;
const file = posixify(`${src}/${route.file}`);
return `{ pattern: ${route.pattern}, params: match => (${params}), load: () => import(/* webpackChunkName: "${route.id}" */ '${file}') }`
})
.join(', ')
}]`;
let main = template
.replace(/__app__/g, posixify(path.resolve(__dirname, '../../runtime/app.js')))
.replace(/__routes__/g, code)
.replace(/__dev__/g, String(dev));
if (dev) {
const hmr_client = posixify(require.resolve(`webpack-hot-middleware/client`));
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`
}
fs.writeFileSync(entry.client, main);
// need to fudge the mtime, because webpack is soft in the head
const { atime, mtime } = fs.statSync(entry.client);
fs.utimesSync(entry.client, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
}
function create_server_routes() {
const imports = routes
.map(route => {
const file = posixify(`${src}/${route.file}`);
return route.type === 'page' ?
`import ${route.id} from '${file}';` :
`import * as ${route.id} from '${file}';`;
})
.join('\n');
const exports = `export { ${routes.map(route => route.id)} };`;
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
const { atime, mtime } = fs.statSync(entry.server);
fs.utimesSync(entry.server, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999));
}
create_client_main();
create_server_routes();
}
if (dev) {
route_manager.onchange(create_app);
const watcher = require('chokidar').watch(`templates/main.js`, {
ignoreInitial: true,
persistent: false
});
watcher.on('add', create_app);
watcher.on('change', create_app);
watcher.on('unlink', create_app);
}
module.exports = create_app;

View File

@@ -1,77 +0,0 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const templates = require('../templates.js');
const route_manager = require('../route_manager.js');
const { dest } = require('../config.js');
function ensure_array(thing) {
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
}
module.exports = function generate_asset_cache(clientInfo, serverInfo) {
const main_file = `/client/${ensure_array(clientInfo.assetsByChunkName.main)[0]}`;
const chunk_files = clientInfo.assets.map(chunk => `/client/${chunk.name}`);
const service_worker = generate_service_worker(chunk_files);
const index = generate_index(main_file);
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
fs.writeFileSync(path.join(dest, 'index.html'), index);
return {
client: {
main_file,
chunk_files,
main: read(`${dest}${main_file}`),
chunks: chunk_files.reduce((lookup, file) => {
lookup[file] = read(`${dest}${file}`);
return lookup;
}, {}),
routes: route_manager.routes.reduce((lookup, route) => {
lookup[route.id] = `/client/${ensure_array(clientInfo.assetsByChunkName[route.id])[0]}`;
return lookup;
}, {}),
index,
service_worker
},
server: {
entry: path.resolve(dest, 'server', serverInfo.assetsByChunkName.main)
}
};
};
function generate_service_worker(chunk_files) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
const route_code = `[${
route_manager.routes
.filter(route => route.type === 'page')
.map(route => `{ pattern: ${route.pattern} }`)
.join(', ')
}]`;
return read('templates/service-worker.js')
.replace(/__timestamp__/g, Date.now())
.replace(/__assets__/g, JSON.stringify(assets))
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
.replace(/__routes__/g, route_code);
}
function generate_index(main_file) {
return templates.render(200, {
styles: '',
head: '',
html: '<noscript>Please enable JavaScript!</noscript>',
main: main_file
});
}
function read(file) {
return fs.readFileSync(file, 'utf-8');
}

View File

@@ -1,3 +1,4 @@
--require source-map-support/register
--recursive
test/unit/**/*.js
test/common/test.js

577
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,15 @@
{
"name": "sapper",
"version": "0.5.0",
"version": "0.6.1",
"description": "Military-grade apps, engineered by Svelte",
"main": "lib/index.js",
"main": "middleware.js",
"bin": {
"sapper": "cli/index.js"
"sapper": "cli.js"
},
"files": [
"cli",
"lib",
"cli.js",
"core.js",
"middleware.js",
"runtime",
"runtime.js",
"webpack"
@@ -22,8 +23,11 @@
"chokidar": "^1.7.0",
"code-frame": "^5.0.0",
"escape-html": "^1.0.3",
"express": "^4.16.2",
"glob": "^7.1.2",
"locate-character": "^2.0.5",
"mkdirp": "^0.5.1",
"node-fetch": "^1.7.3",
"relative": "^3.0.2",
"require-relative": "^0.8.7",
"rimraf": "^2.6.2",
@@ -35,20 +39,24 @@
"webpack-hot-middleware": "^2.21.0"
},
"devDependencies": {
"@std/esm": "^0.19.7",
"@types/glob": "^5.0.34",
"@types/mkdirp": "^0.5.2",
"@types/rimraf": "^2.0.2",
"css-loader": "^0.28.7",
"eslint": "^4.13.1",
"eslint-plugin-import": "^2.8.0",
"express": "^4.16.2",
"get-port": "^3.2.0",
"mocha": "^4.0.1",
"nightmare": "^2.10.0",
"node-fetch": "^1.7.3",
"npm-run-all": "^4.1.2",
"rollup": "^0.53.0",
"rollup-plugin-typescript": "^0.8.1",
"source-map-support": "^0.5.2",
"style-loader": "^0.19.1",
"svelte": "^1.49.1",
"svelte-loader": "^2.3.2",
"ts-node": "^4.1.0",
"tslib": "^1.8.1",
"typescript": "^2.6.2",
"wait-on": "^2.0.2"

View File

@@ -1,13 +1,97 @@
import typescript from 'rollup-plugin-typescript';
import pkg from './package.json';
const external = [].concat(
Object.keys(pkg.dependencies),
Object.keys(process.binding('natives')),
'sapper/core.js'
);
const paths = {
'sapper/core.js': './core.js'
};
export default [
// cli.js
{
input: 'src/cli/index.ts',
output: {
file: 'cli.js',
format: 'cjs',
banner: '#!/usr/bin/env node',
paths,
sourcemap: true
},
external,
plugins: [
typescript({
typescript: require('typescript')
})
]
},
// core.js
{
input: 'src/core/index.ts',
output: {
file: 'core.js',
format: 'cjs',
banner: '#!/usr/bin/env node',
paths,
sourcemap: true
},
external,
plugins: [
typescript({
typescript: require('typescript')
})
]
},
// middleware.js
{
input: 'src/middleware/index.ts',
output: {
file: 'middleware.js',
format: 'cjs',
paths,
sourcemap: true
},
external,
plugins: [
typescript({
typescript: require('typescript')
})
]
},
// runtime.js
{
input: 'src/runtime/index.ts',
output: {
file: 'runtime.js',
format: 'es'
format: 'es',
paths,
sourcemap: true
},
external,
plugins: [
typescript({
typescript: require('typescript')
})
]
},
// webpack/config.js
{
input: 'src/webpack/index.ts',
output: {
file: 'webpack/config.js',
format: 'cjs',
paths,
sourcemap: true
},
external,
plugins: [
typescript({
typescript: require('typescript')

1
runtime/README.md Normal file
View File

@@ -0,0 +1 @@
This directory exists for legacy reasons and should be deleted before releasing version 1.

View File

@@ -1,12 +1,11 @@
#!/usr/bin/env node
const build = require('../lib/build.js');
import { build, export as exporter } from 'sapper/core.js';
import { dest, dev, entry, src } from '../config';
const cmd = process.argv[2];
const start = Date.now();
if (cmd === 'build') {
build()
build({ dest, dev, entry, src })
.then(() => {
const elapsed = Date.now() - start;
console.error(`built in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'
@@ -17,8 +16,8 @@ if (cmd === 'build') {
} else if (cmd === 'export') {
const start = Date.now();
build()
.then(() => require('../lib/utils/export.js')())
build({ dest, dev, entry, src })
.then(() => exporter({ src, dest }))
.then(() => {
const elapsed = Date.now() - start;
console.error(`extracted in ${elapsed}ms`); // TODO beautify this, e.g. 'built in 4.7 seconds'

12
src/config.ts Normal file
View File

@@ -0,0 +1,12 @@
import * as path from 'path';
export const dev = process.env.NODE_ENV !== 'production';
export const templates = path.resolve(process.env.SAPPER_TEMPLATES || 'templates');
export const src = path.resolve(process.env.SAPPER_ROUTES || 'routes');
export const dest = path.resolve(process.env.SAPPER_DEST || '.sapper');
export const entry = {
client: path.resolve(templates, '.main.rendered.js'),
server: path.resolve(dest, 'server-entry.js')
};

62
src/core/build.ts Normal file
View File

@@ -0,0 +1,62 @@
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import create_compilers from './create_compilers.js';
import create_app from './create_app.js';
import create_assets from './create_assets.js';
export default function build({
src,
dest,
dev,
entry
}: {
src: string;
dest: string;
dev: boolean;
entry: { client: string, server: string }
}) {
mkdirp.sync(dest);
rimraf.sync(path.join(dest, '**/*'));
// create main.js and server-routes.js
create_app({ dev, entry, src });
return new Promise((fulfil, reject) => {
function handleErrors(err, stats) {
if (err) {
reject(err);
process.exit(1);
}
if (stats.hasErrors()) {
console.error(stats.toString({ colors: true }));
reject(new Error(`Encountered errors while building app`));
}
}
const { client, server } = create_compilers();
client.run((err, client_stats) => {
handleErrors(err, client_stats);
const client_info = client_stats.toJson();
fs.writeFileSync(
path.join(dest, 'stats.client.json'),
JSON.stringify(client_info, null, ' ')
);
server.run((err, server_stats) => {
handleErrors(err, server_stats);
const server_info = server_stats.toJson();
fs.writeFileSync(
path.join(dest, 'stats.server.json'),
JSON.stringify(server_info, null, ' ')
);
create_assets({ src, dest, dev, client_info, server_info });
fulfil();
});
});
});
}

90
src/core/create_app.ts Normal file
View File

@@ -0,0 +1,90 @@
import * as fs from 'fs';
import * as path from 'path';
import create_routes from './create_routes';
function posixify(file: string) {
return file.replace(/[/\\]/g, '/');
}
function fudge_mtime(file: string) {
// need to fudge the mtime so that webpack doesn't go doolally
const { atime, mtime } = fs.statSync(file);
fs.utimesSync(
file,
new Date(atime.getTime() - 999999),
new Date(mtime.getTime() - 999999)
);
}
function create_app({
src,
dev,
entry
}: {
src: string;
dev: boolean;
entry: { client: string; server: string };
}) {
const routes = create_routes({ src });
function create_client_main() {
const code = `[${routes
.filter(route => route.type === 'page')
.map(route => {
const params =
route.dynamic.length === 0
? '{}'
: `{ ${route.dynamic
.map((part, i) => `${part}: match[${i + 1}]`)
.join(', ')} }`;
const file = posixify(`${src}/${route.file}`);
return `{ pattern: ${
route.pattern
}, params: match => (${params}), load: () => import(/* webpackChunkName: "${
route.id
}" */ '${file}') }`;
})
.join(', ')}]`;
let main = fs
.readFileSync('templates/main.js', 'utf-8')
.replace(
/__app__/g,
posixify(path.resolve(__dirname, '../../runtime/app.js'))
)
.replace(/__routes__/g, code)
.replace(/__dev__/g, String(dev));
if (dev) {
const hmr_client = posixify(
require.resolve(`webpack-hot-middleware/client`)
);
main += `\n\nimport('${hmr_client}?path=/__webpack_hmr&timeout=20000'); if (module.hot) module.hot.accept();`;
}
fs.writeFileSync(entry.client, main);
fudge_mtime(entry.client);
}
function create_server_routes() {
const imports = routes
.map(route => {
const file = posixify(`${src}/${route.file}`);
return route.type === 'page'
? `import ${route.id} from '${file}';`
: `import * as ${route.id} from '${file}';`;
})
.join('\n');
const exports = `export { ${routes.map(route => route.id)} };`;
fs.writeFileSync(entry.server, `${imports}\n\n${exports}`);
fudge_mtime(entry.server);
}
create_client_main();
create_server_routes();
}
export default create_app;

87
src/core/create_assets.ts Normal file
View File

@@ -0,0 +1,87 @@
import * as fs from 'fs';
import * as path from 'path';
import glob from 'glob';
import { create_templates, render } from './templates';
import create_routes from './create_routes';
function ensure_array(thing) {
return Array.isArray(thing) ? thing : [thing]; // omg webpack what the HELL are you doing
}
export default function create_assets({ src, dest, dev, client_info, server_info }) {
create_templates(); // TODO refactor this...
const main_file = `/client/${ensure_array(client_info.assetsByChunkName.main)[0]}`;
const chunk_files = client_info.assets.map(chunk => `/client/${chunk.name}`);
const service_worker = generate_service_worker({ chunk_files, src });
const index = generate_index(main_file);
const routes = create_routes({ src });
if (dev) { // TODO move this into calling code
fs.writeFileSync(path.join(dest, 'service-worker.js'), service_worker);
fs.writeFileSync(path.join(dest, 'index.html'), index);
}
return {
client: {
main_file,
chunk_files,
main: read(`${dest}${main_file}`),
chunks: chunk_files.reduce((lookup, file) => {
lookup[file] = read(`${dest}${file}`);
return lookup;
}, {}),
// TODO confusing that `routes` refers to an array *and* a lookup
routes: routes.reduce((lookup, route) => {
lookup[route.id] = `/client/${ensure_array(client_info.assetsByChunkName[route.id])[0]}`;
return lookup;
}, {}),
index,
service_worker
},
server: {
entry: path.resolve(dest, 'server', server_info.assetsByChunkName.main)
},
service_worker
};
}
function generate_service_worker({ chunk_files, src }) {
const assets = glob.sync('**', { cwd: 'assets', nodir: true });
const routes = create_routes({ src });
const route_code = `[${
routes
.filter(route => route.type === 'page')
.map(route => `{ pattern: ${route.pattern} }`)
.join(', ')
}]`;
return read('templates/service-worker.js')
.replace(/__timestamp__/g, Date.now())
.replace(/__assets__/g, JSON.stringify(assets))
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
.replace(/__routes__/g, route_code);
}
function generate_index(main_file) {
return render(200, {
styles: '',
head: '',
html: '<noscript>Please enable JavaScript!</noscript>',
main: main_file
});
}
function read(file) {
return fs.readFileSync(file, 'utf-8');
}

View File

@@ -0,0 +1,16 @@
import * as path from 'path';
import relative from 'require-relative';
export default function create_compilers() {
const webpack = relative('webpack', process.cwd());
return {
client: webpack(
require(path.resolve('webpack.client.config.js'))
),
server: webpack(
require(path.resolve('webpack.server.config.js'))
)
};
}

View File

@@ -1,6 +1,7 @@
const path = require('path');
import * as path from 'path';
import glob from 'glob';
module.exports = function create_matchers(files) {
export default function create_routes({ src, files = glob.sync('**/*.+(html|js|mjs)', { cwd: src }) }) {
const routes = files
.map(file => {
if (/(^|\/|\\)_/.test(file)) return;
@@ -78,7 +79,7 @@ module.exports = function create_matchers(files) {
const b_is_dynamic = b_part[0] === '[';
if (a_is_dynamic === b_is_dynamic) {
if (!a_is_dynamic) same = false;
if (!a_is_dynamic && a_part !== b_part) same = false;
continue;
}
@@ -87,4 +88,4 @@ module.exports = function create_matchers(files) {
});
return routes;
};
}

View File

@@ -1,22 +1,36 @@
const sander = require('sander');
const app = require('express')();
const cheerio = require('cheerio');
const fetch = require('node-fetch');
const URL = require('url-parse');
const sapper = require('../index.js');
import * as path from 'path';
import * as sander from 'sander';
import express from 'express';
import cheerio from 'cheerio';
import fetch from 'node-fetch';
import URL from 'url-parse';
import create_assets from './create_assets.js';
// import middleware from '../middleware/index.js';
const { PORT = 3000, OUTPUT_DIR = 'dist' } = process.env;
const { dest } = require('../config.js');
const origin = `http://localhost:${PORT}`;
module.exports = function() {
const app = express();
function read_json(file) {
return JSON.parse(sander.readFileSync(file, { encoding: 'utf-8' }));
}
export default function exporter({ src, dest }) { // TODO dest is a terrible name in this context
// Prep output directory
sander.rimrafSync(OUTPUT_DIR);
const { service_worker } = create_assets({
src, dest,
dev: false,
client_info: read_json(path.join(dest, 'stats.client.json')),
server_info: read_json(path.join(dest, 'stats.server.json'))
});
sander.copydirSync('assets').to(OUTPUT_DIR);
sander.copydirSync(`${dest}/client`).to(`${OUTPUT_DIR}/client`);
sander.copyFileSync(`${dest}/service-worker.js`).to(`${OUTPUT_DIR}/service-worker.js`);
sander.copydirSync(dest, 'client').to(OUTPUT_DIR, 'client');
sander.writeFileSync(OUTPUT_DIR, 'service-worker.js', service_worker);
// Intercept server route fetches
function save(res) {
@@ -27,7 +41,7 @@ module.exports = function() {
let dest = OUTPUT_DIR + pathname;
const type = res.headers.get('Content-Type');
if (type.startsWith('text/html;')) dest += '/index.html';
if (type.startsWith('text/html')) dest += '/index.html';
sander.writeFileSync(dest, body);
@@ -49,7 +63,7 @@ module.exports = function() {
return fetch(url, opts);
};
app.use(sapper());
app.use(require('./middleware')()); // TODO this is filthy
const server = app.listen(PORT);
const seen = new Set();
@@ -84,4 +98,4 @@ module.exports = function() {
return handle(new URL(origin)) // TODO all static routes
.then(() => server.close());
};
}

11
src/core/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import { create_templates, render, stream } from './templates'; // TODO templates is an anomaly... fix post-#91
export { default as build } from './build';
export { default as export } from './export.js';
export { default as create_app } from './create_app';
export { default as create_assets } from './create_assets';
export { default as create_compilers } from './create_compilers';
export { default as create_routes } from './create_routes';
export const templates = { create_templates, render, stream };

View File

@@ -1,9 +1,8 @@
const fs = require('fs');
const glob = require('glob');
const chalk = require('chalk');
const framer = require('code-frame');
const { locate } = require('locate-character');
const { dev } = require('./config.js');
import * as fs from 'fs';
import glob from 'glob';
import chalk from 'chalk';
import framer from 'code-frame';
import { locate } from 'locate-character';
let templates;
@@ -16,7 +15,7 @@ function error(e) {
process.exit(1);
}
function create_templates() {
export function create_templates() {
templates = glob.sync('*.html', { cwd: 'templates' })
.map(file => {
const template = fs.readFileSync(`templates/${file}`, 'utf-8');
@@ -97,31 +96,20 @@ function create_templates() {
};
})
.sort((a, b) => b.specificity - a.specificity);
return templates;
}
create_templates();
if (dev) {
const watcher = require('chokidar').watch('templates/**.html', {
ignoreInitial: true,
persistent: false
});
watcher.on('add', create_templates);
watcher.on('change', create_templates);
watcher.on('unlink', create_templates);
}
exports.render = (status, data) => {
export function render(status, data) {
const template = templates.find(template => template.test(status));
if (template) return template.render(data);
return `Missing template for status code ${status}`;
};
}
exports.stream = (res, status, data) => {
export function stream(res, status, data) {
const template = templates.find(template => template.test(status));
if (template) return template.stream(res, data);
return `Missing template for status code ${status}`;
};
}

View File

@@ -1,9 +1,8 @@
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const compilers = require('./compilers.js');
const generate_asset_cache = require('./generate_asset_cache.js');
const { dest } = require('../config.js');
import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';
import { create_app, create_assets, create_routes, templates } from 'sapper/core.js';
import { dest } from '../config.js';
function deferred() {
const d = {};
@@ -16,7 +15,7 @@ function deferred() {
return d;
}
module.exports = function create_watcher() {
export default function create_watcher({ compilers, dev, entry, src, onroutes }) {
const deferreds = {
client: deferred(),
server: deferred()
@@ -32,10 +31,11 @@ module.exports = function create_watcher() {
const server_info = server_stats.toJson();
fs.writeFileSync(path.join(dest, 'stats.server.json'), JSON.stringify(server_info, null, ' '));
return generate_asset_cache(
client_stats.toJson(),
server_stats.toJson()
);
return create_assets({
src, dest, dev,
client_info: client_stats.toJson(),
server_info: server_stats.toJson()
});
});
function watch_compiler(type) {
@@ -60,6 +60,34 @@ module.exports = function create_watcher() {
});
}
const chokidar = require('chokidar');
function watch_files(pattern, callback) {
const watcher = chokidar.watch(pattern, {
persistent: false
});
watcher.on('add', callback);
watcher.on('change', callback);
watcher.on('unlink', callback);
}
watch_files('routes/**/*.+(html|js|mjs)', () => {
const routes = create_routes({ src });
onroutes(routes);
create_app({ dev, entry, src }); // TODO this calls `create_routes` again, we should pass `routes` to `create_app` instead
});
watch_files('templates/main.js', () => {
create_app({ dev, entry, src });
});
watch_files('templates/**.html', () => {
templates.create_templates();
// TODO reload current page?
});
const watcher = {
ready: invalidate(),
client: watch_compiler('client'),
@@ -72,4 +100,4 @@ module.exports = function create_watcher() {
};
return watcher;
};
}

View File

@@ -1,19 +1,28 @@
const fs = require('fs');
const path = require('path');
const serialize = require('serialize-javascript');
const route_manager = require('./route_manager.js');
const templates = require('./templates.js');
const create_app = require('./utils/create_app.js');
const create_watcher = require('./utils/create_watcher.js');
const compilers = require('./utils/compilers.js');
const generate_asset_cache = require('./utils/generate_asset_cache.js');
const escape_html = require('escape-html');
const { dest, dev } = require('./config.js');
import * as fs from 'fs';
import * as path from 'path';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import serialize from 'serialize-javascript';
import escape_html from 'escape-html';
import { create_routes, templates, create_compilers, create_assets } from 'sapper/core.js';
import create_watcher from './create_watcher';
import { dest, dev, entry, src } from '../config';
function connect_dev() {
create_app();
mkdirp.sync(dest);
rimraf.sync(path.join(dest, '**/*'));
const watcher = create_watcher();
const compilers = create_compilers();
let routes;
const watcher = create_watcher({
dev, entry, src,
compilers,
onroutes: _ => {
routes = _;
}
});
let asset_cache;
@@ -54,7 +63,7 @@ function connect_dev() {
fn: pathname => asset_cache.client.chunks[pathname]
}),
get_route_handler(() => asset_cache),
get_route_handler(() => asset_cache, () => routes),
get_not_found_handler(() => asset_cache)
]);
@@ -68,10 +77,14 @@ function connect_dev() {
}
function connect_prod() {
const asset_cache = generate_asset_cache(
read_json(path.join(dest, 'stats.client.json')),
read_json(path.join(dest, 'stats.server.json'))
);
const asset_cache = create_assets({
src, dest,
dev: false,
client_info: read_json(path.join(dest, 'stats.client.json')),
server_info: read_json(path.join(dest, 'stats.server.json'))
});
const routes = create_routes({ src }); // TODO rename update
const middleware = compose_handlers([
set_req_pathname,
@@ -97,7 +110,7 @@ function connect_prod() {
fn: pathname => asset_cache.client.chunks[pathname]
}),
get_route_handler(() => asset_cache),
get_route_handler(() => asset_cache, () => routes),
get_not_found_handler(() => asset_cache)
]);
@@ -109,10 +122,10 @@ function connect_prod() {
return middleware;
}
module.exports = dev ? connect_dev : connect_prod;
export default dev ? connect_dev : connect_prod;
function set_req_pathname(req, res, next) {
req.pathname = req.url.replace(/\?.+/, '');
req.pathname = req.url.replace(/\?.*/, '');
next();
}
@@ -120,26 +133,28 @@ function get_asset_handler(opts) {
return (req, res, next) => {
if (!opts.filter(req.pathname)) return next();
res.set({
'Content-Type': opts.type,
'Cache-Control': opts.cache
});
res.setHeader('Content-Type', opts.type);
res.setHeader('Cache-Control', opts.cache);
res.end(opts.fn(req.pathname));
};
}
const resolved = Promise.resolve();
function get_route_handler(fn) {
function get_route_handler(get_assets, get_routes) {
function handle_route(route, req, res, next, { client, server }) {
req.params = route.exec(req.pathname);
const mod = require(server.entry)[route.id];
if (route.type === 'page') {
// for page routes, we're going to serve some HTML
res.setHeader('Content-Type', 'text/html');
// preload main.js and current route
// TODO detect other stuff we can preload? images, CSS, fonts?
res.set('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
res.setHeader('Link', `<${client.main_file}>;rel="preload";as="script", <${client.routes[route.id]}>;rel="preload";as="script"`);
const data = { params: req.params, query: req.query };
@@ -197,22 +212,18 @@ function get_route_handler(fn) {
return function find_route(req, res, next) {
const url = req.pathname;
// whatever happens, we're going to serve some HTML
res.set({
'Content-Type': 'text/html'
});
resolved
.then(() => {
for (const route of route_manager.routes) {
if (route.test(url)) return handle_route(route, req, res, next, fn());
const routes = get_routes();
for (const route of routes) {
if (route.test(url)) return handle_route(route, req, res, next, get_assets());
}
// no matching route — 404
next();
})
.catch(err => {
res.status(500);
res.statusCode = 500;
res.end(templates.render(500, {
title: (err && err.name) || 'Internal server error',
url,
@@ -227,7 +238,7 @@ function get_not_found_handler(fn) {
return function handle_not_found(req, res) {
const asset_cache = fn();
res.status(404);
res.statusCode = 404;
res.end(templates.render(404, {
title: 'Not found',
status: 404,
@@ -268,4 +279,4 @@ function try_serialize(data) {
} catch (err) {
return null;
}
}
}

View File

@@ -1,5 +1,5 @@
import { detach, findAnchor, scroll_state, which } from './utils';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition } from './interfaces';
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition, Target } from './interfaces';
export let component: Component;
let target: Node;
@@ -19,7 +19,7 @@ if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
function select_route(url: URL): { route: Route, data: RouteData } {
function select_route(url: URL): Target {
if (url.origin !== window.location.origin) return null;
for (const route of routes) {
@@ -28,9 +28,13 @@ function select_route(url: URL): { route: Route, data: RouteData } {
const params = route.params(match);
const query: Record<string, string | true> = {};
for (const [key, value] of url.searchParams) query[key] = value || true;
return { route, data: { params, query } };
if (url.search.length > 0) {
url.search.slice(1).split('&').forEach(searchParam => {
const [, key, value] = /([^=]+)=(.*)/.exec(searchParam);
query[key] = value || true;
})
}
return { url, route, data: { params, query } };
}
}
}
@@ -60,7 +64,7 @@ function render(Component: ComponentConstructor, data: any, scroll: ScrollPositi
component = new Component({
target,
data,
hydrate: !!component
hydrate: !component
});
if (scroll) {
@@ -83,35 +87,31 @@ function prepare_route(Component: ComponentConstructor, data: RouteData) {
});
}
function navigate(url: URL, id: number) {
const selected = select_route(url);
if (selected) {
if (id) {
// popstate or initial navigation
cid = id;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
}
const loaded = prefetching && prefetching.href === url.href ?
prefetching.promise :
selected.route.load().then(mod => prepare_route(mod.default, selected.data));
prefetching = null;
const token = current_token = {};
loaded.then(({ Component, data }) => {
render(Component, data, scroll_history[id], token);
});
function navigate(target: Target, id: number) {
if (id) {
// popstate or initial navigation
cid = id;
return true;
} else {
// clicked on a link. preserve scroll state
scroll_history[cid] = scroll_state();
id = cid = ++uid;
scroll_history[cid] = { x: 0, y: 0 };
}
cid = id;
const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise :
target.route.load().then(mod => prepare_route(mod.default, target.data));
prefetching = null;
const token = current_token = {};
return loaded.then(({ Component, data }) => {
render(Component, data, scroll_history[id], token);
});
}
function handle_click(event: MouseEvent) {
@@ -147,7 +147,9 @@ function handle_click(event: MouseEvent) {
// Don't handle hash changes
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
if (navigate(url, null)) {
const target = select_route(url);
if (target) {
navigate(target, null);
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
}
@@ -157,7 +159,9 @@ function handle_popstate(event: PopStateEvent) {
scroll_history[cid] = scroll_state();
if (event.state) {
navigate(new URL(window.location.href), event.state.id);
const url = new URL(window.location.href);
const target = select_route(url);
navigate(target, event.state.id);
} else {
// hashchange
cid = ++uid;
@@ -205,23 +209,27 @@ export function init(_target: Node, _routes: Route[]) {
inited = true;
}
setTimeout(() => {
return Promise.resolve().then(() => {
const { hash, href } = window.location;
const deep_linked = hash && document.querySelector(hash);
const deep_linked = hash && document.getElementById(hash.slice(1));
scroll_history[uid] = deep_linked ?
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
scroll_state();
history.replaceState({ id: uid }, '', href);
navigate(new URL(window.location.href), uid);
const target = select_route(new URL(window.location.href));
return navigate(target, uid);
});
}
export function goto(href: string, opts = { replaceState: false }) {
if (navigate(new URL(href, window.location.href), null)) {
const target = select_route(new URL(href, window.location.href));
if (target) {
navigate(target, null);
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
} else {
window.location.href = href;
}
}
}

View File

@@ -20,4 +20,10 @@ export type Route = {
export type ScrollPosition = {
x: number;
y: number;
};
export type Target = {
url: URL;
route: Route;
data: RouteData;
};

View File

@@ -1,6 +1,6 @@
const { dest, dev, entry } = require('../lib/config.js');
import { dest, dev, entry } from '../config';
module.exports = {
export default {
dev,
client: {

View File

@@ -6,13 +6,15 @@
<h1>Great success!</h1>
<figure>
<img src='/great-success.png'>
<img alt='borat' src='/great-success.png'>
<figcaption>HIGH FIVE!</figcaption>
</figure>
<p><strong>Try editing this file (routes/index.html) to test hot module reloading.</strong></p>
</Layout>
<div class='hydrate-test'></div>
<style>
h1, figure, p {
text-align: center;

View File

@@ -4,7 +4,11 @@
export default {
preload() {
return new Promise(fulfil => {
window.fulfil = fulfil;
if (typeof window !== 'undefined') {
window.fulfil = fulfil;
} else {
fulfil({});
}
});
}
};

View File

@@ -1,6 +1,5 @@
import { init } from '../../../runtime.js';
// `routes` is an array of route objects injected by Sapper
init(document.querySelector('#sapper'), __routes__);
window.READY = true;
window.init = () => {
return init(document.querySelector('#sapper'), __routes__);
};

View File

@@ -1,11 +1,10 @@
const path = require('path');
const assert = require('assert');
const Nightmare = require('nightmare');
const express = require('express');
const serve = require('serve-static');
const Nightmare = require('nightmare');
const getPort = require('get-port');
const fetch = require('node-fetch');
const walkSync = require('walk-sync');
const fetch = require('node-fetch');
run('production');
run('development');
@@ -41,7 +40,11 @@ function run(env) {
};
const res = {
set: (headers, value) => {
setHeader(header, value) {
result.headers[header] = value;
},
set(headers, value) {
if (typeof headers === 'string') {
return res.set({ [headers]: value });
}
@@ -49,15 +52,15 @@ function run(env) {
Object.assign(result.headers, headers);
},
status: code => {
status(code) {
result.status = code;
},
write: data => {
write(data) {
result.body += data;
},
end: data => {
end(data) {
result.body += data;
fulfil(result);
}
@@ -78,20 +81,26 @@ function run(env) {
let sapper;
if (env === 'production') {
const cli = path.resolve(__dirname, '../../cli/index.js');
exec_promise = exec(`${cli} build`).then(() => exec(`${cli} export`));
const cli = path.resolve(__dirname, '../../cli.js');
exec_promise = exec(`node ${cli} export`);
}
return exec_promise.then(() => {
const resolved = require.resolve('../..');
const resolved = require.resolve('../../middleware.js');
delete require.cache[resolved];
delete require.cache[require.resolve('../../core.js')]; // TODO remove this
sapper = require(resolved);
return getPort();
return require('get-port')();
}).then(port => {
PORT = port;
base = `http://localhost:${PORT}`;
Nightmare.action('init', function(done) {
this.evaluate_now(() => window.init(), done);
});
global.fetch = (url, opts) => {
if (url[0] === '/') url = `${base}${url}`;
return fetch(url, opts);
@@ -162,6 +171,12 @@ function run(env) {
});
});
it('serves /?', () => {
return nightmare.goto(`${base}?`).page.title().then(title => {
assert.equal(title, 'Great success!');
});
});
it('serves static route', () => {
return nightmare.goto(`${base}/about`).page.title().then(title => {
assert.equal(title, 'About this site');
@@ -175,7 +190,7 @@ function run(env) {
});
it('navigates to a new page without reloading', () => {
return nightmare.goto(base).wait(() => window.READY).wait(200)
return nightmare.goto(base).init().wait(100)
.then(() => {
return capture(() => nightmare.click('a[href="/about"]'));
})
@@ -195,7 +210,7 @@ function run(env) {
it('navigates programmatically', () => {
return nightmare
.goto(`${base}/about`)
.wait(() => window.READY)
.init()
.click('.goto')
.wait(() => window.location.pathname === '/blog/what-is-sapper')
.wait(100)
@@ -208,7 +223,7 @@ function run(env) {
it('prefetches programmatically', () => {
return nightmare
.goto(`${base}/about`)
.wait(() => window.READY)
.init()
.then(() => {
return capture(() => {
return nightmare
@@ -224,8 +239,7 @@ function run(env) {
it('scrolls to active deeplink', () => {
return nightmare
.goto(`${base}/blog/a-very-long-post#four`)
.wait(() => window.READY)
.wait(100)
.init()
.evaluate(() => window.scrollY)
.then(scrollY => {
assert.ok(scrollY > 0, scrollY);
@@ -235,8 +249,7 @@ function run(env) {
it('reuses prefetch promise', () => {
return nightmare
.goto(`${base}/blog`)
.wait(() => window.READY)
.wait(200)
.init().wait(100)
.then(() => {
return capture(() => {
return nightmare
@@ -263,7 +276,7 @@ function run(env) {
it('cancels navigation if subsequent navigation occurs during preload', () => {
return nightmare
.goto(base)
.wait(() => window.READY)
.init()
.click('a[href="/slow-preload"]')
.wait(100)
.click('a[href="/about"]')
@@ -290,7 +303,7 @@ function run(env) {
it('passes entire request object to preload', () => {
return nightmare
.goto(`${base}/show-url`)
.wait(() => window.READY)
.init()
.evaluate(() => document.querySelector('p').innerHTML)
.end().then(html => {
assert.equal(html, `URL is /show-url`);
@@ -300,7 +313,7 @@ function run(env) {
it('calls a delete handler', () => {
return nightmare
.goto(`${base}/delete-test`)
.wait(() => window.READY)
.init()
.click('.del')
.wait(() => window.deleted)
.evaluate(() => window.deleted.id)
@@ -308,6 +321,21 @@ function run(env) {
assert.equal(id, 42);
});
});
it('hydrates initial route', () => {
return nightmare.goto(base)
.wait('.hydrate-test')
.evaluate(() => {
window.el = document.querySelector('.hydrate-test');
})
.init()
.evaluate(() => {
return document.querySelector('.hydrate-test') === window.el;
})
.then(matches => {
assert.ok(matches);
});
});
});
describe('headers', () => {

View File

@@ -1,11 +1,11 @@
const path = require('path');
const assert = require('assert');
const create_routes = require('../../lib/utils/create_routes.js');
const { create_routes } = require('../../core.js');
describe('create_routes', () => {
it('sorts routes correctly', () => {
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']);
const routes = create_routes({
files: ['index.html', 'about.html', '[wildcard].html', 'post/foo.html', 'post/[id].html', 'post/bar.html']
});
assert.deepEqual(
routes.map(r => r.file),
@@ -21,7 +21,9 @@ describe('create_routes', () => {
});
it('generates params', () => {
const routes = create_routes(['index.html', 'about.html', '[wildcard].html', 'post/[id].html']);
const routes = create_routes({
files: ['index.html', 'about.html', '[wildcard].html', 'post/[id].html']
});
let file;
let params;
@@ -40,7 +42,9 @@ describe('create_routes', () => {
});
it('ignores files and directories with leading underscores', () => {
const routes = create_routes(['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']);
const routes = create_routes({
files: ['index.html', '_foo.html', 'a/_b/c/d.html', 'e/f/g/h.html', 'i/_j.html']
});
assert.deepEqual(
routes.map(r => r.file),
@@ -52,8 +56,12 @@ describe('create_routes', () => {
});
it('matches /foo/:bar before /:baz/qux', () => {
const a = create_routes(['foo/[bar].html', '[baz]/qux.html']);
const b = create_routes(['[baz]/qux.html', 'foo/[bar].html']);
const a = create_routes({
files: ['foo/[bar].html', '[baz]/qux.html']
});
const b = create_routes({
files: ['[baz]/qux.html', 'foo/[bar].html']
});
assert.deepEqual(
a.map(r => r.file),
@@ -68,12 +76,22 @@ describe('create_routes', () => {
it('fails if routes are indistinguishable', () => {
assert.throws(() => {
create_routes(['[foo].html', '[bar]/index.html']);
create_routes({
files: ['[foo].html', '[bar]/index.html']
});
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
assert.throws(() => {
create_routes({
files: ['foo.html', 'foo.js']
});
}, /The foo.html and foo.js routes clash/);
});
it('matches nested routes', () => {
const route = create_routes(['settings/[submenu].html'])[0];
const route = create_routes({
files: ['settings/[submenu].html']
})[0];
assert.deepEqual(route.exec('/settings/foo'), {
submenu: 'foo'
@@ -85,7 +103,9 @@ describe('create_routes', () => {
});
it('prefers index routes to nested routes', () => {
const routes = create_routes(['settings/[submenu].html', 'settings.html']);
const routes = create_routes({
files: ['settings/[submenu].html', 'settings.html']
});
assert.deepEqual(
routes.map(r => r.file),
@@ -94,7 +114,9 @@ describe('create_routes', () => {
});
it('matches deeply nested routes', () => {
const route = create_routes(['settings/[a]/[b]/index.html'])[0];
const route = create_routes({
files: ['settings/[a]/[b]/index.html']
})[0];
assert.deepEqual(route.exec('/settings/foo/bar'), {
a: 'foo',

View File

@@ -1 +0,0 @@
import 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000';