first crack at context-driven store

This commit is contained in:
Rich Harris
2019-01-31 21:42:29 -05:00
parent d486542a8b
commit f587161d7d
38 changed files with 150 additions and 100 deletions

3
.gitignore vendored
View File

@@ -12,4 +12,5 @@ sapper
runtime.js
dist
!rollup.config.js
templates/*.mjs
templates/app.mjs
templates/server.mjs

6
package-lock.json generated
View File

@@ -5713,9 +5713,9 @@
}
},
"svelte": {
"version": "3.0.0-alpha25",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.0.0-alpha25.tgz",
"integrity": "sha512-Qbziusyrhy2eeonijfpgq1s0CyzsMrwU5hz3+o+bASFjk5kKFCmKRGYLWRU5JnYFTphZyLw4jPdbBKWFDOMmng==",
"version": "3.0.0-alpha26",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.0.0-alpha26.tgz",
"integrity": "sha512-8++B3/arwhWggcvBYZnGDd9xKsYchqw4Os2haA38v6BSIcSSY706puNK01+PAdVvLCUJH/R4U6T8uV/fhjwVVw==",
"dev": true
},
"svelte-dev-helper": {

View File

@@ -11,7 +11,8 @@
"config",
"sapper",
"dist/*.js",
"templates/*.js"
"templates/*.js",
"templates/*.html"
],
"directories": {
"test": "test"
@@ -58,7 +59,7 @@
"sade": "^1.4.1",
"sander": "^0.6.0",
"sirv": "^0.2.2",
"svelte": "^3.0.0-alpha25",
"svelte": "^3.0.0-alpha26",
"svelte-loader": "^2.11.0",
"ts-node": "^8.0.2",
"typescript": "^3.1.3",

View File

@@ -17,7 +17,8 @@ function template(kind, external, target) {
input: `templates/src/${kind}/index.ts`,
output: {
file: `templates/${kind}.mjs`,
format: 'es'
format: 'es',
paths: id => id.replace('@sapper', '.')
},
external,
plugins: [
@@ -35,7 +36,7 @@ function template(kind, external, target) {
}
export default [
template('client', ['__ROOT__', '__ERROR__'], 'ES2017'),
template('app', ['__ROOT__', '__ERROR__', 'svelte', '@sapper/App.html'], 'ES2017'),
template('server', builtinModules, 'ES2015'),
{

View File

@@ -70,7 +70,7 @@ export async function build({
const manifest_data = create_manifest_data(routes);
// create src/manifest/client.js and src/manifest/server.js
// create src/node_modules/@sapper/app.mjs and server.mjs
create_main_manifests({
bundler,
manifest_data,

View File

@@ -72,7 +72,7 @@ class Watcher extends EventEmitter {
cwd = '.',
src = 'src',
routes = 'src/routes',
output = '__sapper__',
output = 'src/node_modules/@sapper',
static: static_files = 'static',
dest = '__sapper__/dev',
'dev-port': dev_port,
@@ -144,6 +144,10 @@ class Watcher extends EventEmitter {
}
const { cwd, src, dest, routes, output, static: static_files } = this.dirs;
rimraf.sync(path.join(output, '**/*'));
mkdirp.sync(output);
rimraf.sync(dest);
mkdirp.sync(`${dest}/client`);
if (this.bundler === 'rollup') copy_shimport(dest);

View File

