@immediately-run/sdk 0.41.0 → 0.42.0
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.
- package/dist/components/HeadingAnchor.cjs +18 -2
- package/dist/components/HeadingAnchor.cjs.map +1 -1
- package/dist/components/HeadingAnchor.js +18 -2
- package/dist/components/HeadingAnchor.js.map +1 -1
- package/dist/urlUtils.cjs +4 -2
- package/dist/urlUtils.cjs.map +1 -1
- package/dist/urlUtils.d.cts +24 -0
- package/dist/urlUtils.d.ts +24 -0
- package/dist/urlUtils.js +4 -2
- package/dist/urlUtils.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -22,16 +22,32 @@ __export(HeadingAnchor_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(HeadingAnchor_exports);
|
|
24
24
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25
|
+
var import_react = require("react");
|
|
26
|
+
var import_scrollToId = require("../scrollToId");
|
|
27
|
+
var import_TinkerableContext = require("../TinkerableContext");
|
|
28
|
+
var import_urlUtils = require("../urlUtils");
|
|
29
|
+
var import_Link = require("./Link");
|
|
25
30
|
const HeadingAnchor = ({
|
|
26
31
|
id,
|
|
27
32
|
...rest
|
|
28
33
|
}) => {
|
|
34
|
+
const ctx = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
|
|
35
|
+
const onAnchorClick = (0, import_react.useCallback)(
|
|
36
|
+
(e) => {
|
|
37
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
if (id) (0, import_scrollToId.scrollToId)(id);
|
|
40
|
+
},
|
|
41
|
+
[id]
|
|
42
|
+
);
|
|
29
43
|
if (!id) return null;
|
|
44
|
+
const href = ctx?.navigationState ? (0, import_urlUtils.constructOuterUrl)(ctx.outerHref, `#${id}`, ctx.navigationState) : `#${id}`;
|
|
30
45
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
31
|
-
|
|
46
|
+
import_Link.FragmentLink,
|
|
32
47
|
{
|
|
33
48
|
className: "ir-heading-anchor",
|
|
34
|
-
href
|
|
49
|
+
href,
|
|
50
|
+
onClick: onAnchorClick,
|
|
35
51
|
"aria-label": "Permalink to this heading",
|
|
36
52
|
...rest,
|
|
37
53
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "#" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/HeadingAnchor.tsx"],"sourcesContent":["import { ReactNode } from 'react';\n\n/**\n * Default MDX `HeadingAnchor` component — the render target for the autolink anchor\n * the transpiler's heading-slug plugin (R3-186, `MARKDOWN_SYNTAX_SPEC §15.4`)\n * prepends to every heading. That plugin sets the heading's own `id` (a text slug,\n * or a `sec-…` section id for a numbered heading, R3-211) and emits\n * `<HeadingAnchor id=\"<that id>\" />` as the heading's first child.\n *\n * It is registered in {@link DEFAULT_MDX_COMPONENTS} so a heading anchor renders\n * even in a plain-markdown repo that never calls `boot({ mdxComponents })` — the\n * MDX missing-reference guard never fires (§11.2 phantom defaults). The markup is\n * semantic and accessible — an `aria`-labelled `<a href=\"#id\">` permalink — but\n * **styling is intentionally minimal** (`§15.4` honesty note): an app supplies CSS\n * targeting the `ir-heading-anchor` class to reveal it on hover / add an icon, or\n * overrides this component via `boot({ mdxComponents: { HeadingAnchor } })` to\n * change the icon, position, or behaviour (e.g. copy-permalink-to-clipboard) of\n * every heading anchor in every content file at once (§15.4 late binding).\n *\n * `id` is the *heading's own* id, so the permalink `#id` is the same URL a reader\n * copies and (for a `sec-…` id) an agent computes from a `§`-citation with zero\n * lookup. A missing/empty `id` (defensive — the plugin always supplies one)\n * renders nothing rather than a dead `#` link.\n */\nexport const HeadingAnchor = ({\n id,\n ...rest\n}: {\n id?: string;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n if (!id) return null;\n return (\n <
|
|
1
|
+
{"version":3,"sources":["../../src/components/HeadingAnchor.tsx"],"sourcesContent":["import { ReactNode, use, useCallback } from 'react';\nimport { scrollToId } from '../scrollToId';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { constructOuterUrl } from '../urlUtils';\nimport { FragmentLink } from './Link';\n\n/**\n * Default MDX `HeadingAnchor` component — the render target for the autolink anchor\n * the transpiler's heading-slug plugin (R3-186, `MARKDOWN_SYNTAX_SPEC §15.4`)\n * prepends to every heading. That plugin sets the heading's own `id` (a text slug,\n * or a `sec-…` section id for a numbered heading, R3-211) and emits\n * `<HeadingAnchor id=\"<that id>\" />` as the heading's first child.\n *\n * It is registered in {@link DEFAULT_MDX_COMPONENTS} so a heading anchor renders\n * even in a plain-markdown repo that never calls `boot({ mdxComponents })` — the\n * MDX missing-reference guard never fires (§11.2 phantom defaults). The markup is\n * semantic and accessible — an `aria`-labelled `<a href=\"#id\">` permalink — but\n * **styling is intentionally minimal** (`§15.4` honesty note): an app supplies CSS\n * targeting the `ir-heading-anchor` class to reveal it on hover / add an icon, or\n * overrides this component via `boot({ mdxComponents: { HeadingAnchor } })` to\n * change the icon, position, or behaviour (e.g. copy-permalink-to-clipboard) of\n * every heading anchor in every content file at once (§15.4 late binding).\n *\n * `id` is the *heading's own* id, so the permalink `#id` is the same URL a reader\n * copies and (for a `sec-…` id) an agent computes from a `§`-citation with zero\n * lookup. A missing/empty `id` (defensive — the plugin always supplies one)\n * renders nothing rather than a dead `#` link.\n */\nexport const HeadingAnchor = ({\n id,\n ...rest\n}: {\n id?: string;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n // ⚠ A BARE `#id` IS THE WRONG HREF INSIDE A SANDBOXED APP, and wrong in a way that only\n // shows up when someone copies the link. The app runs in an iframe whose document URL is\n // the SANDBOX's (`https://sandbox.immediately.run/index.html?href=…`), so the browser\n // resolves `#id` against THAT — \"copy link address\" on a heading yields a\n // sandbox-internal URL that means nothing to anyone else. Reported from a real wiki\n // (2026-08-14) as exactly that URL instead of the entry's.\n //\n // Both halves have to be right, and they pull in opposite directions:\n // • the HREF must be absolute in the HOST's space, so copy-link / middle-click /\n // open-in-new-tab all produce a URL that resolves for a reader;\n // • the CLICK must NOT navigate to it — that would be a full page load to the page you\n // are already on. It scrolls, leaving the sandbox URL the host owns untouched.\n //\n // `FragmentLink` is the SDK's existing home for the second half; it runs the caller's\n // `onClick` first and stands down if it preventDefaults, which is what lets the href be\n // absolute here while the behaviour stays a scroll.\n const ctx = use(TinkerableContext);\n const onAnchorClick = useCallback(\n (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Let a modified click through: cmd/ctrl/middle-click means \"open the permalink\",\n // and that is precisely the case the absolute href exists to serve.\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;\n e.preventDefault();\n if (id) scrollToId(id);\n },\n [id]\n );\n if (!id) return null;\n // No context (a plain-markdown repo that never called `boot()`) → keep the old bare\n // fragment, which FragmentLink handles on its own.\n const href = ctx?.navigationState\n ? constructOuterUrl(ctx.outerHref, `#${id}`, ctx.navigationState)\n : `#${id}`;\n return (\n <FragmentLink\n className=\"ir-heading-anchor\"\n href={href}\n onClick={onAnchorClick}\n aria-label=\"Permalink to this heading\"\n {...rest}\n >\n <span aria-hidden=\"true\">#</span>\n </FragmentLink>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA2EM;AA3EN,mBAA4C;AAC5C,wBAA2B;AAC3B,+BAAkC;AAClC,sBAAkC;AAClC,kBAA6B;AAwBtB,MAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA,GAAG;AACL,MAE+E;AAiB7E,QAAM,UAAM,kBAAI,0CAAiB;AACjC,QAAM,oBAAgB;AAAA,IACpB,CAAC,MAA2C;AAG1C,UAAI,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAG;AACxE,QAAE,eAAe;AACjB,UAAI,GAAI,mCAAW,EAAE;AAAA,IACvB;AAAA,IACA,CAAC,EAAE;AAAA,EACL;AACA,MAAI,CAAC,GAAI,QAAO;AAGhB,QAAM,OAAO,KAAK,sBACd,mCAAkB,IAAI,WAAW,IAAI,EAAE,IAAI,IAAI,eAAe,IAC9D,IAAI,EAAE;AACV,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV;AAAA,MACA,SAAS;AAAA,MACT,cAAW;AAAA,MACV,GAAG;AAAA,MAEJ,sDAAC,UAAK,eAAY,QAAO,eAAC;AAAA;AAAA,EAC5B;AAEJ;","names":[]}
|
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
import "../chunk-VHAA22YE.js";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { use, useCallback } from "react";
|
|
4
|
+
import { scrollToId } from "../scrollToId";
|
|
5
|
+
import { TinkerableContext } from "../TinkerableContext";
|
|
6
|
+
import { constructOuterUrl } from "../urlUtils";
|
|
7
|
+
import { FragmentLink } from "./Link";
|
|
3
8
|
const HeadingAnchor = ({
|
|
4
9
|
id,
|
|
5
10
|
...rest
|
|
6
11
|
}) => {
|
|
12
|
+
const ctx = use(TinkerableContext);
|
|
13
|
+
const onAnchorClick = useCallback(
|
|
14
|
+
(e) => {
|
|
15
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
if (id) scrollToId(id);
|
|
18
|
+
},
|
|
19
|
+
[id]
|
|
20
|
+
);
|
|
7
21
|
if (!id) return null;
|
|
22
|
+
const href = ctx?.navigationState ? constructOuterUrl(ctx.outerHref, `#${id}`, ctx.navigationState) : `#${id}`;
|
|
8
23
|
return /* @__PURE__ */ jsx(
|
|
9
|
-
|
|
24
|
+
FragmentLink,
|
|
10
25
|
{
|
|
11
26
|
className: "ir-heading-anchor",
|
|
12
|
-
href
|
|
27
|
+
href,
|
|
28
|
+
onClick: onAnchorClick,
|
|
13
29
|
"aria-label": "Permalink to this heading",
|
|
14
30
|
...rest,
|
|
15
31
|
children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "#" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/HeadingAnchor.tsx"],"sourcesContent":["import { ReactNode } from 'react';\n\n/**\n * Default MDX `HeadingAnchor` component — the render target for the autolink anchor\n * the transpiler's heading-slug plugin (R3-186, `MARKDOWN_SYNTAX_SPEC §15.4`)\n * prepends to every heading. That plugin sets the heading's own `id` (a text slug,\n * or a `sec-…` section id for a numbered heading, R3-211) and emits\n * `<HeadingAnchor id=\"<that id>\" />` as the heading's first child.\n *\n * It is registered in {@link DEFAULT_MDX_COMPONENTS} so a heading anchor renders\n * even in a plain-markdown repo that never calls `boot({ mdxComponents })` — the\n * MDX missing-reference guard never fires (§11.2 phantom defaults). The markup is\n * semantic and accessible — an `aria`-labelled `<a href=\"#id\">` permalink — but\n * **styling is intentionally minimal** (`§15.4` honesty note): an app supplies CSS\n * targeting the `ir-heading-anchor` class to reveal it on hover / add an icon, or\n * overrides this component via `boot({ mdxComponents: { HeadingAnchor } })` to\n * change the icon, position, or behaviour (e.g. copy-permalink-to-clipboard) of\n * every heading anchor in every content file at once (§15.4 late binding).\n *\n * `id` is the *heading's own* id, so the permalink `#id` is the same URL a reader\n * copies and (for a `sec-…` id) an agent computes from a `§`-citation with zero\n * lookup. A missing/empty `id` (defensive — the plugin always supplies one)\n * renders nothing rather than a dead `#` link.\n */\nexport const HeadingAnchor = ({\n id,\n ...rest\n}: {\n id?: string;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n if (!id) return null;\n return (\n <
|
|
1
|
+
{"version":3,"sources":["../../src/components/HeadingAnchor.tsx"],"sourcesContent":["import { ReactNode, use, useCallback } from 'react';\nimport { scrollToId } from '../scrollToId';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { constructOuterUrl } from '../urlUtils';\nimport { FragmentLink } from './Link';\n\n/**\n * Default MDX `HeadingAnchor` component — the render target for the autolink anchor\n * the transpiler's heading-slug plugin (R3-186, `MARKDOWN_SYNTAX_SPEC §15.4`)\n * prepends to every heading. That plugin sets the heading's own `id` (a text slug,\n * or a `sec-…` section id for a numbered heading, R3-211) and emits\n * `<HeadingAnchor id=\"<that id>\" />` as the heading's first child.\n *\n * It is registered in {@link DEFAULT_MDX_COMPONENTS} so a heading anchor renders\n * even in a plain-markdown repo that never calls `boot({ mdxComponents })` — the\n * MDX missing-reference guard never fires (§11.2 phantom defaults). The markup is\n * semantic and accessible — an `aria`-labelled `<a href=\"#id\">` permalink — but\n * **styling is intentionally minimal** (`§15.4` honesty note): an app supplies CSS\n * targeting the `ir-heading-anchor` class to reveal it on hover / add an icon, or\n * overrides this component via `boot({ mdxComponents: { HeadingAnchor } })` to\n * change the icon, position, or behaviour (e.g. copy-permalink-to-clipboard) of\n * every heading anchor in every content file at once (§15.4 late binding).\n *\n * `id` is the *heading's own* id, so the permalink `#id` is the same URL a reader\n * copies and (for a `sec-…` id) an agent computes from a `§`-citation with zero\n * lookup. A missing/empty `id` (defensive — the plugin always supplies one)\n * renders nothing rather than a dead `#` link.\n */\nexport const HeadingAnchor = ({\n id,\n ...rest\n}: {\n id?: string;\n} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>): ReactNode => {\n // ⚠ A BARE `#id` IS THE WRONG HREF INSIDE A SANDBOXED APP, and wrong in a way that only\n // shows up when someone copies the link. The app runs in an iframe whose document URL is\n // the SANDBOX's (`https://sandbox.immediately.run/index.html?href=…`), so the browser\n // resolves `#id` against THAT — \"copy link address\" on a heading yields a\n // sandbox-internal URL that means nothing to anyone else. Reported from a real wiki\n // (2026-08-14) as exactly that URL instead of the entry's.\n //\n // Both halves have to be right, and they pull in opposite directions:\n // • the HREF must be absolute in the HOST's space, so copy-link / middle-click /\n // open-in-new-tab all produce a URL that resolves for a reader;\n // • the CLICK must NOT navigate to it — that would be a full page load to the page you\n // are already on. It scrolls, leaving the sandbox URL the host owns untouched.\n //\n // `FragmentLink` is the SDK's existing home for the second half; it runs the caller's\n // `onClick` first and stands down if it preventDefaults, which is what lets the href be\n // absolute here while the behaviour stays a scroll.\n const ctx = use(TinkerableContext);\n const onAnchorClick = useCallback(\n (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Let a modified click through: cmd/ctrl/middle-click means \"open the permalink\",\n // and that is precisely the case the absolute href exists to serve.\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;\n e.preventDefault();\n if (id) scrollToId(id);\n },\n [id]\n );\n if (!id) return null;\n // No context (a plain-markdown repo that never called `boot()`) → keep the old bare\n // fragment, which FragmentLink handles on its own.\n const href = ctx?.navigationState\n ? constructOuterUrl(ctx.outerHref, `#${id}`, ctx.navigationState)\n : `#${id}`;\n return (\n <FragmentLink\n className=\"ir-heading-anchor\"\n href={href}\n onClick={onAnchorClick}\n aria-label=\"Permalink to this heading\"\n {...rest}\n >\n <span aria-hidden=\"true\">#</span>\n </FragmentLink>\n );\n};\n"],"mappings":";AA2EM;AA3EN,SAAoB,KAAK,mBAAmB;AAC5C,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAwBtB,MAAM,gBAAgB,CAAC;AAAA,EAC5B;AAAA,EACA,GAAG;AACL,MAE+E;AAiB7E,QAAM,MAAM,IAAI,iBAAiB;AACjC,QAAM,gBAAgB;AAAA,IACpB,CAAC,MAA2C;AAG1C,UAAI,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAG;AACxE,QAAE,eAAe;AACjB,UAAI,GAAI,YAAW,EAAE;AAAA,IACvB;AAAA,IACA,CAAC,EAAE;AAAA,EACL;AACA,MAAI,CAAC,GAAI,QAAO;AAGhB,QAAM,OAAO,KAAK,kBACd,kBAAkB,IAAI,WAAW,IAAI,EAAE,IAAI,IAAI,eAAe,IAC9D,IAAI,EAAE;AACV,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV;AAAA,MACA,SAAS;AAAA,MACT,cAAW;AAAA,MACV,GAAG;AAAA,MAEJ,8BAAC,UAAK,eAAY,QAAO,eAAC;AAAA;AAAA,EAC5B;AAEJ;","names":[]}
|
package/dist/urlUtils.cjs
CHANGED
|
@@ -41,7 +41,7 @@ const APP_ROOT = "/app";
|
|
|
41
41
|
const underAppRoot = (repoRelativePath) => (0, import_pathUtils.joinPaths)(APP_ROOT, repoRelativePath);
|
|
42
42
|
const getOuterHostname = (outerHref) => {
|
|
43
43
|
const url = new URL(outerHref);
|
|
44
|
-
return `${url.protocol}//${url.
|
|
44
|
+
return `${url.protocol}//${url.host}`;
|
|
45
45
|
};
|
|
46
46
|
const getSearchParams = (search) => Object.fromEntries(
|
|
47
47
|
[...new URLSearchParams(search ?? window.location.search).entries()]
|
|
@@ -73,7 +73,9 @@ const maybeParseUrl = (str) => {
|
|
|
73
73
|
const isAbsolutePath = (sandboxPath) => sandboxPath.startsWith("/");
|
|
74
74
|
const repositoryPrefixURL = (outerHref, navigationState) => constructUrl(outerHref, {
|
|
75
75
|
...navigationState,
|
|
76
|
-
sandboxPath: ""
|
|
76
|
+
sandboxPath: "",
|
|
77
|
+
hash: "",
|
|
78
|
+
search: ""
|
|
77
79
|
});
|
|
78
80
|
const constructOuterUrl = (previousOuterHref, sandboxTarget, navigationState, addFilesPrefix = true) => {
|
|
79
81
|
if (isAbsolutePath(sandboxTarget)) {
|
package/dist/urlUtils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.hostname}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: ''\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uBAA0B;AAGnB,MAAM,eAAe;AAQrB,MAAM,WAAW;AAGjB,MAAM,eAAe,CAAC,yBAC3B,4BAAU,UAAU,gBAAgB;AAgB/B,MAAM,mBAAmB,CAAC,cAAqB;AACpD,QAAM,MAAM,IAAI,IAAI,SAAS;AAC7B,SAAO,GAAG,IAAI,QAAQ,KAAK,IAAI,IAAI;AACrC;AAEO,MAAM,kBAAkB,CAAC,WAA4C,OAAO;AAAA,EACjF,CAAC,GAAI,IAAI,gBAAgB,UAAU,OAAO,SAAS,MAAM,EAAE,QAAQ,CAAE;AAAC;AAUjE,MAAM,YAAY,CAAC,WAAqC;AAC7D,QAAM,IAAI,OAAO,QAAQ,GAAG;AAC5B,SAAO,MAAM,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,GAAG,CAAC,GAAG,OAAO,MAAM,IAAI,CAAC,CAAC;AAC3E;AAGO,MAAM,cAAc,CAAC,QAAgB,eAAiD;AAC3F,QAAM,gBAAgB,EAAE,GAAG,WAAW;AACtC,MAAI,CAAC,SAAS,IAAI,IAAI,OAAO,MAAM,GAAG;AACtC,MAAI,SAAS;AACX,QAAI,CAAC,MAAM,MAAM,IAAI,QAAQ,MAAM,GAAG;AACtC,QAAI,MAAM;AACR,oBAAc,cAAc;AAAA,IAC9B;AACA,kBAAc,SAAS,SAAS,SAAS;AAAA,EAC3C;AACA,gBAAc,OAAO,OAAO,OAAO;AACnC,SAAO;AACT;AAGO,MAAM,gBAAgB,CAAC,QAA4B;AACxD,MAAI;AACF,WAAO,IAAI,IAAI,GAAG;AAAA,EACpB,SAAS,GAAG;AACV,WAAO;AAAA,EACT;AACF;AAEO,MAAM,iBAAiB,CAAC,gBAAwB,YAAY,WAAW,GAAG;AAY1E,MAAM,sBAAsB,CAAC,WAAkB,oBAAqC,aAAa,WAAW;AAAA,EAC7G,GAAG;AAAA,EACH,aAAa;AAAA,EACb,MAAM;AAAA,EACN,QAAQ;AACV,CAAC;AAEE,MAAM,oBAAoB,CAAC,mBAA0B,eAAsB,iBAAkC,iBAAe,SAAgB;AACjJ,MAAI,eAAe,aAAa,GAAG;AAKjC,UAAM,CAAC,UAAU,IAAI,IAAI,UAAU,aAAa;AAChD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,QACE,GAAG;AAAA,QACH,aAAa,qBAAiB,4BAAU,cAAc,QAAQ,IAAI;AAAA,QAClE;AAAA,MACF;AAAA,IAAC;AAAA,EACL;AACA,SACE,IAAI;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,IACF;AAAA,EACF,EACA,SAAS;AACb;AAEO,MAAM,iBAAiB,CAAC,WAAkB,QAAgB,oBAAqC;AACpG,QAAM,YAAY,cAAc,MAAM;AACtC,MAAI,WAAW;AACb,WAAO,OAAO,WAAW,oBAAoB,WAAW,eAAe,CAAC;AAAA,EAC1E;AAEA,SAAO;AACT;AAcA,MAAM,gBAA+B;AAAA,EACnC,EAAE,MAAM,QAAQ,SAAS,OAAO;AAAA,EAChC,EAAE,MAAM,YAAY,SAAS,iBAAiB;AAAA,EAC9C,EAAE,MAAM,aAAa,SAAS,iBAAiB;AAAA,EAC/C,EAAE,MAAM,cAAc,SAAS,iBAAiB;AAAA,EAChD,EAAE,MAAM,OAAO,SAAS,iBAAiB;AAAA,EACzC,EAAE,MAAM,eAAe,SAAS,MAAM,WAAW,OAAK,IAAI,CAAC,IAAI,sBAAsB,KAAK;AAC5F;AAEA,MAAM,oBAAoB,IAAI;AAAA,EAC5B,MACA,cAAc;AAAA,IAAI,CAAC,EAAE,MAAM,SAAS,qBAAqB,MACvD,uBACI,UAAW,IAAI,IAAI,OAAO,QAC1B,OAAQ,IAAI,IAAI,OAAO;AAAA,EAC7B,EAAE,KAAK,EAAE,IACT;AACF;AAGO,MAAM,YAAY,CAAC,aAAgC;AACxD,QAAM,eAAe,SAAS,MAAM,iBAAiB,GAAG,UAAU,CAAC;AACnE,SAAO,cAAc,OAAO,CAAC,KAAyB,EAAE,MAAM,UAAU,MAAmB;AACzF,QAAI,QAA4B;AAChC,QAAI,QAAQ,cAAc;AACxB,cAAQ,aAAa,IAAI;AAAA,IAC3B;AACA,QAAI,CAAC,OAAO;AAEV,cAAQ;AAAA,IACV;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,UAAI,IAAI,IAAI,YAAY,UAAU,KAAK,IAAI;AAAA,IAC7C;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AACP;AAEO,MAAM,YAAY,CAAC,SAAkC;AAC1D,QAAM,YAAY,IAAI,IAAI,IAAI;AAC9B,QAAM,gBAAgB,UAAU,UAAU,QAAQ;AAClD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ,UAAU,OAAO,UAAU,CAAC;AAAA,IACpC,MAAM,UAAU,KAAK,UAAU,CAAC;AAAA,EAClC;AACF;AAEA,MAAM,mBAAmB,CAAC,MAAsB,EAAE,WAAW,GAAG,IAAI,EAAE,UAAU,CAAC,IAAI;AAE9E,MAAM,eAAe,CAAC,WAAkB,oBAA6C;AAC1F,QAAM,OAAO,cAAc,IAAI,CAAC,EAAE,KAAK,MAAM;AAC3C,QAAI,QAAQ,gBAAgB,IAAI;AAChC,WAAO,iBAAiB,SAAS,EAAE;AAAA,EACrC,CAAC,EAAE,KAAK,GAAG;AACX,QAAM,OAAO,iBAAiB,SAAS;AACvC,MAAI,MAAM,GAAG,IAAI,IAAI,IAAI;AACzB,MAAI,gBAAgB,QAAQ;AAC1B,WAAO,MAAM,gBAAgB;AAAA,EAC/B;AACA,MAAI,gBAAgB,MAAM;AACxB,WAAO,MAAM,gBAAgB;AAAA,EAC/B;AACA,SAAO;AACT;","names":[]}
|
package/dist/urlUtils.d.cts
CHANGED
|
@@ -13,6 +13,20 @@ declare const FILES_PREFIX = "/files";
|
|
|
13
13
|
declare const APP_ROOT = "/app";
|
|
14
14
|
/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */
|
|
15
15
|
declare const underAppRoot: (repoRelativePath: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* The origin every outer URL is rebuilt on — `protocol//host`, **port included**.
|
|
18
|
+
*
|
|
19
|
+
* `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default
|
|
20
|
+
* 80/443, so a production URL is byte-identical either way). Using `hostname` here was
|
|
21
|
+
* invisible on `https://immediately.run` and broke every routed link under local dev on any
|
|
22
|
+
* port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.
|
|
23
|
+
*
|
|
24
|
+
* Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is
|
|
25
|
+
* built from this, and `isInternalHref` decides by prefix-matching against it — so an
|
|
26
|
+
* absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain
|
|
27
|
+
* `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of
|
|
28
|
+
* routing: the app appeared to "reload" on an ordinary internal link.
|
|
29
|
+
*/
|
|
16
30
|
declare const getOuterHostname: (outerHref: string) => string;
|
|
17
31
|
declare const getSearchParams: (search?: string) => Record<string, string>;
|
|
18
32
|
/**
|
|
@@ -27,6 +41,16 @@ declare const splitHash: (target: string) => [string, string];
|
|
|
27
41
|
declare const parseTarget: (target: string, navigation: NavigationState) => NavigationState;
|
|
28
42
|
declare const maybeParseUrl: (str: string) => URL | null;
|
|
29
43
|
declare const isAbsolutePath: (sandboxPath: string) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to
|
|
46
|
+
* decide whether an absolute href is "this app" or somewhere else.
|
|
47
|
+
*
|
|
48
|
+
* `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,
|
|
49
|
+
* and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,
|
|
50
|
+
* the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute
|
|
51
|
+
* in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away
|
|
52
|
+
* instead of routing. A page with a fragment quietly broke its own links.
|
|
53
|
+
*/
|
|
30
54
|
declare const repositoryPrefixURL: (outerHref: string, navigationState: NavigationState) => string;
|
|
31
55
|
declare const constructOuterUrl: (previousOuterHref: string, sandboxTarget: string, navigationState: NavigationState, addFilesPrefix?: boolean) => string;
|
|
32
56
|
declare const isInternalHref: (outerHref: string, target: string, navigationState: NavigationState) => boolean;
|
package/dist/urlUtils.d.ts
CHANGED
|
@@ -13,6 +13,20 @@ declare const FILES_PREFIX = "/files";
|
|
|
13
13
|
declare const APP_ROOT = "/app";
|
|
14
14
|
/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */
|
|
15
15
|
declare const underAppRoot: (repoRelativePath: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* The origin every outer URL is rebuilt on — `protocol//host`, **port included**.
|
|
18
|
+
*
|
|
19
|
+
* `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default
|
|
20
|
+
* 80/443, so a production URL is byte-identical either way). Using `hostname` here was
|
|
21
|
+
* invisible on `https://immediately.run` and broke every routed link under local dev on any
|
|
22
|
+
* port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.
|
|
23
|
+
*
|
|
24
|
+
* Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is
|
|
25
|
+
* built from this, and `isInternalHref` decides by prefix-matching against it — so an
|
|
26
|
+
* absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain
|
|
27
|
+
* `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of
|
|
28
|
+
* routing: the app appeared to "reload" on an ordinary internal link.
|
|
29
|
+
*/
|
|
16
30
|
declare const getOuterHostname: (outerHref: string) => string;
|
|
17
31
|
declare const getSearchParams: (search?: string) => Record<string, string>;
|
|
18
32
|
/**
|
|
@@ -27,6 +41,16 @@ declare const splitHash: (target: string) => [string, string];
|
|
|
27
41
|
declare const parseTarget: (target: string, navigation: NavigationState) => NavigationState;
|
|
28
42
|
declare const maybeParseUrl: (str: string) => URL | null;
|
|
29
43
|
declare const isAbsolutePath: (sandboxPath: string) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to
|
|
46
|
+
* decide whether an absolute href is "this app" or somewhere else.
|
|
47
|
+
*
|
|
48
|
+
* `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,
|
|
49
|
+
* and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,
|
|
50
|
+
* the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute
|
|
51
|
+
* in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away
|
|
52
|
+
* instead of routing. A page with a fragment quietly broke its own links.
|
|
53
|
+
*/
|
|
30
54
|
declare const repositoryPrefixURL: (outerHref: string, navigationState: NavigationState) => string;
|
|
31
55
|
declare const constructOuterUrl: (previousOuterHref: string, sandboxTarget: string, navigationState: NavigationState, addFilesPrefix?: boolean) => string;
|
|
32
56
|
declare const isInternalHref: (outerHref: string, target: string, navigationState: NavigationState) => boolean;
|
package/dist/urlUtils.js
CHANGED
|
@@ -5,7 +5,7 @@ const APP_ROOT = "/app";
|
|
|
5
5
|
const underAppRoot = (repoRelativePath) => joinPaths(APP_ROOT, repoRelativePath);
|
|
6
6
|
const getOuterHostname = (outerHref) => {
|
|
7
7
|
const url = new URL(outerHref);
|
|
8
|
-
return `${url.protocol}//${url.
|
|
8
|
+
return `${url.protocol}//${url.host}`;
|
|
9
9
|
};
|
|
10
10
|
const getSearchParams = (search) => Object.fromEntries(
|
|
11
11
|
[...new URLSearchParams(search ?? window.location.search).entries()]
|
|
@@ -37,7 +37,9 @@ const maybeParseUrl = (str) => {
|
|
|
37
37
|
const isAbsolutePath = (sandboxPath) => sandboxPath.startsWith("/");
|
|
38
38
|
const repositoryPrefixURL = (outerHref, navigationState) => constructUrl(outerHref, {
|
|
39
39
|
...navigationState,
|
|
40
|
-
sandboxPath: ""
|
|
40
|
+
sandboxPath: "",
|
|
41
|
+
hash: "",
|
|
42
|
+
search: ""
|
|
41
43
|
});
|
|
42
44
|
const constructOuterUrl = (previousOuterHref, sandboxTarget, navigationState, addFilesPrefix = true) => {
|
|
43
45
|
if (isAbsolutePath(sandboxTarget)) {
|
package/dist/urlUtils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.hostname}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: ''\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/urlUtils.ts"],"sourcesContent":["import { joinPaths } from \"./pathUtils\";\nimport { NavigationState, PathState } from \"./TinkerableContext\";\n\nexport const FILES_PREFIX = '/files';\n\n/**\n * Mount point of the Git repository inside the sandbox filesystem. The sandbox\n * fs is rooted at `/` (so apps can reach dynamic mounts like `/firestore`), with\n * the repo mounted here. URL subpaths are repo-relative, so the file router\n * resolves them under `APP_ROOT`.\n */\nexport const APP_ROOT = '/app';\n\n/** Resolve a repo-relative path (e.g. a URL subpath) to its absolute sandbox path. */\nexport const underAppRoot = (repoRelativePath: string): string =>\n joinPaths(APP_ROOT, repoRelativePath);\n\n/**\n * The origin every outer URL is rebuilt on — `protocol//host`, **port included**.\n *\n * `url.hostname` drops the port; `url.host` keeps it (and still omits it for the default\n * 80/443, so a production URL is byte-identical either way). Using `hostname` here was\n * invisible on `https://immediately.run` and broke every routed link under local dev on any\n * port but 80: `http://localhost:3100/edit/…` was rebuilt as `http://localhost/edit/…`.\n *\n * Worse than a wrong link, it silently changed link BEHAVIOUR. `repositoryPrefixURL` is\n * built from this, and `isInternalHref` decides by prefix-matching against it — so an\n * absolute in-app URL failed the match, was classified EXTERNAL, and rendered as a plain\n * `<a>`. Clicking it performed a real navigation out of the sandboxed frame instead of\n * routing: the app appeared to \"reload\" on an ordinary internal link.\n */\nexport const getOuterHostname = (outerHref:string) => {\n const url = new URL(outerHref);\n return `${url.protocol}//${url.host}`;\n}\n\nexport const getSearchParams = (search?: string): Record<string, string> => Object.fromEntries(\n [...(new URLSearchParams(search ?? window.location.search).entries())]);\n\n/**\n * Split a link target into its path part and its `#fragment` (the `#` dropped),\n * e.g. `\"FOO.mdx#sec-8-9\"` → `[\"FOO.mdx\", \"sec-8-9\"]`, `\"#sec-3\"` → `[\"\", \"sec-3\"]`,\n * `\"FOO.mdx\"` → `[\"FOO.mdx\", \"\"]`. Only the **first** `#` splits — a fragment never\n * contains another `#`. The seam between wiki-links (§13) and heading ids (§15):\n * existence is resolved on the path part, the fragment is threaded to navigation.\n * MARKDOWN_SYNTAX_SPEC §13.5.\n */\nexport const splitHash = (target: string): [string, string] => {\n const i = target.indexOf('#');\n return i === -1 ? [target, ''] : [target.slice(0, i), target.slice(i + 1)];\n};\n\n\nexport const parseTarget = (target: string, navigation: NavigationState): NavigationState => {\n const newNavigation = { ...navigation };\n let [prehash, hash] = target.split(\"#\")\n if (prehash) {\n let [path, search] = prehash.split(\"?\")\n if (path) {\n newNavigation.sandboxPath = path\n }\n newNavigation.search = search ? search : '';\n }\n newNavigation.hash = hash ? hash : '';\n return newNavigation\n}\n\n\nexport const maybeParseUrl = (str: string): URL | null => {\n try {\n return new URL(str);\n } catch (_) {\n return null;\n }\n}\n\nexport const isAbsolutePath = (sandboxPath: string) => sandboxPath.startsWith('/');\n\n/**\n * The origin+repo prefix every same-app URL starts with — used by {@link isInternalHref} to\n * decide whether an absolute href is \"this app\" or somewhere else.\n *\n * `hash` and `search` are cleared, not just `sandboxPath`. They belong to the CURRENT page,\n * and leaving them in put them on the end of a *prefix*: from a page carrying `#sec-8-9`,\n * the prefix became `…/main/#sec-8-9`, no same-app URL could start with it, every absolute\n * in-app link was classified EXTERNAL, and clicking one navigated the sandboxed frame away\n * instead of routing. A page with a fragment quietly broke its own links.\n */\nexport const repositoryPrefixURL = (outerHref:string, navigationState: NavigationState) => constructUrl(outerHref, {\n ...navigationState,\n sandboxPath: '',\n hash: '',\n search: '',\n });\n\nexport const constructOuterUrl = (previousOuterHref:string, sandboxTarget:string, navigationState: NavigationState, addFilesPrefix=true):string => {\n if (isAbsolutePath(sandboxTarget)) {\n // Split a trailing `#fragment` off before the target is folded into the\n // sandboxPath, and carry it in `hash` instead — a fragment addresses a section,\n // not a file, so it must not leak into the path (MARKDOWN_SYNTAX_SPEC §13.5). An\n // absolute target with no fragment clears any stale hash from the prior state.\n const [pathPart, hash] = splitHash(sandboxTarget);\n return constructUrl(\n previousOuterHref,\n {\n ...navigationState,\n sandboxPath: addFilesPrefix ? joinPaths(FILES_PREFIX, pathPart) : pathPart,\n hash,\n })\n }\n return (\n new URL(\n sandboxTarget,\n constructUrl(\n previousOuterHref,\n navigationState\n )\n )\n ).toString();\n}\n\nexport const isInternalHref = (outerHref:string, target: string, navigationState: NavigationState) => {\n const parsedUrl = maybeParseUrl(target);\n if (parsedUrl) {\n return target.startsWith(repositoryPrefixURL(outerHref, navigationState));\n }\n // if target is not a valid URL, then assume it's relative.\n return true;\n}\n\nexport type PathSegment = {\n name: string,\n pattern: string,\n transform?: (pathSegment: string) => string,\n // When true, the leading slash that delimits this segment is optional, so the\n // whole `/segment` group can be absent. Used for the trailing sandboxPath:\n // an outer href of `/mode/provider/namespace/repository/ref` (no trailing\n // slash, no sub-path) must still parse, otherwise the regex matches nothing\n // and every segment comes back empty.\n optionalLeadingSlash?: boolean\n}\n\nconst PATH_SEGMENTS: PathSegment[] = [\n { name: 'mode', pattern: '\\\\w+' },\n { name: 'provider', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'namespace', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'repository', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'ref', pattern: '[a-zA-Z0-9-_]+' },\n { name: 'sandboxPath', pattern: '.*', transform: s => `/${s}`, optionalLeadingSlash: true }\n];\n\nconst OUTER_HREF_REGEXP = new RegExp(\n '^' +\n PATH_SEGMENTS.map(({ name, pattern, optionalLeadingSlash }) =>\n optionalLeadingSlash\n ? `(?:\\/(?<${name}>${pattern}))?`\n : `\\/(?<${name}>${pattern})`\n ).join('') +\n \"$\"\n);\n\n\nexport const parsePath = (pathname: string): PathState => {\n const matchResults = pathname.match(OUTER_HREF_REGEXP)?.groups ?? {};\n return PATH_SEGMENTS.reduce((acc: Partial<PathState>, { name, transform }: PathSegment) => {\n let value: string | undefined = undefined;\n if (name in matchResults) {\n value = matchResults[name];\n }\n if (!value) {\n // fall back to default value if var not present in\n value = '';\n }\n if (typeof value === 'string') {\n acc[name] = transform ? transform(value) : value;\n }\n return acc;\n }, {}) as PathState;\n}\n\nexport const parseHref = (href: string): NavigationState => {\n const parsedUrl = new URL(href);\n const pathnameState = parsePath(parsedUrl.pathname);\n return {\n ...pathnameState,\n search: parsedUrl.search.substring(1),\n hash: parsedUrl.hash.substring(1),\n } as NavigationState\n}\n\nconst stripSlashPrefix = (s: string): string => s.startsWith('/') ? s.substring(1) : s;\n\nexport const constructUrl = (outerHref:string, navigationState: NavigationState): string => {\n const path = PATH_SEGMENTS.map(({ name }) => {\n let value = navigationState[name];\n return stripSlashPrefix(value ?? '');\n }).join('/');\n const host = getOuterHostname(outerHref);\n let url = `${host}/${path}`\n if (navigationState.search) {\n url += '?' + navigationState.search\n }\n if (navigationState.hash) {\n url += '#' + navigationState.hash\n }\n return url;\n}\n"],"mappings":"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","names":[]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.
|
|
24
|
+
const SDK_VERSION = "0.42.0";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.42.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.42.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED