From 8f2b894acab0a33e36ea6cc69d9cbd296457cb87 Mon Sep 17 00:00:00 2001 From: Erick Sosa Date: Mon, 1 Feb 2021 17:23:56 -0400 Subject: [PATCH] feat: build in router lib --- README.md | 1 + compiler/types.ts | 1 + core/router/Link.js | 379 ++++++++++++++++++++++++++++ core/router/README.md | 150 +++++++++++ core/router/Route.js | 534 ++++++++++++++++++++++++++++++++++++++++ core/router/Router.js | 349 ++++++++++++++++++++++++++ core/router/actions.js | 83 +++++++ core/router/contexts.js | 2 + core/router/history.js | 108 ++++++++ core/router/mod.js | 5 + core/router/utils.js | 338 +++++++++++++++++++++++++ 11 files changed, 1950 insertions(+) create mode 100644 core/router/Link.js create mode 100644 core/router/README.md create mode 100644 core/router/Route.js create mode 100644 core/router/Router.js create mode 100644 core/router/actions.js create mode 100644 core/router/contexts.js create mode 100644 core/router/history.js create mode 100644 core/router/mod.js create mode 100644 core/router/utils.js diff --git a/README.md b/README.md index cb340be..fdd03a6 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ It is a `tool/framework` to compile .svelte component to javascript files to cre - [import maps](https://github.com/WICG/import-maps) support - support for scss and less out of the box - support for typescript and sass out of the box (soon) +- SSR (soon) ## What do I need to start using Snel? diff --git a/compiler/types.ts b/compiler/types.ts index d29a1e6..87da5e6 100644 --- a/compiler/types.ts +++ b/compiler/types.ts @@ -103,4 +103,5 @@ export interface BuildOptions { dev?: boolean; dist?: boolean; fileOutPut?: string; + generate?: "dom" | "ssr" } diff --git a/core/router/Link.js b/core/router/Link.js new file mode 100644 index 0000000..58f35ee --- /dev/null +++ b/core/router/Link.js @@ -0,0 +1,379 @@ +/* Link.svelte generated by Svelte v3.31.2 */ +import { + SvelteComponentDev, + add_location, + assign, + component_subscribe, + compute_rest_props, + create_slot, + detach_dev, + dispatch_dev, + element, + exclude_internal_props, + get_spread_update, + init, + insert_dev, + listen_dev, + safe_not_equal, + set_attributes, + transition_in, + transition_out, + update_slot, + validate_slots, + validate_store, +} from "https://cdn.skypack.dev/svelte@3.31.2/internal"; + +import { + getContext, + createEventDispatcher, +} from "https://cdn.skypack.dev/svelte@3.31.2/"; + +import { ROUTER, LOCATION } from "./contexts.js"; +import { navigate } from "./history.js"; +import { startsWith, resolve, shouldNavigate } from "./utils.js"; +const file = "Link.svelte"; + +function create_fragment(ctx) { + let a; + let current; + let mounted; + let dispose; + const default_slot_template = /*#slots*/ ctx[16].default; + const default_slot = create_slot( + default_slot_template, + ctx, + /*$$scope*/ ctx[15], + null + ); + + let a_levels = [ + { href: /*href*/ ctx[0] }, + { "aria-current": /*ariaCurrent*/ ctx[2] }, + /*props*/ ctx[1], + /*$$restProps*/ ctx[6], + ]; + + let a_data = {}; + + for (let i = 0; i < a_levels.length; i += 1) { + a_data = assign(a_data, a_levels[i]); + } + + const block = { + c: function create() { + a = element("a"); + if (default_slot) default_slot.c(); + set_attributes(a, a_data); + add_location(a, file, 40, 0, 1186); + }, + l: function claim(nodes) { + throw new Error( + "options.hydrate only works if the component was compiled with the `hydratable: true` option" + ); + }, + m: function mount(target, anchor) { + insert_dev(target, a, anchor); + + if (default_slot) { + default_slot.m(a, null); + } + + current = true; + + if (!mounted) { + dispose = listen_dev( + a, + "click", + /*onClick*/ ctx[5], + false, + false, + false + ); + mounted = true; + } + }, + p: function update(ctx, [dirty]) { + if (default_slot) { + if (default_slot.p && dirty & /*$$scope*/ 32768) { + update_slot( + default_slot, + default_slot_template, + ctx, + /*$$scope*/ ctx[15], + dirty, + null, + null + ); + } + } + + set_attributes( + a, + (a_data = get_spread_update(a_levels, [ + (!current || dirty & /*href*/ 1) && { href: /*href*/ ctx[0] }, + (!current || dirty & /*ariaCurrent*/ 4) && { + "aria-current": /*ariaCurrent*/ ctx[2], + }, + dirty & /*props*/ 2 && /*props*/ ctx[1], + dirty & /*$$restProps*/ 64 && /*$$restProps*/ ctx[6], + ])) + ); + }, + i: function intro(local) { + if (current) return; + transition_in(default_slot, local); + current = true; + }, + o: function outro(local) { + transition_out(default_slot, local); + current = false; + }, + d: function destroy(detaching) { + if (detaching) detach_dev(a); + if (default_slot) default_slot.d(detaching); + mounted = false; + dispose(); + }, + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_fragment.name, + type: "component", + source: "", + ctx, + }); + + return block; +} + +function instance($$self, $$props, $$invalidate) { + let ariaCurrent; + const omit_props_names = ["to", "replace", "state", "getProps"]; + let $$restProps = compute_rest_props($$props, omit_props_names); + let $base; + let $location; + let { $$slots: slots = {}, $$scope } = $$props; + validate_slots("Link", slots, ["default"]); + let { to = "#" } = $$props; + let { replace = false } = $$props; + let { state = {} } = $$props; + let { getProps = () => ({}) } = $$props; + const { base } = getContext(ROUTER); + validate_store(base, "base"); + component_subscribe($$self, base, (value) => + $$invalidate(13, ($base = value)) + ); + const location = getContext(LOCATION); + validate_store(location, "location"); + component_subscribe($$self, location, (value) => + $$invalidate(14, ($location = value)) + ); + const dispatch = createEventDispatcher(); + let href, isPartiallyCurrent, isCurrent, props; + + function onClick(event) { + dispatch("click", event); + + if (shouldNavigate(event)) { + event.preventDefault(); + + // Don't push another entry to the history stack when the user + // clicks on a Link to the page they are currently on. + const shouldReplace = $location.pathname === href || replace; + + navigate(href, { state, replace: shouldReplace }); + } + } + + $$self.$$set = ($$new_props) => { + $$props = assign(assign({}, $$props), exclude_internal_props($$new_props)); + $$invalidate( + 6, + ($$restProps = compute_rest_props($$props, omit_props_names)) + ); + if ("to" in $$new_props) $$invalidate(7, (to = $$new_props.to)); + if ("replace" in $$new_props) + $$invalidate(8, (replace = $$new_props.replace)); + if ("state" in $$new_props) $$invalidate(9, (state = $$new_props.state)); + if ("getProps" in $$new_props) + $$invalidate(10, (getProps = $$new_props.getProps)); + if ("$$scope" in $$new_props) + $$invalidate(15, ($$scope = $$new_props.$$scope)); + }; + + $$self.$capture_state = () => ({ + getContext, + createEventDispatcher, + ROUTER, + LOCATION, + navigate, + startsWith, + resolve, + shouldNavigate, + to, + replace, + state, + getProps, + base, + location, + dispatch, + href, + isPartiallyCurrent, + isCurrent, + props, + onClick, + $base, + $location, + ariaCurrent, + }); + + $$self.$inject_state = ($$new_props) => { + if ("to" in $$props) $$invalidate(7, (to = $$new_props.to)); + if ("replace" in $$props) $$invalidate(8, (replace = $$new_props.replace)); + if ("state" in $$props) $$invalidate(9, (state = $$new_props.state)); + if ("getProps" in $$props) + $$invalidate(10, (getProps = $$new_props.getProps)); + if ("href" in $$props) $$invalidate(0, (href = $$new_props.href)); + if ("isPartiallyCurrent" in $$props) + $$invalidate(11, (isPartiallyCurrent = $$new_props.isPartiallyCurrent)); + if ("isCurrent" in $$props) + $$invalidate(12, (isCurrent = $$new_props.isCurrent)); + if ("props" in $$props) $$invalidate(1, (props = $$new_props.props)); + if ("ariaCurrent" in $$props) + $$invalidate(2, (ariaCurrent = $$new_props.ariaCurrent)); + }; + + if ($$props && "$$inject" in $$props) { + $$self.$inject_state($$props.$$inject); + } + + $$self.$$.update = () => { + if ($$self.$$.dirty & /*to, $base*/ 8320) { + $: $$invalidate( + 0, + (href = to === "/" ? $base.uri : resolve(to, $base.uri)) + ); + } + + if ($$self.$$.dirty & /*$location, href*/ 16385) { + $: $$invalidate( + 11, + (isPartiallyCurrent = startsWith($location.pathname, href)) + ); + } + + if ($$self.$$.dirty & /*href, $location*/ 16385) { + $: $$invalidate(12, (isCurrent = href === $location.pathname)); + } + + if ($$self.$$.dirty & /*isCurrent*/ 4096) { + $: $$invalidate(2, (ariaCurrent = isCurrent ? "page" : undefined)); + } + + if ( + $$self.$$.dirty & + /*getProps, $location, href, isPartiallyCurrent, isCurrent*/ 23553 + ) { + $: $$invalidate( + 1, + (props = getProps({ + location: $location, + href, + isPartiallyCurrent, + isCurrent, + })) + ); + } + }; + + return [ + href, + props, + ariaCurrent, + base, + location, + onClick, + $$restProps, + to, + replace, + state, + getProps, + isPartiallyCurrent, + isCurrent, + $base, + $location, + $$scope, + slots, + ]; +} + +class Link extends SvelteComponentDev { + constructor(options) { + super(options); + + init(this, options, instance, create_fragment, safe_not_equal, { + to: 7, + replace: 8, + state: 9, + getProps: 10, + }); + + dispatch_dev("SvelteRegisterComponent", { + component: this, + tagName: "Link", + options, + id: create_fragment.name, + }); + } + + get to() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set to(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } + + get replace() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set replace(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } + + get state() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set state(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } + + get getProps() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set getProps(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } +} + +export default Link; diff --git a/core/router/README.md b/core/router/README.md new file mode 100644 index 0000000..0156972 --- /dev/null +++ b/core/router/README.md @@ -0,0 +1,150 @@ +# Svelte Routing + +A declarative Svelte routing library with SSR support. + +> this is the already compiled version of [svelte-routing](https://github.com/EmilTholin/svelte-routing) + +## Getting started + +this package is built in snel just import it using: + +```javascript +import { ... } from "@core:router"; +``` + +## Usage + +```html + + + + +
+ + + + +
+
+``` + +## API + +#### `Router` + +The `Router` component supplies the `Link` and `Route` descendant components with routing information through context, so you need at least one `Router` at the top of your application. It assigns a score to all its `Route` descendants and picks the best match to render. + +`Router` components can also be nested to allow for seamless merging of many smaller apps. + +###### Properties + +| Property | Required | Default Value | Description | +| :--------: | :------: | :-----------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `basepath` | | `'/'` | The `basepath` property will be added to all the `to` properties of `Link` descendants and to all `path` properties of `Route` descendants. This property can be ignored in most cases, but if you host your application on e.g. `https://example.com/my-site`, the `basepath` should be set to `/my-site`. | +| `url` | | `''` | The `url` property is used in SSR to force the current URL of the application and will be used by all `Link` and `Route` descendants. A falsy value will be ignored by the `Router`, so it's enough to declare `export let url = '';` for your topmost component and only give it a value in SSR. | + +#### `Link` + +A component used to navigate around the application. + +###### Properties + +| Property | Required | Default Value | Description | +| :--------: | :------: | :-----------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `to` | ✔ ️ | `'#'` | URL the component should link to. | +| `replace` | | `false` | When `true`, clicking the `Link` will replace the current entry in the history stack instead of adding a new one. | +| `state` | | `{}` | An object that will be pushed to the history stack when the `Link` is clicked. | +| `getProps` | | `() => ({})` | A function that returns an object that will be spread on the underlying anchor element's attributes. The first argument given to the function is an object with the properties `location`, `href`, `isPartiallyCurrent`, `isCurrent`. Look at the [`NavLink` component in the example project setup][example-folder-navlink] to see how you can build your own link components with this. | + +#### `Route` + +A component that will render its `component` property or children when its ancestor `Router` component decides it is the best match. + +All properties other than `path` and `component` given to the `Route` will be passed to the rendered `component`. + +Potential path parameters will be passed to the rendered `component` as properties. A wildcard `*` can be given a name with `*wildcardName` to pass the wildcard string as the `wildcardName` property instead of as the `*` property. + +Potential path parameters are passed back to the parent using props, so they can be exposed to the slot template using `let:params`. + +```html + + + +``` + +###### Properties + +| Property | Required | Default Value | Description | +| :---------: | :------: | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `path` | | `''` | The path for when this component should be rendered. If no `path` is given the `Route` will act as the default that matches if no other `Route` in the `Router` matches. | +| `component` | | `null` | The component constructor that will be used for rendering when the `Route` matches. If `component` is not set, the children of `Route` will be rendered instead. | + +#### `navigate` + +A function that allows you to imperatively navigate around the application for those use cases where a `Link` component is not suitable, e.g. after submitting a form. + +The first argument is a string denoting where to navigate to, and the second argument is an object with a `replace` and `state` property equivalent to those in the `Link` component. + +```html + +``` + +#### `link` + +An action used on anchor tags to navigate around the application. You can add an attribute `replace` to replace the current entry in the history stack instead of adding a new one. + +```html + + + + Home + Replace this URL + + +``` + +#### `links` + +An action used on a root element to make all relative anchor elements navigate around the application. You can add an attribute `replace` on any anchor to replace the current entry in the history stack instead of adding a new one. You can add an attribute `noroute` for this action to skip over the anchor and allow it to use the native browser action. + +```html + + + +``` + +## SSR Caveat + +In the browser we wait until all child `Route` components have registered with their ancestor `Router` component before we let the `Router` pick the best match. This approach is not possible on the server, because when all `Route` components have registered and it is time to pick a match the SSR has already completed, and a document with no matching route will be returned. + +We therefore resort to picking the first matching `Route` that is registered on the server, so it is of utmost importance that you `sort your Route components from the most specific to the least specific if you are using SSR`. diff --git a/core/router/Route.js b/core/router/Route.js new file mode 100644 index 0000000..16cc4f4 --- /dev/null +++ b/core/router/Route.js @@ -0,0 +1,534 @@ +/* Route.svelte generated by Svelte v3.31.2 */ +import { + SvelteComponentDev, + assign, + check_outros, + component_subscribe, + create_component, + create_slot, + destroy_component, + detach_dev, + dispatch_dev, + empty, + exclude_internal_props, + get_spread_object, + get_spread_update, + group_outros, + init, + insert_dev, + mount_component, + safe_not_equal, + transition_in, + transition_out, + update_slot, + validate_slots, + validate_store, +} from "https://cdn.skypack.dev/svelte@3.31.2/internal"; + +import { getContext, onDestroy } from "https://cdn.skypack.dev/svelte@3.31.2/"; +import { ROUTER, LOCATION } from "./contexts.js"; +const file = "Route.svelte"; + +const get_default_slot_changes = (dirty) => ({ + params: dirty & /*routeParams*/ 4, + location: dirty & /*$location*/ 16, +}); + +const get_default_slot_context = (ctx) => ({ + params: /*routeParams*/ ctx[2], + location: /*$location*/ ctx[4], +}); + +// (40:0) {#if $activeRoute !== null && $activeRoute.route === route} +function create_if_block(ctx) { + let current_block_type_index; + let if_block; + let if_block_anchor; + let current; + const if_block_creators = [create_if_block_1, create_else_block]; + const if_blocks = []; + + function select_block_type(ctx, dirty) { + if (/*component*/ ctx[0] !== null) return 0; + return 1; + } + + current_block_type_index = select_block_type(ctx, -1); + if_block = if_blocks[current_block_type_index] = if_block_creators[ + current_block_type_index + ](ctx); + + const block = { + c: function create() { + if_block.c(); + if_block_anchor = empty(); + }, + m: function mount(target, anchor) { + if_blocks[current_block_type_index].m(target, anchor); + insert_dev(target, if_block_anchor, anchor); + current = true; + }, + p: function update(ctx, dirty) { + let previous_block_index = current_block_type_index; + current_block_type_index = select_block_type(ctx, dirty); + + if (current_block_type_index === previous_block_index) { + if_blocks[current_block_type_index].p(ctx, dirty); + } else { + group_outros(); + + transition_out(if_blocks[previous_block_index], 1, 1, () => { + if_blocks[previous_block_index] = null; + }); + + check_outros(); + if_block = if_blocks[current_block_type_index]; + + if (!if_block) { + if_block = if_blocks[current_block_type_index] = if_block_creators[ + current_block_type_index + ](ctx); + if_block.c(); + } else { + if_block.p(ctx, dirty); + } + + transition_in(if_block, 1); + if_block.m(if_block_anchor.parentNode, if_block_anchor); + } + }, + i: function intro(local) { + if (current) return; + transition_in(if_block); + current = true; + }, + o: function outro(local) { + transition_out(if_block); + current = false; + }, + d: function destroy(detaching) { + if_blocks[current_block_type_index].d(detaching); + if (detaching) detach_dev(if_block_anchor); + }, + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_if_block.name, + type: "if", + source: + "(40:0) {#if $activeRoute !== null && $activeRoute.route === route}", + ctx, + }); + + return block; +} + +// (48:2) {:else} +function create_else_block(ctx) { + let current; + const default_slot_template = /*#slots*/ ctx[10].default; + const default_slot = create_slot( + default_slot_template, + ctx, + /*$$scope*/ ctx[9], + get_default_slot_context + ); + + const block = { + c: function create() { + if (default_slot) default_slot.c(); + }, + m: function mount(target, anchor) { + if (default_slot) { + default_slot.m(target, anchor); + } + + current = true; + }, + p: function update(ctx, dirty) { + if (default_slot) { + if (default_slot.p && dirty & /*$$scope, routeParams, $location*/ 532) { + update_slot( + default_slot, + default_slot_template, + ctx, + /*$$scope*/ ctx[9], + dirty, + get_default_slot_changes, + get_default_slot_context + ); + } + } + }, + i: function intro(local) { + if (current) return; + transition_in(default_slot, local); + current = true; + }, + o: function outro(local) { + transition_out(default_slot, local); + current = false; + }, + d: function destroy(detaching) { + if (default_slot) default_slot.d(detaching); + }, + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_else_block.name, + type: "else", + source: "(48:2) {:else}", + ctx, + }); + + return block; +} + +// (41:2) {#if component !== null} +function create_if_block_1(ctx) { + let switch_instance; + let switch_instance_anchor; + let current; + + const switch_instance_spread_levels = [ + { location: /*$location*/ ctx[4] }, + /*routeParams*/ ctx[2], + /*routeProps*/ ctx[3], + ]; + + var switch_value = /*component*/ ctx[0]; + + function switch_props(ctx) { + let switch_instance_props = {}; + + for (let i = 0; i < switch_instance_spread_levels.length; i += 1) { + switch_instance_props = assign( + switch_instance_props, + switch_instance_spread_levels[i] + ); + } + + return { + props: switch_instance_props, + $$inline: true, + }; + } + + if (switch_value) { + switch_instance = new switch_value(switch_props(ctx)); + } + + const block = { + c: function create() { + if (switch_instance) create_component(switch_instance.$$.fragment); + switch_instance_anchor = empty(); + }, + m: function mount(target, anchor) { + if (switch_instance) { + mount_component(switch_instance, target, anchor); + } + + insert_dev(target, switch_instance_anchor, anchor); + current = true; + }, + p: function update(ctx, dirty) { + const switch_instance_changes = + dirty & /*$location, routeParams, routeProps*/ 28 + ? get_spread_update(switch_instance_spread_levels, [ + dirty & /*$location*/ 16 && { location: /*$location*/ ctx[4] }, + dirty & /*routeParams*/ 4 && + get_spread_object(/*routeParams*/ ctx[2]), + dirty & /*routeProps*/ 8 && + get_spread_object(/*routeProps*/ ctx[3]), + ]) + : {}; + + if (switch_value !== (switch_value = /*component*/ ctx[0])) { + if (switch_instance) { + group_outros(); + const old_component = switch_instance; + + transition_out(old_component.$$.fragment, 1, 0, () => { + destroy_component(old_component, 1); + }); + + check_outros(); + } + + if (switch_value) { + switch_instance = new switch_value(switch_props(ctx)); + create_component(switch_instance.$$.fragment); + transition_in(switch_instance.$$.fragment, 1); + mount_component( + switch_instance, + switch_instance_anchor.parentNode, + switch_instance_anchor + ); + } else { + switch_instance = null; + } + } else if (switch_value) { + switch_instance.$set(switch_instance_changes); + } + }, + i: function intro(local) { + if (current) return; + if (switch_instance) transition_in(switch_instance.$$.fragment, local); + current = true; + }, + o: function outro(local) { + if (switch_instance) transition_out(switch_instance.$$.fragment, local); + current = false; + }, + d: function destroy(detaching) { + if (detaching) detach_dev(switch_instance_anchor); + if (switch_instance) destroy_component(switch_instance, detaching); + }, + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_if_block_1.name, + type: "if", + source: "(41:2) {#if component !== null}", + ctx, + }); + + return block; +} + +function create_fragment(ctx) { + let if_block_anchor; + let current; + let if_block = + /*$activeRoute*/ ctx[1] !== null && + /*$activeRoute*/ ctx[1].route === /*route*/ ctx[7] && + create_if_block(ctx); + + const block = { + c: function create() { + if (if_block) if_block.c(); + if_block_anchor = empty(); + }, + l: function claim(nodes) { + throw new Error( + "options.hydrate only works if the component was compiled with the `hydratable: true` option" + ); + }, + m: function mount(target, anchor) { + if (if_block) if_block.m(target, anchor); + insert_dev(target, if_block_anchor, anchor); + current = true; + }, + p: function update(ctx, [dirty]) { + if ( + /*$activeRoute*/ ctx[1] !== null && + /*$activeRoute*/ ctx[1].route === /*route*/ ctx[7] + ) { + if (if_block) { + if_block.p(ctx, dirty); + + if (dirty & /*$activeRoute*/ 2) { + transition_in(if_block, 1); + } + } else { + if_block = create_if_block(ctx); + if_block.c(); + transition_in(if_block, 1); + if_block.m(if_block_anchor.parentNode, if_block_anchor); + } + } else if (if_block) { + group_outros(); + + transition_out(if_block, 1, 1, () => { + if_block = null; + }); + + check_outros(); + } + }, + i: function intro(local) { + if (current) return; + transition_in(if_block); + current = true; + }, + o: function outro(local) { + transition_out(if_block); + current = false; + }, + d: function destroy(detaching) { + if (if_block) if_block.d(detaching); + if (detaching) detach_dev(if_block_anchor); + }, + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_fragment.name, + type: "component", + source: "", + ctx, + }); + + return block; +} + +function instance($$self, $$props, $$invalidate) { + let $activeRoute; + let $location; + let { $$slots: slots = {}, $$scope } = $$props; + validate_slots("Route", slots, ["default"]); + let { path = "" } = $$props; + let { component = null } = $$props; + const { registerRoute, unregisterRoute, activeRoute } = getContext(ROUTER); + validate_store(activeRoute, "activeRoute"); + component_subscribe($$self, activeRoute, (value) => + $$invalidate(1, ($activeRoute = value)) + ); + const location = getContext(LOCATION); + validate_store(location, "location"); + component_subscribe($$self, location, (value) => + $$invalidate(4, ($location = value)) + ); + + const route = { + path, + // If no path prop is given, this Route will act as the default Route + // that is rendered if no other Route in the Router is a match. + default: path === "", + }; + + let routeParams = {}; + let routeProps = {}; + registerRoute(route); + + // There is no need to unregister Routes in SSR since it will all be + // thrown away anyway. + if (typeof window !== "undefined") { + onDestroy(() => { + unregisterRoute(route); + }); + } + + $$self.$$set = ($$new_props) => { + $$invalidate( + 13, + ($$props = assign( + assign({}, $$props), + exclude_internal_props($$new_props) + )) + ); + if ("path" in $$new_props) $$invalidate(8, (path = $$new_props.path)); + if ("component" in $$new_props) + $$invalidate(0, (component = $$new_props.component)); + if ("$$scope" in $$new_props) + $$invalidate(9, ($$scope = $$new_props.$$scope)); + }; + + $$self.$capture_state = () => ({ + getContext, + onDestroy, + ROUTER, + LOCATION, + path, + component, + registerRoute, + unregisterRoute, + activeRoute, + location, + route, + routeParams, + routeProps, + $activeRoute, + $location, + }); + + $$self.$inject_state = ($$new_props) => { + $$invalidate(13, ($$props = assign(assign({}, $$props), $$new_props))); + if ("path" in $$props) $$invalidate(8, (path = $$new_props.path)); + if ("component" in $$props) + $$invalidate(0, (component = $$new_props.component)); + if ("routeParams" in $$props) + $$invalidate(2, (routeParams = $$new_props.routeParams)); + if ("routeProps" in $$props) + $$invalidate(3, (routeProps = $$new_props.routeProps)); + }; + + if ($$props && "$$inject" in $$props) { + $$self.$inject_state($$props.$$inject); + } + + $$self.$$.update = () => { + if ($$self.$$.dirty & /*$activeRoute*/ 2) { + $: if ($activeRoute && $activeRoute.route === route) { + $$invalidate(2, (routeParams = $activeRoute.params)); + } + } + + $: { + const { path, component, ...rest } = $$props; + $$invalidate(3, (routeProps = rest)); + } + }; + + $$props = exclude_internal_props($$props); + + return [ + component, + $activeRoute, + routeParams, + routeProps, + $location, + activeRoute, + location, + route, + path, + $$scope, + slots, + ]; +} + +class Route extends SvelteComponentDev { + constructor(options) { + super(options); + init(this, options, instance, create_fragment, safe_not_equal, { + path: 8, + component: 0, + }); + + dispatch_dev("SvelteRegisterComponent", { + component: this, + tagName: "Route", + options, + id: create_fragment.name, + }); + } + + get path() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set path(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } + + get component() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set component(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } +} + +export default Route; diff --git a/core/router/Router.js b/core/router/Router.js new file mode 100644 index 0000000..798c01f --- /dev/null +++ b/core/router/Router.js @@ -0,0 +1,349 @@ +/* Router.svelte generated by Svelte v3.31.2 */ +import { + SvelteComponentDev, + component_subscribe, + create_slot, + dispatch_dev, + init, + safe_not_equal, + transition_in, + transition_out, + update_slot, + validate_slots, + validate_store, +} from "https://cdn.skypack.dev/svelte@3.31.2/internal"; + +import { + getContext, + setContext, + onMount, +} from "https://cdn.skypack.dev/svelte@3.31.2/"; + +import { writable, derived } from "https://cdn.skypack.dev/svelte@3.31.2/store"; +import { LOCATION, ROUTER } from "./contexts.js"; +import { globalHistory } from "./history.js"; +import { pick, match, stripSlashes, combinePaths } from "./utils.js"; +const file = "Router.svelte"; + +function create_fragment(ctx) { + let current; + const default_slot_template = /*#slots*/ ctx[9].default; + const default_slot = create_slot( + default_slot_template, + ctx, + /*$$scope*/ ctx[8], + null + ); + + const block = { + c: function create() { + if (default_slot) default_slot.c(); + }, + l: function claim(nodes) { + throw new Error( + "options.hydrate only works if the component was compiled with the `hydratable: true` option" + ); + }, + m: function mount(target, anchor) { + if (default_slot) { + default_slot.m(target, anchor); + } + + current = true; + }, + p: function update(ctx, [dirty]) { + if (default_slot) { + if (default_slot.p && dirty & /*$$scope*/ 256) { + update_slot( + default_slot, + default_slot_template, + ctx, + /*$$scope*/ ctx[8], + dirty, + null, + null + ); + } + } + }, + i: function intro(local) { + if (current) return; + transition_in(default_slot, local); + current = true; + }, + o: function outro(local) { + transition_out(default_slot, local); + current = false; + }, + d: function destroy(detaching) { + if (default_slot) default_slot.d(detaching); + }, + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_fragment.name, + type: "component", + source: "", + ctx, + }); + + return block; +} + +function instance($$self, $$props, $$invalidate) { + let $base; + let $location; + let $routes; + let { $$slots: slots = {}, $$scope } = $$props; + validate_slots("Router", slots, ["default"]); + let { basepath = "/" } = $$props; + let { url = null } = $$props; + const locationContext = getContext(LOCATION); + const routerContext = getContext(ROUTER); + const routes = writable([]); + validate_store(routes, "routes"); + component_subscribe($$self, routes, (value) => + $$invalidate(7, ($routes = value)) + ); + const activeRoute = writable(null); + let hasActiveRoute = false; // Used in SSR to synchronously set that a Route is active. + + // If locationContext is not set, this is the topmost Router in the tree. + // If the `url` prop is given we force the location to it. + const location = + locationContext || + writable(url ? { pathname: url } : globalHistory.location); + + validate_store(location, "location"); + component_subscribe($$self, location, (value) => + $$invalidate(6, ($location = value)) + ); + + // If routerContext is set, the routerBase of the parent Router + // will be the base for this Router's descendants. + // If routerContext is not set, the path and resolved uri will both + // have the value of the basepath prop. + const base = routerContext + ? routerContext.routerBase + : writable({ path: basepath, uri: basepath }); + + validate_store(base, "base"); + component_subscribe($$self, base, (value) => + $$invalidate(5, ($base = value)) + ); + + const routerBase = derived([base, activeRoute], ([base, activeRoute]) => { + // If there is no activeRoute, the routerBase will be identical to the base. + if (activeRoute === null) { + return base; + } + + const { path: basepath } = base; + const { route, uri } = activeRoute; + + // Remove the potential /* or /*splatname from + // the end of the child Routes relative paths. + const path = route.default ? basepath : route.path.replace(/\*.*$/, ""); + + return { path, uri }; + }); + + function registerRoute(route) { + const { path: basepath } = $base; + let { path } = route; + + // We store the original path in the _path property so we can reuse + // it when the basepath changes. The only thing that matters is that + // the route reference is intact, so mutation is fine. + route._path = path; + + route.path = combinePaths(basepath, path); + + if (typeof window === "undefined") { + // In SSR we should set the activeRoute immediately if it is a match. + // If there are more Routes being registered after a match is found, + // we just skip them. + if (hasActiveRoute) { + return; + } + + const matchingRoute = match(route, $location.pathname); + + if (matchingRoute) { + activeRoute.set(matchingRoute); + hasActiveRoute = true; + } + } else { + routes.update((rs) => { + rs.push(route); + return rs; + }); + } + } + + function unregisterRoute(route) { + routes.update((rs) => { + const index = rs.indexOf(route); + rs.splice(index, 1); + return rs; + }); + } + + if (!locationContext) { + // The topmost Router in the tree is responsible for updating + // the location store and supplying it through context. + onMount(() => { + const unlisten = globalHistory.listen((history) => { + location.set(history.location); + }); + + return unlisten; + }); + + setContext(LOCATION, location); + } + + setContext(ROUTER, { + activeRoute, + base, + routerBase, + registerRoute, + unregisterRoute, + }); + + const writable_props = ["basepath", "url"]; + + Object.keys($$props).forEach((key) => { + if (!~writable_props.indexOf(key) && key.slice(0, 2) !== "$$") + console.warn(` was created with unknown prop '${key}'`); + }); + + $$self.$$set = ($$props) => { + if ("basepath" in $$props) $$invalidate(3, (basepath = $$props.basepath)); + if ("url" in $$props) $$invalidate(4, (url = $$props.url)); + if ("$$scope" in $$props) $$invalidate(8, ($$scope = $$props.$$scope)); + }; + + $$self.$capture_state = () => ({ + getContext, + setContext, + onMount, + writable, + derived, + LOCATION, + ROUTER, + globalHistory, + pick, + match, + stripSlashes, + combinePaths, + basepath, + url, + locationContext, + routerContext, + routes, + activeRoute, + hasActiveRoute, + location, + base, + routerBase, + registerRoute, + unregisterRoute, + $base, + $location, + $routes, + }); + + $$self.$inject_state = ($$props) => { + if ("basepath" in $$props) $$invalidate(3, (basepath = $$props.basepath)); + if ("url" in $$props) $$invalidate(4, (url = $$props.url)); + if ("hasActiveRoute" in $$props) hasActiveRoute = $$props.hasActiveRoute; + }; + + if ($$props && "$$inject" in $$props) { + $$self.$inject_state($$props.$$inject); + } + + $$self.$$.update = () => { + if ($$self.$$.dirty & /*$base*/ 32) { + // This reactive statement will update all the Routes' path when + // the basepath changes. + $: { + const { path: basepath } = $base; + + routes.update((rs) => { + rs.forEach((r) => (r.path = combinePaths(basepath, r._path))); + return rs; + }); + } + } + + if ($$self.$$.dirty & /*$routes, $location*/ 192) { + // This reactive statement will be run when the Router is created + // when there are no Routes and then again the following tick, so it + // will not find an active Route in SSR and in the browser it will only + // pick an active Route after all Routes have been registered. + $: { + const bestMatch = pick($routes, $location.pathname); + activeRoute.set(bestMatch); + } + } + }; + + return [ + routes, + location, + base, + basepath, + url, + $base, + $location, + $routes, + $$scope, + slots, + ]; +} + +class Router extends SvelteComponentDev { + constructor(options) { + super(options); + init(this, options, instance, create_fragment, safe_not_equal, { + basepath: 3, + url: 4, + }); + + dispatch_dev("SvelteRegisterComponent", { + component: this, + tagName: "Router", + options, + id: create_fragment.name, + }); + } + + get basepath() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set basepath(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } + + get url() { + throw new Error( + ": Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''" + ); + } + + set url(value) { + throw new Error( + ": Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''" + ); + } +} + +export default Router; diff --git a/core/router/actions.js b/core/router/actions.js new file mode 100644 index 0000000..ad33a5c --- /dev/null +++ b/core/router/actions.js @@ -0,0 +1,83 @@ +import { navigate } from "./history.js"; +import { shouldNavigate, hostMatches } from "./utils.js"; +/** + * A link action that can be added to tags rather + * than using the component. + * + * Example: + * ```html + * {post.title} + * ``` + */ +function link(node) { + function onClick(event) { + const anchor = event.currentTarget; + + if (anchor.target === "" && hostMatches(anchor) && shouldNavigate(event)) { + event.preventDefault(); + navigate(anchor.pathname + anchor.search, { + replace: anchor.hasAttribute("replace"), + }); + } + } + + node.addEventListener("click", onClick); + + return { + destroy() { + node.removeEventListener("click", onClick); + }, + }; +} +/** + * An action to be added at a root element of your application to + * capture all relative links and push them onto the history stack. + * + * Example: + * ```html + *
+ * + * + * + * {#each projects as project} + * {project.title} + * {/each} + * + *
+ * ``` + */ +function links(node) { + function findClosest(tagName, el) { + while (el && el.tagName !== tagName) { + el = el.parentNode; + } + return el; + } + + function onClick(event) { + const anchor = findClosest("A", event.target); + + if ( + anchor && + anchor.target === "" && + hostMatches(anchor) && + shouldNavigate(event) && + !anchor.hasAttribute("noroute") + ) { + event.preventDefault(); + navigate(anchor.pathname + anchor.search, { + replace: anchor.hasAttribute("replace"), + }); + } + } + + node.addEventListener("click", onClick); + + return { + destroy() { + node.removeEventListener("click", onClick); + }, + }; +} + +export { link, links }; diff --git a/core/router/contexts.js b/core/router/contexts.js new file mode 100644 index 0000000..ebbe78c --- /dev/null +++ b/core/router/contexts.js @@ -0,0 +1,2 @@ +export const LOCATION = {}; +export const ROUTER = {}; diff --git a/core/router/history.js b/core/router/history.js new file mode 100644 index 0000000..3eccc2a --- /dev/null +++ b/core/router/history.js @@ -0,0 +1,108 @@ +/** + * Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/history.js + * + * https://github.com/reach/router/blob/master/LICENSE + * */ + +function getLocation(source) { + return { + ...source.location, + state: source.history.state, + key: (source.history.state && source.history.state.key) || "initial", + }; +} + +function createHistory(source, options) { + const listeners = []; + let location = getLocation(source); + + return { + get location() { + return location; + }, + + listen(listener) { + listeners.push(listener); + + const popstateListener = () => { + location = getLocation(source); + listener({ location, action: "POP" }); + }; + + source.addEventListener("popstate", popstateListener); + + return () => { + source.removeEventListener("popstate", popstateListener); + + const index = listeners.indexOf(listener); + listeners.splice(index, 1); + }; + }, + + navigate(to, { state, replace = false } = {}) { + state = { ...state, key: Date.now() + "" }; + // try...catch iOS Safari limits to 100 pushState calls + try { + if (replace) { + source.history.replaceState(state, null, to); + } else { + source.history.pushState(state, null, to); + } + } catch (e) { + source.location[replace ? "replace" : "assign"](to); + } + + location = getLocation(source); + listeners.forEach((listener) => listener({ location, action: "PUSH" })); + }, + }; +} + +// Stores history entries in memory for testing or other platforms like Native +function createMemorySource(initialPathname = "/") { + let index = 0; + const stack = [{ pathname: initialPathname, search: "" }]; + const states = []; + + return { + get location() { + return stack[index]; + }, + addEventListener(name, fn) {}, + removeEventListener(name, fn) {}, + history: { + get entries() { + return stack; + }, + get index() { + return index; + }, + get state() { + return states[index]; + }, + pushState(state, _, uri) { + const [pathname, search = ""] = uri.split("?"); + index++; + stack.push({ pathname, search }); + states.push(state); + }, + replaceState(state, _, uri) { + const [pathname, search = ""] = uri.split("?"); + stack[index] = { pathname, search }; + states[index] = state; + }, + }, + }; +} + +// Global history uses window.history as the source if available, +// otherwise a memory history +const canUseDOM = Boolean( + typeof window !== "undefined" && + window.document && + window.document.createElement +); +const globalHistory = createHistory(canUseDOM ? window : createMemorySource()); +const { navigate } = globalHistory; + +export { globalHistory, navigate, createHistory, createMemorySource }; diff --git a/core/router/mod.js b/core/router/mod.js new file mode 100644 index 0000000..b0644a3 --- /dev/null +++ b/core/router/mod.js @@ -0,0 +1,5 @@ +export { default as Router } from "./Router.js"; +export { default as Route } from "./Route.js"; +export { default as Link } from "./Link.js"; +export { link, links } from "./actions.js"; +export { navigate } from "./history.js"; diff --git a/core/router/utils.js b/core/router/utils.js new file mode 100644 index 0000000..f5dfb97 --- /dev/null +++ b/core/router/utils.js @@ -0,0 +1,338 @@ +/** + * Adapted from https://github.com/reach/router/blob/b60e6dd781d5d3a4bdaaf4de665649c0f6a7e78d/src/lib/utils.js + * + * https://github.com/reach/router/blob/master/LICENSE + * + */ + +const paramRe = /^:(.+)/; + +const SEGMENT_POINTS = 4; +const STATIC_POINTS = 3; +const DYNAMIC_POINTS = 2; +const SPLAT_PENALTY = 1; +const ROOT_POINTS = 1; +/** + * Check if `string` starts with `search` + * @param {string} string + * @param {string} search + * @return {boolean} + */ +export function startsWith(string, search) { + return string.substr(0, search.length) === search; +} +/** + * Check if `segment` is a root segment + * @param {string} segment + * @return {boolean} + */ +function isRootSegment(segment) { + return segment === ""; +} +/** + * Check if `segment` is a dynamic segment + * @param {string} segment + * @return {boolean} + */ +function isDynamic(segment) { + return paramRe.test(segment); +} +/** + * Check if `segment` is a splat + * @param {string} segment + * @return {boolean} + */ +function isSplat(segment) { + return segment[0] === "*"; +} +/** + * Split up the URI into segments delimited by `/` + * @param {string} uri + * @return {string[]} + */ +function segmentize(uri) { + return ( + uri + // Strip starting/ending `/` + .replace(/(^\/+|\/+$)/g, "") + .split("/") + ); +} +/** + * Strip `str` of potential start and end `/` + * @param {string} str + * @return {string} + */ +function stripSlashes(str) { + return str.replace(/(^\/+|\/+$)/g, ""); +} +/** + * Score a route depending on how its individual segments look + * @param {object} route + * @param {number} index + * @return {object} + */ +function rankRoute(route, index) { + const score = route.default + ? 0 + : segmentize(route.path).reduce((score, segment) => { + score += SEGMENT_POINTS; + + if (isRootSegment(segment)) { + score += ROOT_POINTS; + } else if (isDynamic(segment)) { + score += DYNAMIC_POINTS; + } else if (isSplat(segment)) { + score -= SEGMENT_POINTS + SPLAT_PENALTY; + } else { + score += STATIC_POINTS; + } + + return score; + }, 0); + + return { route, score, index }; +} +/** + * Give a score to all routes and sort them on that + * @param {object[]} routes + * @return {object[]} + */ +function rankRoutes(routes) { + return ( + routes + .map(rankRoute) + // If two routes have the exact same score, we go by index instead + .sort((a, b) => + a.score < b.score ? 1 : a.score > b.score ? -1 : a.index - b.index + ) + ); +} +/** + * Ranks and picks the best route to match. Each segment gets the highest + * amount of points, then the type of segment gets an additional amount of + * points where + * + * static > dynamic > splat > root + * + * This way we don't have to worry about the order of our routes, let the + * computers do it. + * + * A route looks like this + * + * { path, default, value } + * + * And a returned match looks like: + * + * { route, params, uri } + * + * @param {object[]} routes + * @param {string} uri + * @return {?object} + */ +function pick(routes, uri) { + let match; + let default_; + + const [uriPathname] = uri.split("?"); + const uriSegments = segmentize(uriPathname); + const isRootUri = uriSegments[0] === ""; + const ranked = rankRoutes(routes); + + for (let i = 0, l = ranked.length; i < l; i++) { + const route = ranked[i].route; + let missed = false; + + if (route.default) { + default_ = { + route, + params: {}, + uri, + }; + continue; + } + + const routeSegments = segmentize(route.path); + const params = {}; + const max = Math.max(uriSegments.length, routeSegments.length); + let index = 0; + + for (; index < max; index++) { + const routeSegment = routeSegments[index]; + const uriSegment = uriSegments[index]; + + if (routeSegment !== undefined && isSplat(routeSegment)) { + // Hit a splat, just grab the rest, and return a match + // uri: /files/documents/work + // route: /files/* or /files/*splatname + const splatName = routeSegment === "*" ? "*" : routeSegment.slice(1); + + params[splatName] = uriSegments + .slice(index) + .map(decodeURIComponent) + .join("/"); + break; + } + + if (uriSegment === undefined) { + // URI is shorter than the route, no match + // uri: /users + // route: /users/:userId + missed = true; + break; + } + + let dynamicMatch = paramRe.exec(routeSegment); + + if (dynamicMatch && !isRootUri) { + const value = decodeURIComponent(uriSegment); + params[dynamicMatch[1]] = value; + } else if (routeSegment !== uriSegment) { + // Current segments don't match, not dynamic, not splat, so no match + // uri: /users/123/settings + // route: /users/:id/profile + missed = true; + break; + } + } + + if (!missed) { + match = { + route, + params, + uri: "/" + uriSegments.slice(0, index).join("/"), + }; + break; + } + } + + return match || default_ || null; +} +/** + * Check if the `path` matches the `uri`. + * @param {string} path + * @param {string} uri + * @return {?object} + */ +function match(route, uri) { + return pick([route], uri); +} +/** + * Add the query to the pathname if a query is given + * @param {string} pathname + * @param {string} [query] + * @return {string} + */ +function addQuery(pathname, query) { + return pathname + (query ? `?${query}` : ""); +} +/** + * Resolve URIs as though every path is a directory, no files. Relative URIs + * in the browser can feel awkward because not only can you be "in a directory", + * you can be "at a file", too. For example: + * + * browserSpecResolve('foo', '/bar/') => /bar/foo + * browserSpecResolve('foo', '/bar') => /foo + * + * But on the command line of a file system, it's not as complicated. You can't + * `cd` from a file, only directories. This way, links have to know less about + * their current path. To go deeper you can do this: + * + * + * // instead of + * + * + * Just like `cd`, if you want to go deeper from the command line, you do this: + * + * cd deeper + * # not + * cd $(pwd)/deeper + * + * By treating every path as a directory, linking to relative paths should + * require less contextual information and (fingers crossed) be more intuitive. + * @param {string} to + * @param {string} base + * @return {string} + */ +function resolve(to, base) { + // /foo/bar, /baz/qux => /foo/bar + if (startsWith(to, "/")) { + return to; + } + + const [toPathname, toQuery] = to.split("?"); + const [basePathname] = base.split("?"); + const toSegments = segmentize(toPathname); + const baseSegments = segmentize(basePathname); + + // ?a=b, /users?b=c => /users?a=b + if (toSegments[0] === "") { + return addQuery(basePathname, toQuery); + } + + // profile, /users/789 => /users/789/profile + if (!startsWith(toSegments[0], ".")) { + const pathname = baseSegments.concat(toSegments).join("/"); + + return addQuery((basePathname === "/" ? "" : "/") + pathname, toQuery); + } + + // ./ , /users/123 => /users/123 + // ../ , /users/123 => /users + // ../.. , /users/123 => / + // ../../one, /a/b/c/d => /a/b/one + // .././one , /a/b/c/d => /a/b/c/one + const allSegments = baseSegments.concat(toSegments); + const segments = []; + + allSegments.forEach((segment) => { + if (segment === "..") { + segments.pop(); + } else if (segment !== ".") { + segments.push(segment); + } + }); + + return addQuery("/" + segments.join("/"), toQuery); +} +/** + * Combines the `basepath` and the `path` into one path. + * @param {string} basepath + * @param {string} path + */ +function combinePaths(basepath, path) { + return `${stripSlashes( + path === "/" ? basepath : `${stripSlashes(basepath)}/${stripSlashes(path)}` + )}/`; +} +/** + * Decides whether a given `event` should result in a navigation or not. + * @param {object} event + */ +function shouldNavigate(event) { + return ( + !event.defaultPrevented && + event.button === 0 && + !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) + ); +} + +function hostMatches(anchor) { + const host = location.host; + return ( + anchor.host == host || + // svelte seems to kill anchor.host value in ie11, so fall back to checking href + anchor.href.indexOf(`https://${host}`) === 0 || + anchor.href.indexOf(`http://${host}`) === 0 + ); +} + +export { + stripSlashes, + pick, + match, + resolve, + combinePaths, + shouldNavigate, + hostMatches, +};