mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-14 03:54:46 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1e632057 | ||
|
|
aeeb231477 | ||
|
|
d1940db8c0 | ||
|
|
98f9a64b64 | ||
|
|
b9bef802d3 | ||
|
|
a7024b3806 | ||
|
|
423e02aeae | ||
|
|
12b73ecebf | ||
|
|
e1bc38b5a7 | ||
|
|
b66f624f01 | ||
|
|
502dd547d1 | ||
|
|
4c343490d2 | ||
|
|
b3027c5816 | ||
|
|
c29e8022cc | ||
|
|
e4cd4c9cb0 | ||
|
|
feddad42b2 | ||
|
|
3c4ebcda30 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
test/app/.sapper
|
test/app/.sapper
|
||||||
|
runtime.js
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
# sapper changelog
|
# sapper changelog
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
* Move `sapper/runtime/app.js` to `sapper/runtime.js`
|
||||||
|
* Cancel navigation if overtaken by second navigation ([#48](https://github.com/sveltejs/sapper/issues/48))
|
||||||
|
* Store preloaded data, to avoiding double prefetching ([#49](https://github.com/sveltejs/sapper/issues/49))
|
||||||
|
* Pass server request object to `preload` ([#54](https://github.com/sveltejs/sapper/pull/54))
|
||||||
|
* Nested routes ([#55](https://github.com/sveltejs/sapper/issues/55))
|
||||||
|
|
||||||
## 0.2.10
|
## 0.2.10
|
||||||
|
|
||||||
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
* Handle deep links correctly ([#44](https://github.com/sveltejs/sapper/issues/44))
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function get_route_handler(fn) {
|
|||||||
const data = { params: req.params, query: req.query };
|
const data = { params: req.params, query: req.query };
|
||||||
|
|
||||||
if (mod.preload) {
|
if (mod.preload) {
|
||||||
const promise = Promise.resolve(mod.preload(data)).then(preloaded => {
|
const promise = Promise.resolve(mod.preload(req)).then(preloaded => {
|
||||||
Object.assign(data, preloaded);
|
Object.assign(data, preloaded);
|
||||||
return mod.render(data);
|
return mod.render(data);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,9 +16,22 @@ module.exports = function create_matchers(files) {
|
|||||||
.filter(part => part[0] === '[')
|
.filter(part => part[0] === '[')
|
||||||
.map(part => part.slice(1, -1));
|
.map(part => part.slice(1, -1));
|
||||||
|
|
||||||
const pattern = new RegExp(
|
let pattern_string = '';
|
||||||
`^\\/${parts.map(p => p[0] === '[' ? '([^/]+)' : p).join('\\/')}$`
|
let i = parts.length;
|
||||||
);
|
let nested = true;
|
||||||
|
while (i--) {
|
||||||
|
const part = parts[i];
|
||||||
|
const dynamic = part[0] === '[';
|
||||||
|
|
||||||
|
if (dynamic) {
|
||||||
|
pattern_string = nested ? `(?:\\/([^/]+)${pattern_string})?` : `\\/([^/]+)${pattern_string}`;
|
||||||
|
} else {
|
||||||
|
nested = false;
|
||||||
|
pattern_string = `\\/${part}${pattern_string}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern = new RegExp(`^${pattern_string || '\\/'}$`);
|
||||||
|
|
||||||
const test = url => pattern.test(url);
|
const test = url => pattern.test(url);
|
||||||
|
|
||||||
@@ -58,8 +71,8 @@ module.exports = function create_matchers(files) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!a_part) return 1;
|
if (!a_part) return -1;
|
||||||
if (!b_part) return -1;
|
if (!b_part) return 1;
|
||||||
|
|
||||||
const a_is_dynamic = a_part[0] === '[';
|
const a_is_dynamic = a_part[0] === '[';
|
||||||
const b_is_dynamic = b_part[0] === '[';
|
const b_is_dynamic = b_part[0] === '[';
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ function generate_service_worker(chunk_files) {
|
|||||||
}]`;
|
}]`;
|
||||||
|
|
||||||
return read('templates/service-worker.js')
|
return read('templates/service-worker.js')
|
||||||
.replace('__timestamp__', Date.now())
|
.replace(/__timestamp__/g, Date.now())
|
||||||
.replace('__assets__', JSON.stringify(assets))
|
.replace(/__assets__/g, JSON.stringify(assets))
|
||||||
.replace('__shell__', JSON.stringify(chunk_files.concat('/index.html')))
|
.replace(/__shell__/g, JSON.stringify(chunk_files.concat('/index.html')))
|
||||||
.replace('__routes__', route_code);
|
.replace(/__routes__/g, route_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generate_index(main_file) {
|
function generate_index(main_file) {
|
||||||
|
|||||||
69
package-lock.json
generated
69
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.2.7",
|
"version": "0.2.10",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -790,6 +790,12 @@
|
|||||||
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"compare-versions": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-Htwfk2h/2XoyXFn1XkWgfbEGrKY=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -1707,6 +1713,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
|
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
|
||||||
},
|
},
|
||||||
|
"estree-walker": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.2.1.tgz",
|
||||||
|
"integrity": "sha1-va/oCVOD2EFNXcLs9MkXO225QS4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"esutils": {
|
"esutils": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
|
||||||
@@ -5583,6 +5595,43 @@
|
|||||||
"inherits": "2.0.3"
|
"inherits": "2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rollup": {
|
||||||
|
"version": "0.53.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-0.53.0.tgz",
|
||||||
|
"integrity": "sha512-bG5RzkF7wcOHmKoVAFtERZ5P9TNJP9/AF+ldwGm/Rx6pejura+Z9BDU0GJtzWu+lYXwjfINmgiCclhLJzP/OXA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"rollup-plugin-typescript": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-typescript/-/rollup-plugin-typescript-0.8.1.tgz",
|
||||||
|
"integrity": "sha1-L/fuzCHPa7K0P8J+W2iJUs5xkko=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"compare-versions": "2.0.1",
|
||||||
|
"object-assign": "4.1.1",
|
||||||
|
"rollup-pluginutils": "1.5.2",
|
||||||
|
"tippex": "2.3.1",
|
||||||
|
"typescript": "1.8.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"typescript": {
|
||||||
|
"version": "1.8.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-1.8.10.tgz",
|
||||||
|
"integrity": "sha1-tHXW4N/wv1DyluXKbvn7tccyDx4=",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rollup-pluginutils": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz",
|
||||||
|
"integrity": "sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"estree-walker": "0.2.1",
|
||||||
|
"minimatch": "3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"run-async": {
|
"run-async": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
||||||
@@ -6191,6 +6240,12 @@
|
|||||||
"setimmediate": "1.0.5"
|
"setimmediate": "1.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tippex": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tippex/-/tippex-2.3.1.tgz",
|
||||||
|
"integrity": "sha1-ov1bcIfXy/sgyYBqbBYQjCwPr9o=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.0.33",
|
"version": "0.0.33",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
|
||||||
@@ -6229,6 +6284,12 @@
|
|||||||
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
|
"integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"tslib": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.1.tgz",
|
||||||
|
"integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"tty-browserify": {
|
"tty-browserify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||||
@@ -6275,6 +6336,12 @@
|
|||||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"typescript": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz",
|
||||||
|
"integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "2.8.29",
|
"version": "2.8.29",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,11 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "sapper",
|
"name": "sapper",
|
||||||
"version": "0.2.10",
|
"version": "0.3.0",
|
||||||
"description": "Military-grade apps, engineered by Svelte",
|
"description": "Military-grade apps, engineered by Svelte",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"sapper": "cli/index.js"
|
"sapper": "cli/index.js"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"cli",
|
||||||
|
"lib",
|
||||||
|
"runtime",
|
||||||
|
"webpack"
|
||||||
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
@@ -30,14 +36,21 @@
|
|||||||
"nightmare": "^2.10.0",
|
"nightmare": "^2.10.0",
|
||||||
"node-fetch": "^1.7.3",
|
"node-fetch": "^1.7.3",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
|
"rollup": "^0.53.0",
|
||||||
|
"rollup-plugin-typescript": "^0.8.1",
|
||||||
"style-loader": "^0.19.1",
|
"style-loader": "^0.19.1",
|
||||||
"svelte": "^1.49.1",
|
"svelte": "^1.49.1",
|
||||||
"svelte-loader": "^2.3.2",
|
"svelte-loader": "^2.3.2",
|
||||||
|
"tslib": "^1.8.1",
|
||||||
|
"typescript": "^2.6.2",
|
||||||
"wait-on": "^2.0.2"
|
"wait-on": "^2.0.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"test": "mocha --opts mocha.opts"
|
"test": "mocha --opts mocha.opts",
|
||||||
|
"pretest": "npm run build",
|
||||||
|
"build": "rollup -c",
|
||||||
|
"dev": "rollup -cw"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/sveltejs/sapper",
|
"repository": "https://github.com/sveltejs/sapper",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
17
rollup.config.js
Normal file
17
rollup.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import typescript from 'rollup-plugin-typescript';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// runtime.js
|
||||||
|
{
|
||||||
|
input: 'src/runtime/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'runtime.js',
|
||||||
|
format: 'es'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
typescript: require('typescript')
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
213
runtime/app.js
213
runtime/app.js
@@ -1,211 +1,2 @@
|
|||||||
const detach = node => {
|
console.error('sapper/runtime/app.js has been deprecated in favour of sapper/runtime.js');
|
||||||
node.parentNode.removeChild(node);
|
export * from '../runtime.js';
|
||||||
};
|
|
||||||
|
|
||||||
export let component;
|
|
||||||
let target;
|
|
||||||
let routes;
|
|
||||||
|
|
||||||
const history = typeof window !== 'undefined' ? window.history : {
|
|
||||||
pushState: () => {},
|
|
||||||
replaceState: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const scroll_history = {};
|
|
||||||
let uid = 1;
|
|
||||||
let cid;
|
|
||||||
|
|
||||||
if ('scrollRestoration' in history) {
|
|
||||||
history.scrollRestoration = 'manual';
|
|
||||||
}
|
|
||||||
|
|
||||||
function select_route(url) {
|
|
||||||
if (url.origin !== window.location.origin) return null;
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
const match = route.pattern.exec(url.pathname);
|
|
||||||
if (match) {
|
|
||||||
const params = route.params(match);
|
|
||||||
|
|
||||||
const query = {};
|
|
||||||
for (const [key, value] of url.searchParams) query[key] = value || true;
|
|
||||||
|
|
||||||
return { route, data: { params, query } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(Component, data, scroll) {
|
|
||||||
Promise.resolve(
|
|
||||||
Component.preload ? Component.preload(data) : {}
|
|
||||||
).then(preloaded => {
|
|
||||||
if (component) {
|
|
||||||
component.destroy();
|
|
||||||
} else {
|
|
||||||
// first load — remove SSR'd <head> contents
|
|
||||||
const start = document.querySelector('#sapper-head-start');
|
|
||||||
const end = document.querySelector('#sapper-head-end');
|
|
||||||
|
|
||||||
if (start && end) {
|
|
||||||
while (start.nextSibling !== end) detach(start.nextSibling);
|
|
||||||
detach(start);
|
|
||||||
detach(end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// preload additional routes
|
|
||||||
routes.reduce((promise, route) => promise.then(route.load), Promise.resolve());
|
|
||||||
}
|
|
||||||
|
|
||||||
component = new Component({
|
|
||||||
target,
|
|
||||||
data: Object.assign(data, preloaded),
|
|
||||||
hydrate: !!component
|
|
||||||
});
|
|
||||||
|
|
||||||
if (scroll) {
|
|
||||||
window.scrollTo(scroll.x, scroll.y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigate(url, id) {
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
selected.route.load().then(mod => {
|
|
||||||
render(mod.default, selected.data, scroll_history[id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
cid = id;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_click(event) {
|
|
||||||
// Adapted from https://github.com/visionmedia/page.js
|
|
||||||
// MIT license https://github.com/visionmedia/page.js#license
|
|
||||||
if (which(event) !== 1) return;
|
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
|
||||||
if (event.defaultPrevented) return;
|
|
||||||
|
|
||||||
const a = findAnchor(event.target);
|
|
||||||
if (!a) return;
|
|
||||||
|
|
||||||
// check if link is inside an svg
|
|
||||||
// in this case, both href and target are always inside an object
|
|
||||||
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
|
||||||
const href = svg ? a.href.baseVal : a.href;
|
|
||||||
|
|
||||||
if (href === window.location.href) {
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore if tag has
|
|
||||||
// 1. 'download' attribute
|
|
||||||
// 2. rel='external' attribute
|
|
||||||
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
|
||||||
|
|
||||||
// Ignore if <a> has a target
|
|
||||||
if (svg ? a.target.baseVal : a.target) return;
|
|
||||||
|
|
||||||
const url = new URL(href);
|
|
||||||
|
|
||||||
// Don't handle hash changes
|
|
||||||
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
|
||||||
|
|
||||||
if (navigate(url, null)) {
|
|
||||||
event.preventDefault();
|
|
||||||
history.pushState({ id: cid }, '', url.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handle_popstate(event) {
|
|
||||||
scroll_history[cid] = scroll_state();
|
|
||||||
|
|
||||||
if (event.state) {
|
|
||||||
navigate(new URL(window.location), event.state.id);
|
|
||||||
} else {
|
|
||||||
// hashchange
|
|
||||||
cid = ++uid;
|
|
||||||
history.replaceState({ id: cid }, '', window.location.href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prefetch(event) {
|
|
||||||
const a = findAnchor(event.target);
|
|
||||||
if (!a || a.rel !== 'prefetch') return;
|
|
||||||
|
|
||||||
const selected = select_route(new URL(a.href));
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
selected.route.load().then(mod => {
|
|
||||||
if (mod.default.preload) mod.default.preload(selected.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function findAnchor(node) {
|
|
||||||
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inited;
|
|
||||||
|
|
||||||
export function init(_target, _routes) {
|
|
||||||
target = _target;
|
|
||||||
routes = _routes;
|
|
||||||
|
|
||||||
if (!inited) { // this check makes HMR possible
|
|
||||||
window.addEventListener('click', handle_click);
|
|
||||||
window.addEventListener('popstate', handle_popstate);
|
|
||||||
|
|
||||||
// prefetch
|
|
||||||
window.addEventListener('touchstart', prefetch);
|
|
||||||
window.addEventListener('mouseover', prefetch);
|
|
||||||
|
|
||||||
inited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const { hash, href } = window.location;
|
|
||||||
|
|
||||||
const deep_linked = hash && document.querySelector(hash);
|
|
||||||
scroll_history[uid] = deep_linked ?
|
|
||||||
{ x: 0, y: deep_linked.getBoundingClientRect().top } :
|
|
||||||
scroll_state();
|
|
||||||
|
|
||||||
history.replaceState({ id: uid }, '', href);
|
|
||||||
navigate(new URL(window.location), uid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function which(event) {
|
|
||||||
event = event || window.event;
|
|
||||||
return event.which === null ? event.button : event.which;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scroll_state() {
|
|
||||||
return {
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function goto(href, opts = {}) {
|
|
||||||
if (navigate(new URL(href, window.location.href))) {
|
|
||||||
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
|
||||||
} else {
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
217
src/runtime/index.ts
Normal file
217
src/runtime/index.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { detach, findAnchor, scroll_state, which } from './utils';
|
||||||
|
import { Component, ComponentConstructor, Params, Query, Route, RouteData, ScrollPosition } from './interfaces';
|
||||||
|
|
||||||
|
export let component: Component;
|
||||||
|
let target: Node;
|
||||||
|
let routes: Route[];
|
||||||
|
|
||||||
|
const history = typeof window !== 'undefined' ? window.history : {
|
||||||
|
pushState: (state: any, title: string, href: string) => {},
|
||||||
|
replaceState: (state: any, title: string, href: string) => {},
|
||||||
|
scrollRestoration: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const scroll_history: Record<string, ScrollPosition> = {};
|
||||||
|
let uid = 1;
|
||||||
|
let cid: number;
|
||||||
|
|
||||||
|
if ('scrollRestoration' in history) {
|
||||||
|
history.scrollRestoration = 'manual';
|
||||||
|
}
|
||||||
|
|
||||||
|
function select_route(url: URL): { route: Route, data: RouteData } {
|
||||||
|
if (url.origin !== window.location.origin) return null;
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
const match = route.pattern.exec(url.pathname);
|
||||||
|
if (match) {
|
||||||
|
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 } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_token: {};
|
||||||
|
|
||||||
|
function render(Component: ComponentConstructor, data: any, scroll: ScrollPosition, token: {}) {
|
||||||
|
if (current_token !== token) return;
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
component.destroy();
|
||||||
|
} else {
|
||||||
|
// first load — remove SSR'd <head> contents
|
||||||
|
const start = document.querySelector('#sapper-head-start');
|
||||||
|
const end = document.querySelector('#sapper-head-end');
|
||||||
|
|
||||||
|
if (start && end) {
|
||||||
|
while (start.nextSibling !== end) detach(start.nextSibling);
|
||||||
|
detach(start);
|
||||||
|
detach(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// preload additional routes
|
||||||
|
routes.reduce((promise: Promise<any>, route) => promise.then(route.load), Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
component = new Component({
|
||||||
|
target,
|
||||||
|
data,
|
||||||
|
hydrate: !!component
|
||||||
|
});
|
||||||
|
|
||||||
|
if (scroll) {
|
||||||
|
window.scrollTo(scroll.x, scroll.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare_route(Component, data) {
|
||||||
|
return Promise.resolve(
|
||||||
|
Component.preload ? Component.preload(data) : {}
|
||||||
|
).then(preloaded => {
|
||||||
|
Object.assign(data, preloaded)
|
||||||
|
return { Component, data };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
cid = id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_click(event: MouseEvent) {
|
||||||
|
// Adapted from https://github.com/visionmedia/page.js
|
||||||
|
// MIT license https://github.com/visionmedia/page.js#license
|
||||||
|
if (which(event) !== 1) return;
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
|
||||||
|
const a: HTMLAnchorElement | SVGAElement = <HTMLAnchorElement | SVGAElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a) return;
|
||||||
|
|
||||||
|
// check if link is inside an svg
|
||||||
|
// in this case, both href and target are always inside an object
|
||||||
|
const svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString';
|
||||||
|
const href = String(svg ? (<SVGAElement>a).href.baseVal : a.href);
|
||||||
|
|
||||||
|
if (href === window.location.href) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if tag has
|
||||||
|
// 1. 'download' attribute
|
||||||
|
// 2. rel='external' attribute
|
||||||
|
if (a.hasAttribute('download') || a.getAttribute('rel') === 'external') return;
|
||||||
|
|
||||||
|
// Ignore if <a> has a target
|
||||||
|
if (svg ? (<SVGAElement>a).target.baseVal : a.target) return;
|
||||||
|
|
||||||
|
const url = new URL(href);
|
||||||
|
|
||||||
|
// Don't handle hash changes
|
||||||
|
if (url.pathname === window.location.pathname && url.search === window.location.search) return;
|
||||||
|
|
||||||
|
if (navigate(url, null)) {
|
||||||
|
event.preventDefault();
|
||||||
|
history.pushState({ id: cid }, '', url.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_popstate(event: PopStateEvent) {
|
||||||
|
scroll_history[cid] = scroll_state();
|
||||||
|
|
||||||
|
if (event.state) {
|
||||||
|
navigate(new URL(window.location.href), event.state.id);
|
||||||
|
} else {
|
||||||
|
// hashchange
|
||||||
|
cid = ++uid;
|
||||||
|
history.replaceState({ id: cid }, '', window.location.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefetching: {
|
||||||
|
href: string;
|
||||||
|
promise: Promise<{ Component: ComponentConstructor, data: any }>;
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
function prefetch(event: MouseEvent | TouchEvent) {
|
||||||
|
const a: HTMLAnchorElement = <HTMLAnchorElement>findAnchor(<Node>event.target);
|
||||||
|
if (!a || a.rel !== 'prefetch') return;
|
||||||
|
|
||||||
|
const selected = select_route(new URL(a.href));
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
prefetching = {
|
||||||
|
href: a.href,
|
||||||
|
promise: selected.route.load().then(mod => prepare_route(mod.default, selected.data))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let inited: boolean;
|
||||||
|
|
||||||
|
export function init(_target: Node, _routes: Route[]) {
|
||||||
|
target = _target;
|
||||||
|
routes = _routes;
|
||||||
|
|
||||||
|
if (!inited) { // this check makes HMR possible
|
||||||
|
window.addEventListener('click', handle_click);
|
||||||
|
window.addEventListener('popstate', handle_popstate);
|
||||||
|
|
||||||
|
// prefetch
|
||||||
|
window.addEventListener('touchstart', prefetch);
|
||||||
|
window.addEventListener('mouseover', prefetch);
|
||||||
|
|
||||||
|
inited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const { hash, href } = window.location;
|
||||||
|
|
||||||
|
const deep_linked = hash && document.querySelector(hash);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function goto(href: string, opts = { replaceState: false }) {
|
||||||
|
if (navigate(new URL(href, window.location.href), null)) {
|
||||||
|
if (history) history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
|
||||||
|
} else {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/runtime/interfaces.ts
Normal file
23
src/runtime/interfaces.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export type Params = Record<string, string>;
|
||||||
|
export type Query = Record<string, string | true>;
|
||||||
|
export type RouteData = { params: Params, query: Query };
|
||||||
|
|
||||||
|
export interface ComponentConstructor {
|
||||||
|
new (options: { target: Node, data: any, hydrate: boolean }): Component;
|
||||||
|
preload: (data: { params: Params, query: Query }) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Component {
|
||||||
|
destroy: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
pattern: RegExp;
|
||||||
|
params: (match: RegExpExecArray) => Record<string, string>;
|
||||||
|
load: () => Promise<{ default: ComponentConstructor }>
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScrollPosition = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
19
src/runtime/utils.ts
Normal file
19
src/runtime/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export function detach(node: Node) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findAnchor(node: Node) {
|
||||||
|
while (node && node.nodeName.toUpperCase() !== 'A') node = node.parentNode; // SVG <a> elements have a lowercase name
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function which(event: MouseEvent) {
|
||||||
|
return event.which === null ? event.button : event.which;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scroll_state() {
|
||||||
|
return {
|
||||||
|
x: window.scrollX,
|
||||||
|
y: window.scrollY
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a class='{{page === "home" ? "selected" : ""}}' href='/'>home</a></li>
|
<li><a href='/'>home</a></li>
|
||||||
<li><a class='{{page === "about" ? "selected" : ""}}' href='/about'>about</a></li>
|
<li><a href='/about'>about</a></li>
|
||||||
|
<li><a href='/slow-preload'>slow preload</a></li>
|
||||||
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
|
|
||||||
the blog data when we hover over the link or tap it on a touchscreen -->
|
|
||||||
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
<li><a rel=prefetch class='{{page === "blog" ? "selected" : ""}}' href='/blog'>blog</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Layout from './_components/Layout.html';
|
import Layout from './_components/Layout.html';
|
||||||
import { goto } from '../../../runtime/app.js';
|
import { goto } from '../../../runtime.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function get(req, res, next) {
|
|||||||
if (slug in lookup) {
|
if (slug in lookup) {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Cache-Control': `max-age=${30 * 60 * 1e3}` // cache for 30 minutes
|
'Cache-Control': `no-cache`
|
||||||
});
|
});
|
||||||
|
|
||||||
res.end(lookup[slug]);
|
res.end(lookup[slug]);
|
||||||
|
|||||||
9
test/app/routes/show-url.html
Normal file
9
test/app/routes/show-url.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<p>URL is {{url}}</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload({ url }) {
|
||||||
|
return { url };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
11
test/app/routes/slow-preload.html
Normal file
11
test/app/routes/slow-preload.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<h1>This page should never render</h1>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
preload() {
|
||||||
|
return new Promise(fulfil => {
|
||||||
|
window.fulfil = fulfil;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { init } from '../../../runtime/app.js';
|
import { init } from '../../../runtime.js';
|
||||||
|
|
||||||
// `routes` is an array of route objects injected by Sapper
|
// `routes` is an array of route objects injected by Sapper
|
||||||
init(document.querySelector('#sapper'), __routes__);
|
init(document.querySelector('#sapper'), __routes__);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ run('development');
|
|||||||
|
|
||||||
function run(env) {
|
function run(env) {
|
||||||
describe(`env=${env}`, function () {
|
describe(`env=${env}`, function () {
|
||||||
this.timeout(5000);
|
this.timeout(20000);
|
||||||
|
|
||||||
let PORT;
|
let PORT;
|
||||||
let server;
|
let server;
|
||||||
@@ -208,6 +208,73 @@ function run(env) {
|
|||||||
|
|
||||||
assert.ok(scrollY > 0, scrollY);
|
assert.ok(scrollY > 0, scrollY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reuses prefetch promise', async () => {
|
||||||
|
await nightmare
|
||||||
|
.goto(`${base}/blog`)
|
||||||
|
.wait(() => window.READY)
|
||||||
|
.wait(200);
|
||||||
|
|
||||||
|
const mouseover_requests = (await capture(async () => {
|
||||||
|
await nightmare
|
||||||
|
.mouseover('[href="/blog/what-is-sapper"]')
|
||||||
|
.wait(200);
|
||||||
|
})).map(r => r.url);
|
||||||
|
|
||||||
|
assert.deepEqual(mouseover_requests, [
|
||||||
|
'/api/blog/what-is-sapper'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const click_requests = (await capture(async () => {
|
||||||
|
await nightmare
|
||||||
|
.click('[href="/blog/what-is-sapper"]')
|
||||||
|
.wait(200);
|
||||||
|
})).map(r => r.url);
|
||||||
|
|
||||||
|
assert.deepEqual(click_requests, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels navigation if subsequent navigation occurs during preload', async () => {
|
||||||
|
await nightmare
|
||||||
|
.goto(base)
|
||||||
|
.wait(() => window.READY)
|
||||||
|
.click('a[href="/slow-preload"]')
|
||||||
|
.wait(100)
|
||||||
|
.click('a[href="/about"]')
|
||||||
|
.wait(100);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await nightmare.path(),
|
||||||
|
'/about'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
||||||
|
'About this site'
|
||||||
|
);
|
||||||
|
|
||||||
|
await nightmare
|
||||||
|
.evaluate(() => window.fulfil({}))
|
||||||
|
.wait(100);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await nightmare.path(),
|
||||||
|
'/about'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await nightmare.evaluate(() => document.querySelector('h1').textContent),
|
||||||
|
'About this site'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes entire request object to preload', async () => {
|
||||||
|
const html = await nightmare
|
||||||
|
.goto(`${base}/show-url`)
|
||||||
|
.evaluate(() => document.querySelector('p').innerHTML);
|
||||||
|
|
||||||
|
assert.equal(html, `URL is /show-url`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('headers', () => {
|
describe('headers', () => {
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ describe('create_routes', () => {
|
|||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
[
|
[
|
||||||
|
'index.html',
|
||||||
|
'about.html',
|
||||||
'post/foo.html',
|
'post/foo.html',
|
||||||
'post/bar.html',
|
'post/bar.html',
|
||||||
'post/[id].html',
|
'post/[id].html',
|
||||||
'about.html',
|
'[wildcard].html'
|
||||||
'[wildcard].html',
|
|
||||||
'index.html'
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -45,8 +45,8 @@ describe('create_routes', () => {
|
|||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
routes.map(r => r.file),
|
routes.map(r => r.file),
|
||||||
[
|
[
|
||||||
'e/f/g/h.html',
|
'index.html',
|
||||||
'index.html'
|
'e/f/g/h.html'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -71,4 +71,44 @@ describe('create_routes', () => {
|
|||||||
create_routes(['[foo].html', '[bar]/index.html']);
|
create_routes(['[foo].html', '[bar]/index.html']);
|
||||||
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
}, /The \[foo\].html and \[bar\]\/index.html routes clash/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches nested routes', () => {
|
||||||
|
const route = create_routes(['settings/[submenu].html'])[0];
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings/foo'), {
|
||||||
|
submenu: 'foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings'), {
|
||||||
|
submenu: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers index routes to nested routes', () => {
|
||||||
|
const routes = create_routes(['settings/[submenu].html', 'settings.html']);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
routes.map(r => r.file),
|
||||||
|
['settings.html', 'settings/[submenu].html']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches deeply nested routes', () => {
|
||||||
|
const route = create_routes(['settings/[a]/[b]/index.html'])[0];
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings/foo/bar'), {
|
||||||
|
a: 'foo',
|
||||||
|
b: 'bar'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings/foo'), {
|
||||||
|
a: 'foo',
|
||||||
|
b: null
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(route.exec('/settings'), {
|
||||||
|
a: null,
|
||||||
|
b: null
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"diagnostics": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"lib": ["es5", "es6", "dom"],
|
||||||
|
"importHelpers": true
|
||||||
|
},
|
||||||
|
"target": "ES5",
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
const path = require('path');
|
const { dest, dev, entry } = require('../lib/config.js');
|
||||||
const { src, dest, dev, entry } = require('../lib/config.js');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
dev,
|
dev,
|
||||||
@@ -25,7 +24,7 @@ module.exports = {
|
|||||||
entry: () => {
|
entry: () => {
|
||||||
return {
|
return {
|
||||||
main: entry.server
|
main: entry.server
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
output: () => {
|
output: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user