@manyducks.co/dolla 2.0.0-alpha.57 → 2.0.0-alpha.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/core/app.d.ts +17 -0
  2. package/dist/core/context.d.ts +71 -19
  3. package/dist/core/index.d.ts +3 -3
  4. package/dist/core/logger.d.ts +2 -2
  5. package/dist/core/markup.d.ts +14 -13
  6. package/dist/core/mount.d.ts +2 -2
  7. package/dist/core/nodes/dom.d.ts +3 -2
  8. package/dist/core/nodes/dynamic.d.ts +2 -1
  9. package/dist/core/nodes/html.d.ts +7 -5
  10. package/dist/core/nodes/portal.d.ts +4 -3
  11. package/dist/core/nodes/repeat.d.ts +5 -3
  12. package/dist/core/nodes/view.d.ts +6 -8
  13. package/dist/core/signals.d.ts +3 -23
  14. package/dist/core/views/default-crash-view.d.ts +12 -1
  15. package/dist/i18n.js +1 -1
  16. package/dist/index.js +87 -88
  17. package/dist/index.js.map +1 -1
  18. package/dist/jsx-dev-runtime.d.ts +2 -1
  19. package/dist/jsx-dev-runtime.js +4 -11
  20. package/dist/jsx-dev-runtime.js.map +1 -1
  21. package/dist/jsx-runtime.d.ts +3 -2
  22. package/dist/jsx-runtime.js +7 -13
  23. package/dist/jsx-runtime.js.map +1 -1
  24. package/dist/logger-Ck_9LCem.js +516 -0
  25. package/dist/logger-Ck_9LCem.js.map +1 -0
  26. package/dist/markup-BBa4WBj1.js +1019 -0
  27. package/dist/markup-BBa4WBj1.js.map +1 -0
  28. package/dist/{router-ClSFnmRK.js → router-CaR7Xd4T.js} +77 -76
  29. package/dist/router-CaR7Xd4T.js.map +1 -0
  30. package/dist/router.js +1 -1
  31. package/dist/types.d.ts +7 -0
  32. package/docs/markup.md +4 -4
  33. package/notes/observable.md +180 -0
  34. package/package.json +2 -2
  35. package/dist/logger-sSxIw5od.js +0 -626
  36. package/dist/logger-sSxIw5od.js.map +0 -1
  37. package/dist/markup-CAJd0zdA.js +0 -937
  38. package/dist/markup-CAJd0zdA.js.map +0 -1
  39. package/dist/router-ClSFnmRK.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-CaR7Xd4T.js","sources":["../src/router/router.utils.ts","../src/router/router.ts"],"sourcesContent":["import { assertString, assertArrayOf, isFunction } from \"../typeChecking.js\";\n\nexport type RouteMatch<T = Record<string, any>> = {\n /**\n * The path string that triggered this match.\n */\n path: string;\n\n /**\n * The pattern satisfied by `path`.\n */\n pattern: string;\n\n /**\n * Named params as parsed from `path`.\n */\n params: Record<string, string>;\n\n /**\n * Query params as parsed from `path`.\n */\n query: Record<string, string>;\n\n /**\n * Metadata registered to this route.\n */\n meta: T;\n};\n\nexport enum FragTypes {\n Literal = 1,\n Param = 2,\n Wildcard = 3,\n NumericParam = 4,\n}\n\nexport type RouteFragment = {\n name: string;\n type: FragTypes;\n value: string | number | null;\n};\n\nexport type ParsedRoute<T> = {\n pattern: string;\n fragments: RouteFragment[];\n meta: T;\n};\n\nexport type RouteMatchOptions<T> = {\n willMatch?: (route: ParsedRoute<T>) => boolean;\n};\n\n/**\n * Separates a URL path into multiple fragments.\n *\n * @param path - A path string (e.g. `\"/api/users/5\"`)\n * @returns an array of fragments (e.g. `[\"api\", \"users\", \"5\"]`)\n */\nexport function splitPath(path: string): string[] {\n assertString(path, \"Expected `path` to be a string. Got type: %t, value: %v\");\n\n return path\n .split(\"/\")\n .map((f) => f.trim())\n .filter((f) => f !== \"\");\n}\n\n/**\n * Joins multiple URL path fragments into a single string.\n *\n * @param parts - One or more URL fragments (e.g. `[\"api\", \"users\", 5]`)\n * @returns a joined path (e.g. `\"api/users/5\"`)\n */\nexport function joinPath(parts: { toString(): string }[]): string {\n assertArrayOf(\n (part) => isFunction(part?.toString),\n parts,\n \"Expected `parts` to be an array of objects with a .toString() method. Got type: %t, value: %v\",\n );\n\n parts = parts.filter((x) => x).flatMap(String);\n\n let joined = parts.shift()?.toString();\n\n if (joined) {\n for (const part of parts.map((p) => p.toString())) {\n if (part.startsWith(\".\")) {\n // Resolve relative path against joined\n joined = resolvePath(joined, part);\n } else if (joined[joined.length - 1] !== \"/\") {\n if (part[0] !== \"/\") {\n joined += \"/\" + part;\n } else {\n joined += part;\n }\n } else {\n if (part[0] === \"/\") {\n joined += part.slice(1);\n } else {\n joined += part;\n }\n }\n }\n\n // Remove trailing slash (unless path is just '/')\n if (joined && joined !== \"/\" && joined.endsWith(\"/\")) {\n joined = joined.slice(0, joined.length - 1);\n }\n }\n\n return joined ?? \"\";\n}\n\nexport function resolvePath(base: string, part: string | null) {\n assertString(base, \"Expected `base` to be a string. Got type: %t, value: %v\");\n\n if (part == null) {\n part = base;\n base = \"\";\n }\n\n if (part.startsWith(\"/\")) {\n return part;\n }\n\n let resolved = base;\n\n while (true) {\n if (part.startsWith(\"..\")) {\n for (let i = resolved.length; i > 0; --i) {\n if (resolved[i] === \"/\" || i === 0) {\n resolved = resolved.slice(0, i);\n part = part.replace(/^\\.\\.\\/?/, \"\");\n break;\n }\n }\n } else if (part.startsWith(\".\")) {\n part = part.replace(/^\\.\\/?/, \"\");\n } else {\n break;\n }\n }\n\n return joinPath([resolved, part]);\n}\n\nexport function parseQueryParams(query: string): Record<string, string> {\n if (!query) return {};\n\n if (query.startsWith(\"?\")) {\n query = query.slice(1);\n }\n\n const entries = query\n .split(\"&\")\n .filter((x) => x.trim() !== \"\")\n .map((entry) =>\n entry\n .split(\"=\")\n .map((x) => x.trim())\n .slice(0, 2),\n );\n\n return Object.fromEntries(entries);\n}\n\n/**\n * Returns the nearest match, or undefined if the path matches no route.\n *\n * @param url - Path to match against routes.\n * @param options - Options to customize how matching operates.\n */\nexport function matchRoutes<T>(\n routes: ParsedRoute<T>[],\n url: string,\n options: RouteMatchOptions<T> = {},\n): RouteMatch<T> | undefined {\n const [path, query] = url.split(\"?\");\n const parts = splitPath(path);\n\n routes: for (const route of routes) {\n const { fragments } = route;\n const hasWildcard = fragments[fragments.length - 1]?.type === FragTypes.Wildcard;\n\n if (!hasWildcard && fragments.length !== parts.length) {\n continue routes;\n }\n\n if (options.willMatch && !options.willMatch(route)) {\n continue routes;\n }\n\n const matched: RouteFragment[] = [];\n\n fragments: for (let i = 0; i < fragments.length; i++) {\n const part = parts[i];\n const frag = fragments[i];\n\n if (part == null && frag.type !== FragTypes.Wildcard) {\n continue routes;\n }\n\n switch (frag.type) {\n case FragTypes.Literal:\n if (frag.name.toLowerCase() === part.toLowerCase()) {\n matched.push(frag);\n break;\n } else {\n continue routes;\n }\n case FragTypes.Param:\n matched.push({ ...frag, value: part });\n break;\n case FragTypes.Wildcard:\n matched.push({ ...frag, value: parts.slice(i).join(\"/\") });\n break fragments;\n case FragTypes.NumericParam:\n if (!isNaN(Number(part))) {\n matched.push({ ...frag, value: part });\n break;\n } else {\n continue routes;\n }\n default:\n throw new Error(`Unknown fragment type: ${frag.type}`);\n }\n }\n\n const params: Record<string, string> = {};\n\n for (const frag of matched) {\n if (frag.type === FragTypes.Param) {\n params[frag.name] = decodeURIComponent(frag.value as string);\n }\n\n if (frag.type === FragTypes.NumericParam) {\n params[frag.name] = String(frag.value);\n }\n\n if (frag.type === FragTypes.Wildcard) {\n params.wildcard = \"/\" + decodeURIComponent(frag.value as string);\n }\n }\n\n return {\n path: \"/\" + matched.map((f) => f.value).join(\"/\"),\n pattern:\n \"/\" +\n fragments\n .map((f) => {\n if (f.type === FragTypes.Param) {\n return `{${f.name}}`;\n }\n\n if (f.type === FragTypes.NumericParam) {\n return `{#${f.name}}`;\n }\n\n return f.name;\n })\n .join(\"/\"),\n params,\n query: parseQueryParams(query),\n meta: route.meta,\n };\n }\n}\n\n/**\n * Sort routes descending by specificity. Guarantees that the most specific route matches first\n * no matter the order in which they were added.\n *\n * Routes without named params and routes with more fragments are weighted more heavily.\n */\nexport function sortRoutes<T>(routes: ParsedRoute<T>[]): ParsedRoute<T>[] {\n const withoutParams = [];\n const withNumericParams = [];\n const withParams = [];\n const wildcard = [];\n\n for (const route of routes) {\n const { fragments } = route;\n\n if (fragments.some((f) => f.type === FragTypes.Wildcard)) {\n wildcard.push(route);\n } else if (fragments.some((f) => f.type === FragTypes.NumericParam)) {\n withNumericParams.push(route);\n } else if (fragments.some((f) => f.type === FragTypes.Param)) {\n withParams.push(route);\n } else {\n withoutParams.push(route);\n }\n }\n\n const bySizeDesc = (a: ParsedRoute<T>, b: ParsedRoute<T>) => {\n if (a.fragments.length > b.fragments.length) {\n return -1;\n } else {\n return 1;\n }\n };\n\n withoutParams.sort(bySizeDesc);\n withNumericParams.sort(bySizeDesc);\n withParams.sort(bySizeDesc);\n wildcard.sort(bySizeDesc);\n\n return [...withoutParams, ...withNumericParams, ...withParams, ...wildcard];\n}\n\n/**\n * Converts a route pattern into a set of matchable fragments.\n *\n * @param route - A route string (e.g. \"/api/users/{id}\")\n */\nexport function patternToFragments(pattern: string): RouteFragment[] {\n const parts = splitPath(pattern);\n const fragments = [];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n\n if (part === \"*\") {\n if (i !== parts.length - 1) {\n throw new Error(`Wildcard must be at the end of a pattern. Received: ${pattern}`);\n }\n fragments.push({\n type: FragTypes.Wildcard,\n name: \"*\",\n value: null,\n });\n } else if (part.at(0) === \"{\" && part.at(-1) === \"}\") {\n fragments.push({\n type: part[1] === \"#\" ? FragTypes.NumericParam : FragTypes.Param,\n name: part[1] === \"#\" ? part.slice(2, -1) : part.slice(1, -1),\n value: null,\n });\n } else {\n fragments.push({\n type: FragTypes.Literal,\n name: part,\n value: part,\n });\n }\n }\n\n return fragments;\n}\n\nconst safeExternalLink = /(noopener|noreferrer) (noopener|noreferrer)/;\nconst protocolLink = /^[\\w-_]+:/;\n\n/**\n * Intercepts links within the root node.\n *\n * This is adapted from https://github.com/choojs/nanohref/blob/master/index.js\n *\n * @param root - Element under which to intercept link clicks\n * @param callback - Function to call when a click event is intercepted\n * @param _window - (optional) Override for global window object\n */\nexport function catchLinks(root: Element, callback: (anchor: HTMLAnchorElement) => void, _window = window) {\n function traverse(node: HTMLElement | null): HTMLAnchorElement | null {\n if (!node || node === root) {\n return null;\n }\n\n if (node.localName !== \"a\" || (node as any).href === undefined) {\n return traverse(node.parentNode as HTMLElement | null);\n }\n\n return node as HTMLAnchorElement;\n }\n\n function handler(e: MouseEvent) {\n if ((e.button && e.button !== 0) || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.defaultPrevented) {\n return;\n }\n\n const anchor = traverse(e.target as HTMLElement);\n\n if (!anchor) {\n return;\n }\n\n if (\n _window.location.protocol !== anchor.protocol ||\n _window.location.hostname !== anchor.hostname ||\n _window.location.port !== anchor.port ||\n anchor.hasAttribute(\"data-router-ignore\") ||\n anchor.hasAttribute(\"download\") ||\n (anchor.getAttribute(\"target\") === \"_blank\" && safeExternalLink.test(anchor.getAttribute(\"rel\")!)) ||\n protocolLink.test(anchor.getAttribute(\"href\")!)\n ) {\n return;\n }\n\n e.preventDefault();\n callback(anchor);\n }\n\n root.addEventListener(\"click\", handler as any);\n\n return function cancel() {\n root.removeEventListener(\"click\", handler as any);\n };\n}\n\n/**\n * Replace route pattern param placeholders with real matched values.\n */\nexport function replaceParams(path: string, params: Record<string, string | number>) {\n for (const key in params) {\n const value = params[key].toString();\n path = path.replace(`{${key}}`, value).replace(`{#${key}}`, value);\n }\n\n return path;\n}\n","import { Context } from \"../core/context.js\";\nimport { createLogger } from \"../core/logger.js\";\nimport { m, type MarkupNode } from \"../core/markup.js\";\nimport { Dynamic } from \"../core/nodes/dynamic.js\";\nimport { ViewInstance } from \"../core/nodes/view.js\";\nimport { $, untracked, Source, type UnsubscribeFn } from \"../core/signals.js\";\nimport { assertObject, isArray, isFunction, isObject, isString } from \"../typeChecking.js\";\nimport type { View } from \"../types.js\";\nimport { shallowEqual } from \"../utils.js\";\nimport {\n catchLinks,\n joinPath,\n matchRoutes,\n patternToFragments,\n replaceParams,\n resolvePath,\n sortRoutes,\n splitPath,\n type ParsedRoute,\n type RouteMatch,\n} from \"./router.utils.js\";\n\n// ----- Types ----- //\n\nexport type Stringable = { toString(): string };\n\nexport interface RouteMatchContext {\n path: string;\n pattern: string;\n params: Record<string, string>;\n query: Record<string, string>;\n\n /**\n * Redirects the user to a different route instead of matching the current one.\n */\n redirect(path: string): void;\n\n /**\n * Triggers the next beforeMatch function, or mounts the route.\n */\n next(): void;\n}\n\nexport interface Route {\n /**\n * The path or path fragment to match.\n */\n path: string;\n\n /**\n * Path to redirect to when this route is matched, or a callback function that returns such path.\n */\n redirect?: string | ((ctx: RouteRedirectContext) => string) | ((ctx: RouteRedirectContext) => Promise<string>);\n\n /**\n * View to display when this route is matched.\n */\n view?: View<any>;\n\n /**\n * Subroutes.\n */\n routes?: Route[];\n\n /**\n * Called after the match is identified but before it is acted on. Use this to set state, load data, etc.\n */\n beforeMatch?: (ctx: RouteMatchContext) => void | Promise<void>;\n}\n\nexport interface RouteMeta {\n redirect?: string | ((ctx: RouteRedirectContext) => string) | ((ctx: RouteRedirectContext) => Promise<string>);\n pattern?: string;\n layers?: RouteLayer[];\n beforeMatch?: ((ctx: RouteMatchContext) => void | Promise<void>)[];\n}\n\nexport interface RouteConfig {\n pattern: string;\n meta: RouteMeta;\n}\n\nexport interface RouteLayer {\n id: number;\n view: View<{}>;\n}\n\n/**\n * An active route layer whose markup has been initialized into a view.\n */\ninterface ActiveLayer {\n id: number;\n element: MarkupNode;\n context: Context;\n $slot: Source<MarkupNode | undefined>;\n}\n\n/**\n * Object passed to redirect callbacks. Contains information useful for determining how to redirect.\n */\nexport interface RouteRedirectContext {\n /**\n * The path as it appears in the URL bar.\n */\n path: string;\n\n /**\n * The pattern that this path was matched with.\n */\n pattern: string;\n\n /**\n * Named route params parsed from `path`.\n */\n params: Record<string, string>;\n\n /**\n * Query params parsed from `path`.\n */\n query: Record<string, string>;\n}\n\n/**\n * A log for a single step in the route resolution process.\n */\ninterface JourneyStep {\n kind: \"match\" | \"redirect\" | \"miss\";\n message: string;\n}\n\nexport interface NavigateOptions {\n /**\n * Replace the current item in the history stack instead of adding a new one.\n * The back button will send the user to the page they visited before this. Defaults to false.\n */\n replace?: boolean;\n\n /**\n * Preserve existing query params (if any) when navigating. Defaults to false.\n * If true, all existing query params are preserved and merged with new ones.\n * If an array of strings is passed only those keys will be preserved, then merged with any new ones.\n */\n preserveQuery?: boolean | string[];\n}\n\nexport interface RouterOptions {\n routes: Route[];\n\n /**\n * When true, the router will construct routes like \"https://www.example.com/#/sub/route\" which work without any backend intervention.\n */\n hash?: boolean;\n}\n\n// ----- Code ----- //\n\nexport const MOUNT = Symbol();\nexport const UNMOUNT = Symbol();\nexport const ROOT_VIEW = Symbol();\n\nexport class Router {\n #logger = createLogger(\"dolla.router\");\n\n #layerId = 0;\n #activeLayers: ActiveLayer[] = [];\n #routes: ParsedRoute<RouteMeta>[] = [];\n\n #isMounted = false;\n\n #rootLayer!: ActiveLayer;\n\n /**\n * Use hash routing when true. Configured in router options.\n */\n #hash = false;\n\n // Callbacks that need to be called on unmount.\n #unsubscribers: UnsubscribeFn[] = [];\n\n /**\n * The current match object.\n */\n #match = $<RouteMatch>();\n\n /**\n * The currently matched route pattern, if any.\n */\n readonly $pattern = $(() => this.#match()?.pattern);\n\n /**\n * The current URL path.\n */\n readonly $path = $(() => this.#match()?.path ?? window.location.pathname);\n\n /**\n * The current named path params.\n */\n readonly $params = $(() => this.#match()?.params ?? {}, { equals: shallowEqual });\n\n /**\n * The current query params.\n */\n readonly $query = $(() => this.#match()?.query ?? {}, { equals: shallowEqual });\n\n constructor(options: RouterOptions) {\n assertObject(options, \"Options must be an object. Got: %t\");\n\n if (options.hash) {\n this.#hash = true;\n }\n\n // Add routes.\n this.#routes = sortRoutes(\n options.routes\n .flatMap((route) => this.#prepareRoute(route))\n .map((route) => ({\n pattern: route.pattern,\n meta: route.meta,\n fragments: patternToFragments(route.pattern),\n })),\n );\n\n assertValidRedirects(this.#routes);\n }\n\n async [MOUNT](parent: Element, context: Context): Promise<MarkupNode> {\n const $slot = $<MarkupNode>();\n this.#rootLayer = {\n id: -1,\n element: new Dynamic(context, $slot),\n context,\n $slot,\n };\n\n // Listen for popstate events and update route accordingly.\n const onPopState = () => {\n this.#updateRoute(undefined, {});\n };\n window.addEventListener(\"popstate\", onPopState);\n this.#unsubscribers.push(() => window.removeEventListener(\"popstate\", onPopState));\n\n // Listen for clicks on <a> tags within the app.\n this.#unsubscribers.push(\n catchLinks(parent, (anchor) => {\n let href = anchor.getAttribute(\"href\")!;\n this.#logger.info(\"intercepted click on <a> tag\", anchor);\n\n const preserve = anchor.getAttribute(\"data-router-preserve-query\");\n\n this.go(href, {\n preserveQuery: parsePreserveQueryAttribute(preserve),\n });\n }),\n );\n this.#logger.info(\"will intercept clicks on <a> tags within root element\", parent);\n\n this.#isMounted = true;\n\n // Setup initial route content.\n await this.#updateRoute(undefined, {});\n\n return this.#rootLayer.element;\n }\n\n async [UNMOUNT]() {\n for (const callback of this.#unsubscribers) {\n callback();\n }\n this.#unsubscribers = [];\n }\n\n /**\n * Navigate backward. Pass a number of steps to hit the back button that many times.\n */\n back(steps = 1) {\n window.history.go(-steps);\n }\n\n /**\n * Navigate forward. Pass a number of steps to hit the forward button that many times.\n */\n forward(steps = 1) {\n window.history.go(steps);\n }\n\n /**\n * Navigates to another route.\n *\n * @example\n * router.go(\"/login\"); // navigate to `/login`\n * router.go[\"/users\", 215], { replace: true }); // replace current history entry with `/users/215`\n */\n go(path: Stringable | Stringable[], options: NavigateOptions = {}) {\n let joined: string;\n\n if (Array.isArray(path)) {\n joined = joinPath(path);\n } else {\n joined = path.toString();\n }\n\n joined = resolvePath(window.location.pathname, joined);\n\n if (options.replace) {\n this.#replace(joined, options);\n } else {\n this.#push(joined, options);\n }\n }\n\n /**\n * Updates query params, keeping existing ones and applying new ones. Removes the query param if value is set to `null`.\n */\n updateQuery(values: Record<string, Stringable | null>) {\n const match = untracked(this.#match)!;\n const query = { ...this.$query() };\n\n for (const key in values) {\n const value = values[key];\n if (value === null) {\n delete query[key];\n } else {\n query[key] = value.toString();\n }\n }\n\n let queryParts: string[] = [];\n\n for (const key in query) {\n queryParts.push(`${key}=${query[key]}`);\n }\n const queryString = queryParts.length > 0 ? \"?\" + queryParts.join(\"&\") : \"\";\n\n this.#match({ ...match, query });\n\n window.history.replaceState(null, \"\", this.#hash ? \"/#\" + match.path + queryString : match.path + queryString);\n }\n\n #push(href: string, options: NavigateOptions) {\n this.#logger.info(\"(push)\", href);\n\n window.history.pushState(null, \"\", this.#hash ? \"/#\" + href : href);\n this.#updateRoute(href, options);\n }\n\n #replace(href: string, options: NavigateOptions) {\n this.#logger.info(\"(replace)\", href);\n\n window.history.replaceState(null, \"\", this.#hash ? \"/#\" + href : href);\n this.#updateRoute(href, options);\n }\n\n #getCurrentURL(): URL {\n if (this.#hash) {\n return new URL(window.location.hash.slice(1), window.location.origin);\n } else {\n return new URL(window.location.pathname, window.location.origin);\n }\n }\n\n /**\n * Run when the location changes. Diffs and mounts new routes and updates\n * the $path, $route, $params and $query states accordingly.\n */\n async #updateRoute(href: string | undefined, options: NavigateOptions) {\n const logger = this.#logger;\n const url = href ? new URL(href, window.location.origin) : this.#getCurrentURL();\n\n const { match, journey } = await this.#resolveRoute(url);\n\n for (const step of journey) {\n switch (step.kind) {\n case \"match\":\n logger?.info(`📍 ${step.message}`);\n break;\n case \"redirect\":\n logger?.info(`â†Šī¸ ${step.message}`);\n break;\n case \"miss\":\n logger?.info(`💀 ${step.message}`);\n break;\n default:\n break;\n }\n }\n\n if (match) {\n const oldPattern = untracked(this.$pattern);\n\n // Merge query params.\n let query = match.query;\n let queryParts: string[] = [];\n\n if (options.preserveQuery === true) {\n query = Object.assign({}, this.$query(), match.query);\n } else if (isArray(options.preserveQuery)) {\n const preserved: Record<string, any> = {};\n const current = this.$query();\n for (const key in current) {\n if (options.preserveQuery.includes(key)) {\n preserved[key] = current[key];\n }\n }\n query = Object.assign({}, preserved, match.query);\n }\n\n for (const key in query) {\n queryParts.push(`${key}=${query[key]}`);\n }\n const queryString = queryParts.length > 0 ? \"?\" + queryParts.join(\"&\") : \"\";\n\n this.#match({ ...match, query });\n\n // Update the URL if matched path differs from navigator path.\n // This happens if route resolution involved redirects.\n if (match.path !== location.pathname || location.search !== queryString) {\n window.history.replaceState(null, \"\", this.#hash ? \"/#\" + match.path + queryString : match.path + queryString);\n }\n\n if (match.pattern !== oldPattern) {\n this.#mountRoute(match);\n }\n } else {\n // Only crash if routing has been configured.\n if (this.#isMounted) {\n logger.crash(new NoRouteError(`Failed to match route '${url.pathname}'`));\n }\n }\n\n return { match, journey };\n }\n\n /**\n * Takes a matched route and mounts it.\n */\n #mountRoute(match: RouteMatch<RouteMeta>) {\n const layers = match.meta.layers!;\n\n this.#logger.info(\"mounting\", match);\n\n // Diff and update route layers.\n for (let i = 0; i < layers.length; i++) {\n const matchedLayer = layers[i];\n const activeLayer = this.#activeLayers[i];\n\n if (activeLayer?.id !== matchedLayer.id) {\n // Discard all previously active layers starting at this depth.\n this.#activeLayers = this.#activeLayers.slice(0, i);\n activeLayer?.element.unmount();\n\n const parentLayer = this.#activeLayers.at(-1) ?? this.#rootLayer;\n\n // Create a $slot and element for this layer.\n const $slot = $<MarkupNode>();\n const element = new ViewInstance(parentLayer.context, matchedLayer.view, {\n children: m(\"$dynamic\", { source: $slot }),\n });\n\n // Add new layer to activeLayers.\n this.#activeLayers.push({\n id: matchedLayer.id,\n element,\n context: element.context,\n $slot,\n });\n\n // Slot this layer into parent $slot.\n parentLayer.$slot(element);\n }\n }\n }\n\n /**\n * Takes a URL and finds a match, following redirects.\n */\n async #resolveRoute(\n url: URL,\n journey: JourneyStep[] = [],\n ): Promise<{\n match: RouteMatch<RouteMeta> | null;\n journey: JourneyStep[];\n }> {\n return new Promise((resolve, reject) => {\n const match = matchRoutes(this.#routes, url.pathname);\n\n if (!match) {\n return resolve({\n match: null,\n journey: [...journey, { kind: \"miss\", message: `no match for '${url.pathname}'` }],\n });\n }\n\n let redirect = match.meta.redirect;\n\n const finalize = async () => {\n if (redirect != null) {\n let path: string;\n\n if (isString(redirect)) {\n path = replaceParams(redirect, match.params);\n } else if (isFunction(redirect)) {\n const redirectContext: RouteRedirectContext = {\n path: match.path,\n pattern: match.pattern,\n params: match.params,\n query: match.query,\n };\n path = await redirect(redirectContext);\n if (!isString(path)) {\n return reject(new Error(`Redirect function must return a path to redirect to.`));\n }\n if (!path.startsWith(\"/\")) {\n // Not absolute. Resolve against matched path.\n path = resolvePath(match.path, path);\n }\n } else {\n return reject(new TypeError(`Redirect must either be a path string or a function.`));\n }\n\n resolve(\n this.#resolveRoute(new URL(path, window.location.origin), [\n ...journey,\n { kind: \"redirect\", message: `redirecting '${match.path}' -> '${path}'` },\n ]),\n );\n } else {\n resolve({ match, journey: [...journey, { kind: \"match\", message: `matched route '${match.path}'` }] });\n }\n };\n\n if (match.meta.beforeMatch?.length) {\n const callbacks = match.meta.beforeMatch;\n let i = -1;\n const next = () => {\n i++;\n if (i === callbacks.length) {\n // Mount route\n finalize();\n } else {\n // Next callback\n callbacks[i]({\n path: match.path,\n pattern: match.pattern,\n params: match.params,\n query: match.query,\n\n // TODO: Allow setting context variables from here? Would apply to the context of the matched view.\n redirect: (path) => {\n redirect = path;\n finalize();\n },\n\n next,\n });\n }\n };\n\n next();\n\n // TODO: Show warning after timeout if next hasn't been called?\n } else {\n finalize();\n }\n });\n }\n\n /**\n * Parses a route definition object into a set of matchable routes.\n *\n * @param route - Route config object.\n * @param layers - Array of parent layers. Passed when this function calls itself on nested routes.\n */\n #prepareRoute(route: Route, parents: Route[] = [], layers: RouteLayer[] = []) {\n if (!isObject(route) || !isString(route.path)) {\n throw new TypeError(`Route configs must be objects with a 'path' string property. Got: ${route}`);\n }\n\n if (route.redirect && route.routes) {\n throw new Error(`Route cannot have both a 'redirect' and nested 'routes'.`);\n } else if (route.redirect && route.view) {\n throw new Error(`Route cannot have both a 'redirect' and a 'view'.`);\n } else if (!route.view && !route.routes && !route.redirect) {\n throw new Error(`Route must have a 'view', a 'redirect', or a set of nested 'routes'.`);\n }\n\n let parts: string[] = [];\n\n for (const parent of parents) {\n parts.push(...splitPath(parent.path));\n }\n\n parts.push(...splitPath(route.path));\n\n // Remove trailing wildcard for joining with nested routes.\n if (parts[parts.length - 1] === \"*\") {\n parts.pop();\n }\n\n const routes: RouteConfig[] = [];\n\n if (route.redirect) {\n let redirect = route.redirect;\n\n if (isString(redirect)) {\n redirect = resolvePath(joinPath(parts), redirect);\n\n if (!redirect.startsWith(\"/\")) {\n redirect = \"/\" + redirect;\n }\n }\n\n routes.push({\n pattern: \"/\" + joinPath([...parts, ...splitPath(route.path)]),\n meta: {\n redirect,\n },\n });\n\n return routes;\n }\n\n let view: View<any> = (props: any) => props.children;\n\n if (isFunction(route.view)) {\n view = route.view;\n } else if (route.view) {\n throw new TypeError(`Route '${route.path}' expected a view function or undefined. Got: ${route.view}`);\n }\n\n const layer: RouteLayer = { id: this.#layerId++, view };\n\n // Parse nested routes if they exist.\n if (route.routes) {\n for (const subroute of route.routes) {\n routes.push(...this.#prepareRoute(subroute, [...parents, route], [...layers, layer]));\n }\n } else {\n routes.push({\n pattern: parent ? joinPath([...parents.map((p) => p.path), route.path]) : route.path,\n meta: {\n pattern: route.path,\n layers: [...layers, layer],\n beforeMatch: parents\n .flatMap((parent) => parent.beforeMatch)\n .concat(route.beforeMatch)\n .filter((x) => x != null),\n },\n });\n }\n\n return routes;\n }\n}\n\nfunction assertValidRedirects(routes: ParsedRoute<RouteMeta>[]) {\n // Test redirects to make sure all possible redirect targets actually exist.\n for (const route of routes) {\n if (route.meta.redirect) {\n let redirectPath: string;\n\n if (isFunction(route.meta.redirect)) {\n // throw new Error(`Redirect functions are not yet supported.`);\n // Just allow, though it could fail later. Best not to call the function and cause potential side effects.\n } else if (isString(route.meta.redirect)) {\n redirectPath = route.meta.redirect;\n\n const match = matchRoutes(routes, redirectPath, {\n willMatch(r) {\n return r !== route;\n },\n });\n\n if (!match) {\n throw new Error(`Found a redirect to an undefined URL. From '${route.pattern}' to '${route.meta.redirect}'`);\n }\n } else {\n throw new TypeError(`Expected a string or redirect function. Got: ${route.meta.redirect}`);\n }\n }\n }\n}\n\n/**\n * Parses the data-router-preserve-query attribute from a link.\n */\nfunction parsePreserveQueryAttribute(value: null | string | boolean): boolean | string[] {\n if (value === null) {\n return false;\n } else if (value === true || value === false) {\n return value;\n } else if (typeof value === \"string\") {\n value = value.trim();\n if (value === \"\" || value === \"true\") {\n return true;\n } else if (value === \"false\") {\n return false;\n }\n\n return value\n .split(\",\")\n .map((k) => k.trim())\n .filter((k) => k.length > 0);\n } else {\n throw new Error(`Invalid type for data-router-preserve-query attribute: ${typeof value} (value: ${value})`);\n }\n}\n\nclass NoRouteError extends Error {}\n"],"names":["splitPath","path","assertString","f","joinPath","parts","assertArrayOf","part","isFunction","x","joined","_a","p","resolvePath","base","resolved","i","parseQueryParams","query","entries","entry","matchRoutes","routes","url","options","route","fragments","matched","frag","params","sortRoutes","withoutParams","withNumericParams","withParams","wildcard","bySizeDesc","b","patternToFragments","pattern","safeExternalLink","protocolLink","catchLinks","root","callback","_window","traverse","node","handler","e","anchor","replaceParams","key","value","MOUNT","UNMOUNT","Router","__privateAdd","_Router_instances","_logger","createLogger","_layerId","_activeLayers","_routes","_isMounted","_rootLayer","_hash","_unsubscribers","_match","$","__publicField","__privateGet","shallowEqual","assertObject","__privateSet","__privateMethod","prepareRoute_fn","assertValidRedirects","parent","context","$slot","Dynamic","onPopState","updateRoute_fn","href","preserve","parsePreserveQueryAttribute","steps","replace_fn","push_fn","values","match","untracked","queryParts","queryString","getCurrentURL_fn","logger","journey","resolveRoute_fn","step","oldPattern","isArray","preserved","current","mountRoute_fn","NoRouteError","layers","matchedLayer","activeLayer","parentLayer","element","ViewInstance","m","resolve","reject","redirect","finalize","isString","redirectContext","callbacks","next","parents","isObject","view","props","layer","__privateWrapper","subroute","redirectPath","r","k"],"mappings":"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"}
