diff --git a/.gitignore b/.gitignore index 7ecf62a..57aee3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,14 @@ .DS_Store yarn.lock +yarn-error.log node_modules cypress/screenshots test/app/.sapper test/app/app/manifest test/app/export test/app/build -runtime.js -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 -yarn-error.log \ No newline at end of file +*.js +*.js.map +*.ts.js +*.ts.js.map +!rollup.config.js \ No newline at end of file diff --git a/middleware.js b/middleware.js new file mode 100644 index 0000000..9e3b243 --- /dev/null +++ b/middleware.js @@ -0,0 +1,2 @@ +// TODO write to this file, instead of middleware.ts.js +module.exports = require('./middleware.ts.js'); \ No newline at end of file diff --git a/package.json b/package.json index 358fb4a..2ee260d 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,12 @@ "description": "Military-grade apps, engineered by Svelte", "main": "middleware.js", "bin": { - "sapper": "cli.js" + "sapper": "./sapper" }, "files": [ - "cli.js", - "core.js", - "middleware.js", + "*.js", + "*.ts.js", "runtime", - "runtime.js", - "sapper-dev-client.js", "webpack" ], "directories": { @@ -51,7 +48,7 @@ "mocha": "^4.0.1", "nightmare": "^2.10.0", "npm-run-all": "^4.1.2", - "rollup": "^0.53.0", + "rollup": "^0.56.5", "rollup-plugin-commonjs": "^8.3.0", "rollup-plugin-json": "^2.3.0", "rollup-plugin-string": "^2.0.2", diff --git a/rollup.config.js b/rollup.config.js index 6bad2d6..cea6673 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -10,36 +10,39 @@ const external = [].concat( 'sapper/core.js' ); -const paths = { - 'sapper/core.js': './core.js' -}; - -const plugins = [ - string({ - include: '**/*.md' - }), - json(), - commonjs(), - typescript({ - typescript: require('typescript') - }) -]; - export default [ - { name: 'cli', banner: true }, - { name: 'core' }, - { name: 'middleware' }, - { name: 'runtime', format: 'es' }, - { name: 'webpack', file: 'webpack/config' } -].map(obj => ({ - input: `src/${obj.name}/index.ts`, - output: { - file: `${obj.file || obj.name}.js`, - format: obj.format || 'cjs', - banner: obj.banner && '#!/usr/bin/env node', - paths, - sourcemap: true + { + input: `src/runtime/index.ts`, + output: { + file: `runtime.js`, + format: 'es' + }, + plugins: [ + typescript({ + typescript: require('typescript') + }) + ] }, - external, - plugins -})); + + { + input: [`src/cli.ts`, `src/core.ts`, `src/middleware.ts`, `src/webpack.ts`], + output: { + dir: '.', + format: 'cjs', + sourcemap: true + }, + external, + plugins: [ + string({ + include: '**/*.md' + }), + json(), + commonjs(), + typescript({ + typescript: require('typescript') + }) + ], + experimentalCodeSplitting: true, + experimentalDynamicImport: true + } +]; \ No newline at end of file diff --git a/runtime.js b/runtime.js new file mode 100644 index 0000000..a3db060 --- /dev/null +++ b/runtime.js @@ -0,0 +1,271 @@ +function detach(node) { + node.parentNode.removeChild(node); +} +function findAnchor(node) { + while (node && node.nodeName.toUpperCase() !== 'A') + node = node.parentNode; // SVG elements have a lowercase name + return node; +} +function which(event) { + return event.which === null ? event.button : event.which; +} +function scroll_state() { + return { + x: window.scrollX, + y: window.scrollY + }; +} + +var component; +var target; +var routes; +var errors; +var history = typeof window !== 'undefined' ? window.history : { + pushState: function (state, title, href) { }, + replaceState: function (state, title, href) { }, + scrollRestoration: '' +}; +var scroll_history = {}; +var uid = 1; +var cid; +if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; +} +function select_route(url) { + if (url.origin !== window.location.origin) + return null; + var _loop_1 = function (route) { + var match = route.pattern.exec(url.pathname); + if (match) { + if (route.ignore) + return { value: null }; + var params = route.params(match); + var query_1 = {}; + if (url.search.length > 0) { + url.search.slice(1).split('&').forEach(function (searchParam) { + var _a = /([^=]+)=(.*)/.exec(searchParam), key = _a[1], value = _a[2]; + query_1[key] = value || true; + }); + } + return { value: { url: url, route: route, data: { params: params, query: query_1 } } }; + } + }; + for (var _i = 0, routes_1 = routes; _i < routes_1.length; _i++) { + var route = routes_1[_i]; + var state_1 = _loop_1(route); + if (typeof state_1 === "object") + return state_1.value; + } +} +var current_token; +function render(Component, data, scroll, token) { + if (current_token !== token) + return; + if (component) { + component.destroy(); + } + else { + // first load — remove SSR'd contents + var start = document.querySelector('#sapper-head-start'); + var end = document.querySelector('#sapper-head-end'); + if (start && end) { + while (start.nextSibling !== end) + detach(start.nextSibling); + detach(start); + detach(end); + } + } + component = new Component({ + target: target, + data: data, + hydrate: !component + }); + if (scroll) { + window.scrollTo(scroll.x, scroll.y); + } +} +function prepare_route(Component, data) { + var redirect = null; + var error = null; + if (!Component.preload) { + return { Component: Component, data: data, redirect: redirect, error: error }; + } + if (!component && window.__SAPPER__ && window.__SAPPER__.preloaded) { + return { Component: Component, data: Object.assign(data, window.__SAPPER__.preloaded), redirect: redirect, error: error }; + } + return Promise.resolve(Component.preload.call({ + redirect: function (statusCode, location) { + redirect = { statusCode: statusCode, location: location }; + }, + error: function (statusCode, message) { + error = { statusCode: statusCode, message: message }; + } + }, data))["catch"](function (err) { + error = { statusCode: 500, message: err }; + }).then(function (preloaded) { + if (error) { + var route = error.statusCode >= 400 && error.statusCode < 500 + ? errors['4xx'] + : errors['5xx']; + return route.load().then(function (_a) { + var Component = _a["default"]; + var err = error.message instanceof Error ? error.message : new Error(error.message); + Object.assign(data, { status: error.statusCode, error: err }); + return { Component: Component, data: data, redirect: null }; + }); + } + Object.assign(data, preloaded); + return { Component: Component, data: data, redirect: redirect }; + }); +} +function navigate(target, id) { + 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 }; + } + cid = id; + var loaded = prefetching && prefetching.href === target.url.href ? + prefetching.promise : + target.route.load().then(function (mod) { return prepare_route(mod["default"], target.data); }); + prefetching = null; + var token = current_token = {}; + return loaded.then(function (_a) { + var Component = _a.Component, data = _a.data, redirect = _a.redirect; + if (redirect) { + return goto(redirect.location, { replaceState: true }); + } + render(Component, data, scroll_history[id], token); + }); +} +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; + var 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 + var svg = typeof a.href === 'object' && a.href.constructor.name === 'SVGAnimatedString'; + var href = String(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 has a target + if (svg ? a.target.baseVal : a.target) + return; + var url = new URL(href); + // Don't handle hash changes + if (url.pathname === window.location.pathname && url.search === window.location.search) + return; + var target = select_route(url); + if (target) { + navigate(target, null); + event.preventDefault(); + history.pushState({ id: cid }, '', url.href); + } +} +function handle_popstate(event) { + scroll_history[cid] = scroll_state(); + if (event.state) { + var url = new URL(window.location.href); + var target_1 = select_route(url); + navigate(target_1, event.state.id); + } + else { + // hashchange + cid = ++uid; + history.replaceState({ id: cid }, '', window.location.href); + } +} +var prefetching = null; +function prefetch(href) { + var selected = select_route(new URL(href)); + if (selected) { + prefetching = { + href: href, + promise: selected.route.load().then(function (mod) { return prepare_route(mod["default"], selected.data); }) + }; + } +} +function handle_touchstart_mouseover(event) { + var a = findAnchor(event.target); + if (!a || a.rel !== 'prefetch') + return; + prefetch(a.href); +} +var inited; +function init(_target, _routes) { + target = _target; + routes = _routes.filter(function (r) { return !r.error; }); + errors = { + '4xx': _routes.find(function (r) { return r.error === '4xx'; }), + '5xx': _routes.find(function (r) { return r.error === '5xx'; }) + }; + if (!inited) { + window.addEventListener('click', handle_click); + window.addEventListener('popstate', handle_popstate); + // prefetch + window.addEventListener('touchstart', handle_touchstart_mouseover); + window.addEventListener('mouseover', handle_touchstart_mouseover); + inited = true; + } + return Promise.resolve().then(function () { + var _a = window.location, hash = _a.hash, href = _a.href; + var 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); + var target = select_route(new URL(window.location.href)); + return navigate(target, uid); + }); +} +function goto(href, opts) { + if (opts === void 0) { opts = { replaceState: false }; } + var 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; + } +} +function prefetchRoutes(pathnames) { + if (!routes) + throw new Error("You must call init() first"); + return routes + .filter(function (route) { + if (!pathnames) + return true; + return pathnames.some(function (pathname) { + return route.error + ? route.error === pathname + : route.pattern.test(pathname); + }); + }) + .reduce(function (promise, route) { + return promise.then(route.load); + }, Promise.resolve()); +} + +export { component, prefetch, init, goto, prefetchRoutes, prefetchRoutes as preloadRoutes }; diff --git a/sapper b/sapper new file mode 100755 index 0000000..3f0b783 --- /dev/null +++ b/sapper @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./cli.ts.js'); \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts new file mode 100755 index 0000000..eeb7814 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,77 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as child_process from 'child_process'; +import sade from 'sade'; +import * as clorox from 'clorox'; +import prettyMs from 'pretty-ms'; +// import upgrade from './cli/upgrade'; +import * as ports from 'port-authority'; +import * as pkg from '../package.json'; + +const prog = sade('sapper').version(pkg.version); + +prog.command('dev') + .describe('Start a development server') + .option('-p, --port', 'Specify a port') + .action(async (opts: { port: number }) => { + const { dev } = await import('./cli/dev'); + dev(opts); + }); + +prog.command('build [dest]') + .describe('Create a production-ready version of your app') + .action(async (dest = 'build') => { + console.log(`> Building...`); + + process.env.NODE_ENV = 'production'; + process.env.SAPPER_DEST = dest; + + const start = Date.now(); + + try { + const { build } = await import('./cli/build'); + await build(); + console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(dest === 'build' ? 'npx sapper start' : `npx sapper start ${dest}`)} to run the app.`); + } catch (err) { + console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); + } + }); + +prog.command('start [dir]') + .describe('Start your app') + .option('-p, --port', 'Specify a port') + .action(async (dir = 'build', opts: { port: number }) => { + const { start } = await import('./cli/start'); + start(dir, opts); + }); + +prog.command('export [dest]') + .describe('Export your app as static files (if possible)') + .action(async (dest = 'export') => { + console.log(`> Building...`); + + process.env.NODE_ENV = 'production'; + process.env.SAPPER_DEST = '.sapper/.export'; + + const start = Date.now(); + + try { + const { build } = await import('./cli/build'); + await build(); + console.error(`\n> Built in ${elapsed(start)}. Exporting...`); + + const { exporter } = await import('./cli/export'); + await exporter(dest); + console.error(`\n> Finished in ${elapsed(start)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`); + } catch (err) { + console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); + } + }); + +// TODO upgrade + +prog.parse(process.argv); + +function elapsed(start: number) { + return prettyMs(Date.now() - start); +} \ No newline at end of file diff --git a/src/cli/build.ts b/src/cli/build.ts index f71b06e..c599594 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -3,10 +3,10 @@ import * as path from 'path'; import * as clorox from 'clorox'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; -import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js' +import { create_compilers, create_app, create_routes, create_serviceworker } from '../core' import { src, dest, dev } from '../config'; -export default async function build() { +export async function build() { const output = dest(); mkdirp.sync(output); @@ -20,12 +20,12 @@ export default async function build() { const { client, server, serviceworker } = create_compilers(); const client_stats = await compile(client); - console.log(clorox.inverse(`\nbuilt client`)); + console.log(clorox.inverse(`\nbuilt client`).toString()); console.log(client_stats.toString({ colors: true })); fs.writeFileSync(path.join(output, 'client_info.json'), JSON.stringify(client_stats.toJson())); const server_stats = await compile(server); - console.log(clorox.inverse(`\nbuilt server`)); + console.log(clorox.inverse(`\nbuilt server`).toString()); console.log(server_stats.toString({ colors: true })); let serviceworker_stats; @@ -38,7 +38,7 @@ export default async function build() { }); serviceworker_stats = await compile(serviceworker); - console.log(clorox.inverse(`\nbuilt service worker`)); + console.log(clorox.inverse(`\nbuilt service worker`).toString()); console.log(serviceworker_stats.toString({ colors: true })); } } diff --git a/src/cli/dev.ts b/src/cli/dev.ts index 2313479..99f9a9f 100644 --- a/src/cli/dev.ts +++ b/src/cli/dev.ts @@ -10,7 +10,7 @@ import format_messages from 'webpack-format-messages'; import prettyMs from 'pretty-ms'; import * as ports from 'port-authority'; import { dest } from '../config'; -import { create_compilers, create_app, create_routes, create_serviceworker } from 'sapper/core.js'; +import { create_compilers, create_app, create_routes, create_serviceworker } from '../core'; type Deferred = { promise?: Promise; @@ -70,9 +70,20 @@ function create_hot_update_server(port: number, interval = 10000) { return { send }; } -export default async function dev(port: number) { +export async function dev(opts: { port: number }) { process.env.NODE_ENV = 'development'; + let port = opts.port || +process.env.PORT; + + if (port) { + if (!await ports.check(port)) { + console.log(clorox.bold.red(`> Port ${port} is unavailable`)); + return; + } + } else { + port = await ports.find(3000); + } + const dir = dest(); rimraf.sync(dir); mkdirp.sync(dir); diff --git a/src/cli/export.ts b/src/cli/export.ts index 6dcf9d3..8e4598a 100644 --- a/src/cli/export.ts +++ b/src/cli/export.ts @@ -10,7 +10,7 @@ import { dest } from '../config'; const app = polka(); -export default async function exporter(export_dir: string) { +export async function exporter(export_dir: string) { const build_dir = dest(); // Prep output directory diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100755 index f6678ed..0000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as child_process from 'child_process'; -import sade from 'sade'; -import * as clorox from 'clorox'; -import prettyMs from 'pretty-ms'; -import help from './help.md'; -import build from './build'; -import exporter from './export'; -import dev from './dev'; -import upgrade from './upgrade'; -import * as ports from 'port-authority'; -import * as pkg from '../../package.json'; - -const prog = sade('sapper').version(pkg.version); - -prog.command('dev') - .describe('Start a development server') - .option('-p, --port', 'Specify a port') - .action(async (opts: { port: number }) => { - let port = opts.port || +process.env.PORT; - - if (port) { - if (!await ports.check(port)) { - console.log(clorox.bold.red(`> Port ${port} is unavailable`)); - return; - } - } else { - port = await ports.find(3000); - } - - dev(port); - }); - -prog.command('build [dest]') - .describe('Create a production-ready version of your app') - .action((dest = 'build') => { - console.log(`> Building...`); - - process.env.NODE_ENV = 'production'; - process.env.SAPPER_DEST = dest; - - const start = Date.now(); - - build() - .then(() => { - const elapsed = Date.now() - start; - console.error(`\n> Finished in ${prettyMs(elapsed)}. Type ${clorox.bold.cyan(dest === 'build' ? 'npx sapper start' : `npx sapper start ${dest}`)} to run the app.`); - }) - .catch(err => { - console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); - }); - }); - -prog.command('start [dir]') - .describe('Start your app') - .option('-p, --port', 'Specify a port') - .action(async (dir = 'build', opts: { port: number }) => { - let port = opts.port || +process.env.PORT; - - const resolved = path.resolve(dir); - const server = path.resolve(dir, 'server.js'); - - if (!fs.existsSync(server)) { - console.log(clorox.bold.red(`> ${dir}/server.js does not exist — type ${clorox.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)); - return; - } - - if (port) { - if (!await ports.check(port)) { - console.log(clorox.bold.red(`> Port ${port} is unavailable`)); - return; - } - } else { - port = await ports.find(3000); - } - - child_process.fork(server, [], { - cwd: process.cwd(), - env: Object.assign({ - NODE_ENV: 'production', - PORT: port, - SAPPER_DEST: dir - }, process.env) - }); - }); - -prog.command('export [dest]') - .describe('Export your app as static files (if possible)') - .action((dest = 'export') => { - console.log(`> Building...`); - - process.env.NODE_ENV = 'production'; - process.env.SAPPER_DEST = '.sapper/.export'; - - const start = Date.now(); - - build() - .then(() => { - const elapsed = Date.now() - start; - console.error(`\n> Built in ${prettyMs(elapsed)}. Exporting...`); - }) - .then(() => exporter(dest)) - .then(() => { - const elapsed = Date.now() - start; - console.error(`\n> Finished in ${prettyMs(elapsed)}. Type ${clorox.bold.cyan(`npx serve ${dest}`)} to run the app.`); - }) - .catch(err => { - console.error(err ? err.details || err.stack || err.message || err : 'Unknown error'); - }); - }); - -// TODO upgrade - -prog.parse(process.argv); diff --git a/src/cli/start.ts b/src/cli/start.ts new file mode 100644 index 0000000..ffb1ec8 --- /dev/null +++ b/src/cli/start.ts @@ -0,0 +1,35 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as child_process from 'child_process'; +import * as clorox from 'clorox'; +import * as ports from 'port-authority'; + +export async function start(dir: string, opts: { port: number }) { + let port = opts.port || +process.env.PORT; + + const resolved = path.resolve(dir); + const server = path.resolve(dir, 'server.js'); + + if (!fs.existsSync(server)) { + console.log(clorox.bold.red(`> ${dir}/server.js does not exist — type ${clorox.bold.cyan(dir === 'build' ? `npx sapper build` : `npx sapper build ${dir}`)} to create it`)); + return; + } + + if (port) { + if (!await ports.check(port)) { + console.log(clorox.bold.red(`> Port ${port} is unavailable`)); + return; + } + } else { + port = await ports.find(3000); + } + + child_process.fork(server, [], { + cwd: process.cwd(), + env: Object.assign({ + NODE_ENV: 'production', + PORT: port, + SAPPER_DEST: dir + }, process.env) + }); +} \ No newline at end of file diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..d6c2400 --- /dev/null +++ b/src/core.ts @@ -0,0 +1,4 @@ +export { default as create_app } from './core/create_app'; +export { default as create_serviceworker } from './core/create_serviceworker'; +export { default as create_compilers } from './core/create_compilers'; +export { default as create_routes } from './core/create_routes'; \ No newline at end of file diff --git a/src/core/index.ts b/src/core/index.ts deleted file mode 100644 index 80b7812..0000000 --- a/src/core/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as create_app } from './create_app'; -export { default as create_serviceworker } from './create_serviceworker'; -export { default as create_compilers } from './create_compilers'; -export { default as create_routes } from './create_routes'; \ No newline at end of file diff --git a/src/middleware/index.ts b/src/middleware.ts similarity index 97% rename from src/middleware/index.ts rename to src/middleware.ts index ebe0db3..6aa9e5e 100644 --- a/src/middleware/index.ts +++ b/src/middleware.ts @@ -4,10 +4,10 @@ import { ClientRequest, ServerResponse } from 'http'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; import devalue from 'devalue'; -import { lookup } from './mime'; -import { create_routes, templates, create_compilers } from 'sapper/core.js'; -import { dest, dev } from '../config'; -import { Route, Template } from '../interfaces'; +import { lookup } from './middleware/mime'; +import { create_routes, templates, create_compilers } from './core/index'; +import { dest, dev } from './config'; +import { Route, Template } from './interfaces'; import sourceMapSupport from 'source-map-support'; sourceMapSupport.install(); diff --git a/src/webpack/index.ts b/src/webpack.ts similarity index 95% rename from src/webpack/index.ts rename to src/webpack.ts index 5b29992..7fd3e86 100644 --- a/src/webpack/index.ts +++ b/src/webpack.ts @@ -1,4 +1,4 @@ -import { dest, dev } from '../config'; +import { dest, dev } from './config'; export default { dev: dev(), diff --git a/test/common/test.js b/test/common/test.js index f825e84..e40c87b 100644 --- a/test/common/test.js +++ b/test/common/test.js @@ -25,7 +25,7 @@ Nightmare.action('prefetchRoutes', function(done) { this.evaluate_now(() => window.prefetchRoutes(), done); }); -const cli = path.resolve(__dirname, '../../cli.js'); +const cli = path.resolve(__dirname, '../../sapper'); describe('sapper', function() { process.chdir(path.resolve(__dirname, '../app')); diff --git a/test/unit/create_routes.test.js b/test/unit/create_routes.test.js index a58b626..50924dd 100644 --- a/test/unit/create_routes.test.js +++ b/test/unit/create_routes.test.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const { create_routes } = require('../../core.js'); +const { create_routes } = require('../../core.ts.js'); describe('create_routes', () => { it('sorts routes correctly', () => { diff --git a/webpack.js b/webpack.js new file mode 100644 index 0000000..38e44b8 --- /dev/null +++ b/webpack.js @@ -0,0 +1,2 @@ +// TODO write to this file, instead of webpack.ts.js +module.exports = require('./webpack.ts.js'); \ No newline at end of file diff --git a/webpack/config.js b/webpack/config.js new file mode 100644 index 0000000..9e50d96 --- /dev/null +++ b/webpack/config.js @@ -0,0 +1,2 @@ +// TODO deprecate this file in favour of sapper/webpack.js +module.exports = require('../webpack.ts.js'); \ No newline at end of file