@@ -10,7 +10,7 @@ const prog = sade('sapper').version(pkg.version);
if (process.argv[2] === 'start') {
// remove this in a future version
console.error(colors.bold.red(`'sapper start' has been removed`));
console.error(colors.bold().red(`'sapper start' has been removed`));
console.error(`Use 'node [build_dir]' instead`);
process.exit(1);
}
@@ -74,7 +74,7 @@ prog.command('dev')
watcher.on('ready', async (event: ReadyEvent) => {
if (first) {
console.log(colors.bold.cyan(`> Listening on http://localhost:${event.port}`));
console.log(colors.bold().cyan(`> Listening on http://localhost:${event.port}`));
if (opts.open) {
const { exec } = await import('child_process');
exec(`open http://localhost:${event.port}`);
@@ -85,7 +85,7 @@ prog.command('dev')
watcher.on('invalid', (event: InvalidEvent) => {
const changed = event.changed.map(filename => path.relative(process.cwd(), filename)).join(', ');
console.log(`\n${colors.bold.cyan(changed)} changed. rebuilding...`);
console.log(`\n${colors.bold().cyan(changed)} changed. rebuilding...`);
});
watcher.on('error', (event: ErrorEvent) => {
@@ -94,16 +94,16 @@ prog.command('dev')
});
watcher.on('fatal', (event: FatalEvent) => {
console.log(colors.bold.red(`> ${event.message}`));
console.log(colors.bold().red(`> ${event.message}`));
if (event.log) console.log(event.log);
});
watcher.on('build', (event: BuildEvent) => {
if (event.errors.length) {
console.log(colors.bold.red(`${event.type}`));
console.log(colors.bold().red(`${event.type}`));
event.errors.filter(e => !e.duplicate).forEach(error => {
if (error.file) console.log(colors.bold(error.file));
if (error.file) console.log(colors.bold()(error.file));
console.log(error.message);
});
@@ -112,10 +112,10 @@ prog.command('dev')
console.log(`${hidden} duplicate ${hidden === 1 ? 'error' : 'errors'} hidden\n`);
}
} else if (event.warnings.length) {
console.log(colors.bold.yellow(`${event.type}`));
console.log(colors.bold().yellow(`${event.type}`));
event.warnings.filter(e => !e.duplicate).forEach(warning => {
if (warning.file) console.log(colors.bold(warning.file));
if (warning.file) console.log(colors.bold()(warning.file));
console.log(warning.message);
});
@@ -124,11 +124,11 @@ prog.command('dev')
console.log(`${hidden} duplicate ${hidden === 1 ? 'warning' : 'warnings'} hidden\n`);
}
} else {
console.log(`${colors.bold.green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
console.log(`${colors.bold().green(`${event.type}`)} ${colors.gray(`(${format_milliseconds(event.duration)})`)}`);
}
});
} catch (err) {
console.log(colors.bold.red(`> ${err.message}`));
console.log(colors.bold().red(`> ${err.message}`));
console.log(colors.gray(err.stack));
process.exit(1);
}
@@ -169,9 +169,9 @@ prog.command('build [dest]')
require('./server/server.js');
`.replace(/^\t+/gm, '').trim());
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`node ${dest}`)} to run the app.`);
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`node ${dest}`)} to run the app.`);
} catch (err) {
console.log(`${colors.bold.red(`> ${err.message}`)}`);
console.log(`${colors.bold().red(`> ${err.message}`)}`);
console.log(colors.gray(err.stack));
process.exit(1);
}
@@ -222,24 +222,24 @@ prog.command('export [dest]')
timeout: opts.timeout,
oninfo: event => {
console.log(colors.bold.cyan(`> ${event.message}`));
console.log(colors.bold().cyan(`> ${event.message}`));
},
onfile: event => {
const size_color = event.size > 150000 ? colors.bold().red : event.size > 50000 ? colors.bold().yellow : colors.bold().gray;
const size_color = event.size > 150000 ? colors.bold()().red : event.size > 50000 ? colors.bold()().yellow : colors.bold()().gray;
const size_label = size_color(left_pad(pb(event.size), 10));
const file_label = event.status === 200
? event.file
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
: colors.bold()[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
console.log(`${size_label} ${file_label}`);
}
});
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold.cyan(`npx serve ${dest}`)} to run the app.`);
console.error(`\n> Finished in ${elapsed(start)}. Type ${colors.bold().cyan(`npx serve ${dest}`)} to run the app.`);
} catch (err) {
console.error(colors.bold.red(`> ${err.message}`));
console.error(colors.bold().red(`> ${err.message}`));
process.exit(1);
}
});
@@ -278,7 +278,7 @@ async function _build(
console.log();
console.log(c(`┌─${repeat('─', banner.length)}─┐`));
console.log(c(`${colors.bold(banner) }`));
console.log(c(`${colors.bold()(banner) }`));
console.log(c(`└─${repeat('─', banner.length)}─┘`));
console.log(event.result.print());

View File

@@ -153,7 +153,7 @@ export default function extract_css(client_result: CompileResult, components: Pa
chunks_with_css.add(chunk);
});
const entry = path.resolve(dirs.src, 'client.js');
const entry = path.resolve(dirs.src, 'app.mjs');
const entry_chunk = client_result.chunks.find(chunk => chunk.modules.indexOf(entry) !== -1);
const entry_chunk_dependencies: Set<Chunk> = new Set([entry_chunk]);

View File

@@ -3,6 +3,10 @@ import * as path from 'path';
import { posixify, stringify, walk, write_if_changed } from '../utils';
import { Page, PageComponent, ManifestData } from '../interfaces';
const app = fs.readFileSync(path.resolve(__dirname, '../templates/App.html'), 'utf-8');
const internal = fs.readFileSync(path.resolve(__dirname, '../templates/internal.mjs'), 'utf-8');
const layout = `<svelte:component this={child.component} {...child.props}/>`;
export function create_main_manifests({
bundler,
manifest_data,
@@ -31,12 +35,11 @@ export function create_main_manifests({
const client_manifest = generate_client(manifest_data, path_to_routes, bundler, dev, dev_port);
const server_manifest = generate_server(manifest_data, path_to_routes, cwd, src, dest, dev);
write_if_changed(
`${output}/_layout.html`,
`<svelte:component this={child.component} {...child.props}/>`
);
write_if_changed(`${output}/client.js`, client_manifest);
write_if_changed(`${output}/server.js`, server_manifest);
write_if_changed(`${output}/_layout.html`, layout);
write_if_changed(`${output}/internal.mjs`, internal);
write_if_changed(`${output}/App.html`, app);
write_if_changed(`${output}/app.mjs`, client_manifest);
write_if_changed(`${output}/server.mjs`, server_manifest);
}
export function create_serviceworker_manifest({ manifest_data, output, client_files, static_files }: {
@@ -80,7 +83,7 @@ function generate_client(
dev: boolean,
dev_port?: number
) {
const template_file = path.resolve(__dirname, '../templates/client.js');
const template_file = path.resolve(__dirname, '../templates/app.mjs');
const template = fs.readFileSync(template_file, 'utf-8');
const page_ids = new Set(manifest_data.pages.map(page =>
@@ -167,7 +170,7 @@ function generate_server(
dest: string,
dev: boolean
) {
const template_file = path.resolve(__dirname, '../templates/server.js');
const template_file = path.resolve(__dirname, '../templates/server.mjs');
const template = fs.readFileSync(template_file, 'utf-8');
const imports = [].concat(

View File

@@ -19,8 +19,10 @@ export type Template = {
stream: (req, res, data: Record<string, string | Promise<string>>) => void;
};
export type Store = {
get: () => any;
export type WritableStore<T> = {
set: (value: T) => void;
update: (fn: (value: T) => T) => void;
subscribe: (fn: (T: any) => void) => () => void;
};
export type PageComponent = {

13
templates/App.html Normal file
View File

@@ -0,0 +1,13 @@
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
import { CONTEXT_KEY } from './internal';
export let Root;
export let props;
export let session;
setContext(CONTEXT_KEY, writable(session));
</script>
<Root {...props}/>

1
templates/internal.mjs Normal file
View File

@@ -0,0 +1 @@
export const CONTEXT_KEY = {};

View File

@@ -1,5 +1,6 @@
import { tick } from 'svelte';
import RootComponent, * as RootComponentStatic from '__ROOT__';
import App from '@sapper/App.html';
import { preloading, page } from '../shared/stores';
import Root, * as RootStatic from '__ROOT__';
import ErrorComponent from '__ERROR__';
import {
Target,
@@ -128,7 +129,9 @@ export function navigate(target: Target, id: number, noscroll?: boolean, hash?:
cid = id;
if (root_component) {
root_component.$set({ preloading: true });
preloading.set({
// TODO path, params, query
});
}
const loaded = prefetching && prefetching.href === target.url.href ?
prefetching.promise :
@@ -150,6 +153,8 @@ export function navigate(target: Target, id: number, noscroll?: boolean, hash?:
async function render(props: any, nullable_depth: number, scroll: ScrollPosition, noscroll: boolean, hash: string, token: {}) {
if (current_token !== token) return;
preloading.set(null);
if (root_component) {
// first, clear out highest-level root component
let level = props.child;
@@ -160,18 +165,12 @@ async function render(props: any, nullable_depth: number, scroll: ScrollPosition
const { component } = level;
level.component = null;
root_component.$set({ child: props.child });
await tick();
root_component.props = props;
// then render new stuff
// TODO do we need to call `flush` before doing this?
level.component = component;
root_component.$set(props);
// if we need to scroll to a deep link, we need to
// wait for the current update to happen first
if (!noscroll && hash) await tick();
root_component.props = props;
} else {
// first load — remove SSR'd <head> contents
const start = document.querySelector('#sapper-head-start');
@@ -185,10 +184,13 @@ async function render(props: any, nullable_depth: number, scroll: ScrollPosition
Object.assign(props, root_data);
root_component = new RootComponent({
root_component = new App({
target,
props,
store,
props: {
Root,
props,
session: __SAPPER__.session
},
hydrate: true
});
}
@@ -247,7 +249,7 @@ export function prepare_page(target: Target): Promise<{
};
if (!root_preload) {
const preload_fn = RootComponentStatic['pre' + 'load']; // Rollup makes us jump through these hoops :(
const preload_fn = RootStatic['pre' + 'load']; // Rollup makes us jump through these hoops :(
root_preload = preload_fn
? initial_data.preloaded[0] || preload_fn.call(preload_context, {
path,
@@ -315,7 +317,6 @@ export function prepare_page(target: Target): Promise<{
return {
nullable_depth: 0,
data: Object.assign({}, props, {
preloading: false,
child: {
component: ErrorComponent,
props
@@ -327,7 +328,6 @@ export function prepare_page(target: Target): Promise<{
const props = { path, query, error: null, status: null };
const data = {
path,
preloading: false,
child: Object.assign({}, root_props.child, {
segment: segments[0]
})

View File

@@ -0,0 +1,13 @@
import { getContext } from 'svelte';
import { CONTEXT_KEY } from '@sapper/internal';
import * as stores from '../shared/stores';
export const preloading = { subscribe: stores.preloading.subscribe };
export const page = { subscribe: stores.page.subscribe };
export const getSession = () => getContext(CONTEXT_KEY);
export { default as start } from './start/index';
export { default as goto } from './goto/index';
export { default as prefetch } from './prefetch/index';
export { default as prefetchRoutes } from './prefetchRoutes/index';

View File

@@ -6,25 +6,21 @@ import {
scroll_history,
scroll_state,
select_route,
set_store,
set_target,
uid,
set_uid,
set_cid
} from '../app';
import prefetch from '../prefetch/index';
import { Store, ScrollPosition } from '../types';
export default function start(opts: {
target: Node,
store?: (data: any) => Store
target: Node
}) {
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
set_target(opts.target);
if (opts.store) set_store(opts.store);
addEventListener('click', handle_click);
addEventListener('popstate', handle_popstate);

View File

@@ -1,4 +0,0 @@
export { default as start } from './start/index';
export { default as goto } from './goto/index';
export { default as prefetch } from './prefetch/index';
export { default as prefetchRoutes } from './prefetchRoutes/index';

View File

@@ -4,12 +4,14 @@ import cookie from 'cookie';
import devalue from 'devalue';
import fetch from 'node-fetch';
import { URL, resolve } from 'url';
import * as stores from '../../shared/stores';
import { build_dir, dev, src_dir, IGNORE } from '../placeholders';
import { Manifest, Page, Props, Req, Res, Store } from './types';
import { Manifest, Page, Props, Req, Res } from './types';
import App from '@sapper/App.html';
export function get_page_handler(
manifest: Manifest,
store_getter: (req: Req, res: Res) => Store
session_getter: (req: Req, res: Res) => any
) {
const get_build_info = dev
? () => JSON.parse(fs.readFileSync(path.join(build_dir, 'build.json'), 'utf-8'))
@@ -76,7 +78,7 @@ export function get_page_handler(
res.setHeader('Link', link);
}
const store = store_getter ? store_getter(req, res) : null;
const session = session_getter(req, res);
let redirect: { statusCode: number, location: string };
let preload_error: { statusCode: number, message: Error | string };
@@ -127,8 +129,7 @@ export function get_page_handler(
}
return fetch(parsed.href, opts);
},
store
}
};
let preloaded;
@@ -177,11 +178,6 @@ export function get_page_handler(
return;
}
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
store: store && try_serialize(store.get())
};
const segments = req.path.split('/').filter(Boolean);
const props: Props = {
@@ -223,15 +219,30 @@ export function get_page_handler(
level = level.props.child;
}
const { html, head, css } = manifest.root.render(data, {
store
stores.page.set({
path: req.path,
query: req.query,
params: req.params
});
const { html, head, css } = App.render({
Root: manifest.root,
props: data,
session
});
const serialized = {
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
session: session && try_serialize(session, err => {
throw new Error(`Failed to serialize session data: ${err.message}`);
})
};
let script = `__SAPPER__={${[
error && `error:1`,
`baseUrl:"${req.baseUrl}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.store && `store:${serialized.store}`
serialized.session && `session:${serialized.session}`
].filter(Boolean).join(',')}};`;
if (has_service_worker) {
@@ -320,10 +331,11 @@ function read_template(dir = build_dir) {
return fs.readFileSync(`${dir}/template.html`, 'utf-8');
}
function try_serialize(data: any) {
function try_serialize(data: any, fail?: (err) => void) {
try {
return devalue(data);
} catch (err) {
if (fail) fail(err);
return null;
}
}

View File

@@ -7,10 +7,10 @@ import { get_page_handler } from './get_page_handler';
import { lookup } from './mime';
export default function middleware(opts: {
store?: (req: Req, res: Res) => Store,
session?: (req: Req, res: Res) => any,
ignore?: any
} = {}) {
const { store, ignore } = opts;
const { session, ignore } = opts;
let emitted_basepath = false;
@@ -73,7 +73,7 @@ export default function middleware(opts: {
get_server_route_handler(manifest.server_routes),
get_page_handler(manifest, store)
get_page_handler(manifest, session || noop)
].filter(Boolean));
}
@@ -140,4 +140,6 @@ export function serve({ prefix, pathname, cache_control }: {
next();
}
};
}
}
function noop(){}

View File

@@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
export const preloading = writable(null);
export const page = writable(null);

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -7,12 +7,13 @@
</script>
<script>
export let preloading;
import { preloading } from '@sapper/app';
export let child;
export let rootPreloadFunctionRan;
</script>
{#if preloading}
{#if $preloading}
<progress class='preloading-progress' value=0.5/>
{/if}

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')

View File

@@ -1,6 +1,6 @@
<script>
import * as sapper from '@sapper/client';
const session = sapper.session();
import { getSession } from '@sapper/app';
const session = getSession();
</script>
<h1>{$session.title}</h1>

View File

@@ -1,4 +1,4 @@
import * as sapper from '@sapper/client';
import * as sapper from '@sapper/app';
window.start = () => sapper.start({
target: document.querySelector('#sapper')