package/dist/router.js CHANGED
@@ -1,4 +1,4 @@
1
- import { R as e } from "./router-ClSFnmRK.js";
1
+ import { R as e } from "./router-CaR7Xd4T.js";
2
2
  function o(r) {
3
3
  return new e(r);
4
4
  }
package/dist/types.d.ts CHANGED
@@ -9,6 +9,9 @@ export type Env = "production" | "development";
9
9
  * These are all the items considered valid to pass as children to any element.
10
10
  */
11
11
  export type Renderable = string | number | Node | Markup | MarkupNode | false | null | undefined | Signal<any> | (string | number | Node | Markup | MarkupNode | false | null | undefined | Signal<any>)[];
12
+ export interface BaseProps {
13
+ children?: Renderable;
14
+ }
12
15
  /**
13
16
  *
14
17
  */
@@ -102,6 +105,10 @@ export interface ElementProps {
102
105
  * `NOTE` This property does no sanitization. If it's in the string, it's in the DOM. Be mindful when handling user-generated content.
103
106
  */
104
107
  innerHTML?: OptionalProperty<string>;
108
+ /**
109
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/popover
110
+ */
111
+ popover?: OptionalProperty<"auto" | "hint" | "manual" | true | false>;
105
112
  /**
106
113
  * Specifies the element's [WAI-ARIA role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles).
107
114
  *
package/docs/markup.md CHANGED
@@ -3,12 +3,12 @@
3
3
  Dolla creates a tree of views that manage the DOM, updating attributes and recreating parts of the DOM as signal values change.
4
4
 
5
5
  ```js
6
- import { $, markup, render } from "@manyducks.co/dolla";
6
+ import { $, m, render } from "@manyducks.co/dolla";
7
7
 
8
- const count = $(0);
9
- const labelMarkup = markup("span", {}, [count]);
8
+ const $count = $(0);
9
+ const labelMarkup = m("span", { children: $count });
10
10
  // or in JSX:
11
- const labelMarkup = <span>{count}</span>;
11
+ const labelMarkup = <span>{$count}</span>;
12
12
 
13
13
  const rendered = render(labelMarkup);
14
14
 
@@ -0,0 +1,180 @@
1
+ # Observable
2
+
3
+ Signals have some downsides, like if you call them inside a function, and you then call that function inside a tracking context, it can cause the tracking context to re-run unexpectedly. You then have to defensively call things inside `untracked` to avoid tracking deeply nested signal getters. It's not unmanageable but it's extremely surprising and unintuitive when you first run into it. It's a new and unnecessary consideration that makes code feel less safe and predictable.
4
+
5
+ I'm considering using Observable (or my own extended version of it) as a basis for a state management system. Still built on top of `alien-signals` but with explicit tracking of signal values. It could look something like the following.
6
+
7
+ This would probably be best as a separate library, maybe called `@manyducks.co/atomic`.
8
+
9
+ Going back to the atom/compose terminology.
10
+
11
+ ```js
12
+ // Define an atom, the basic value holder object.
13
+ const count = atom(5);
14
+
15
+ // Atoms have a `value` field that is writable. This is not tracked by default.
16
+ count.value; // 5
17
+ count.value = 12;
18
+ count.value; // 12
19
+
20
+ // Implements the Observable interface.
21
+ const subscription = count.subscribe({
22
+ next: (value) => {
23
+ console.log("count is now", value);
24
+ },
25
+ error: (error) => {},
26
+ completed: () => {},
27
+ });
28
+ subscription.closed; // boolean
29
+ subscription.unsubscribe();
30
+
31
+ // Like `value` getter but tracks count in a signal tracking scope.
32
+ count.track(); // 12
33
+
34
+ // Can be closed which completes all subscribers and will throw an error if a new value is set.
35
+ count.close();
36
+ ```
37
+
38
+ Atoms can be composed.
39
+
40
+ ```js
41
+ // You explicitly pass a dependencies array at the end, similar to React.
42
+ // Dependencies will be tracked and the compose function re-run any time they receive a new value.
43
+ const doubled = composed((prev) => count.value * 2, [count]);
44
+
45
+ // Read-only value.
46
+ doubled.value;
47
+
48
+ // Observable
49
+ const subscription = doubled.subscribe((value) => {
50
+ // ...
51
+ });
52
+
53
+ // Trackable
54
+ doubled.track();
55
+
56
+ // Completes subscriptions, untracks deps and prevents receiving any new values.
57
+ doubled.close();
58
+ ```
59
+
60
+ Effects work basically the same as `composed` but they return a cancel function instead of a value.
61
+
62
+ ```js
63
+ const cancel = effect(() => {
64
+ console.log(`count is now ${count.value}`);
65
+ }, [count]);
66
+
67
+ cancel();
68
+ ```
69
+
70
+ Other thoughts:
71
+
72
+ ```js
73
+ // You can name observables for debugging purposes. If one of them throws an error it can include the name.
74
+ const count = atom(5).named("count");
75
+
76
+ // Maybe even named effects.
77
+ const cancel = effect(() => {
78
+ console.log(`count is now ${count.value}`);
79
+ }, [count]).named("countReader");
80
+
81
+ // Promise-based await next? This will resolve when count.value is set.
82
+ // If the subscription errors it rejects with that error. If the subscription completes it rejects with an error to indicate that.
83
+ const nextCount = await count.nextValue();
84
+
85
+ // Filter and signal. Wait up to 5 seconds for next even value.
86
+ const controller = new AbortController();
87
+ setTimeout(controller.abort, 5000);
88
+ const nextEven = await count.nextValue({
89
+ filter: (value) => value % 2 === 0,
90
+ signal: controller.signal,
91
+ // or timeout: 5000
92
+ });
93
+ // Resolves to null if aborted or timed out.
94
+
95
+ // Batching
96
+
97
+ const count1 = atom(5);
98
+ const count2 = atom(12);
99
+
100
+ effect(() => {
101
+ console.log(`total: ${count1.value + count2.value}`);
102
+ }, [count1, count2]);
103
+
104
+ // This causes the effect to run twice
105
+ count1.value = 50;
106
+ count2.value = 8;
107
+
108
+ // A batch suspends effects until it concludes; this runs the effect once
109
+ batch(() => {
110
+ count1.value = 50;
111
+ count2.value = 8;
112
+ });
113
+
114
+ // Deep reactivity
115
+ const data = atom(
116
+ {
117
+ users: [
118
+ { id: 1, name: "Tony" },
119
+ { id: 2, name: "Morgan" },
120
+ ],
121
+ },
122
+ { deep: true },
123
+ );
124
+
125
+ // These updates trigger effects and subscriptions.
126
+ // By default only setting `.value` directly will trigger notifications.
127
+ data.value.users[0].name = "Bon";
128
+ data.value.users.find((user) => user.id === 1).name = "Tony";
129
+
130
+ // Then in theory, if you referenced one of the values
131
+ const morgan = data.value.users.find((user) => user.id === 2);
132
+
133
+ // And passed that around and modified it that would also still be reactive to the original atom.
134
+ // I don't know if this is a good idea.
135
+ morgan.name = "AKLSJDAKSD";
136
+ ```
137
+
138
+ ## What would Dolla look like with this?
139
+
140
+ ```jsx
141
+ function CounterView() {
142
+ const count = atom(0);
143
+
144
+ const increment = () => count.value++;
145
+ const decrement = () => count.value--;
146
+
147
+ return (
148
+ <div>
149
+ Counter: {count}
150
+ <button onClick={increment}>+1</button>
151
+ <button onClick={decrement}>-1</button>
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function ExampleView(props, ctx) {
157
+ const name = atom("");
158
+
159
+ // Update local name whenever props.name changes
160
+ ctx.effect(() => {
161
+ name.value = props.name.value;
162
+ }, [props.name]);
163
+
164
+ // Update greeting whenever local name changes
165
+ const greeting = composed(() => `Hello, ${name.value}`, [name]);
166
+
167
+ return (
168
+ <div>
169
+ <span>{greeting}</span>
170
+ <input value={name} onInput={(e) => (name.value = e.target.value)} />
171
+ </div>
172
+ );
173
+ }
174
+ ```
175
+
176
+ ## TypeScript
177
+
178
+ - `Atom<T>` for the basic building block with a writable value.
179
+ - `Composed<T>` for a derived state based on other `Atom<T>` and `Composed<T>` values.
180
+ - `Atomic<T>` to encompass the basic API of both `Atom<T>` and `Composed<T>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manyducks.co/dolla",
3
- "version": "2.0.0-alpha.57",
3
+ "version": "2.0.0-alpha.59",
4
4
  "description": "Front-end components, routing and state management.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/core/index.d.ts",
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "alien-signals": "^1.0.3"
55
+ "alien-signals": "^2.0.5"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^22.12.0",