@immediately-run/sdk 0.64.1 → 0.66.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/boot.cjs +6 -1
- package/dist/boot.cjs.map +1 -1
- package/dist/boot.js +6 -1
- package/dist/boot.js.map +1 -1
- package/dist/components/ScrollRestoration.cjs +117 -0
- package/dist/components/ScrollRestoration.cjs.map +1 -0
- package/dist/components/ScrollRestoration.d.cts +13 -0
- package/dist/components/ScrollRestoration.d.ts +13 -0
- package/dist/components/ScrollRestoration.js +94 -0
- package/dist/components/ScrollRestoration.js.map +1 -0
- package/dist/entryState.cjs +99 -0
- package/dist/entryState.cjs.map +1 -0
- package/dist/entryState.d.cts +42 -0
- package/dist/entryState.d.ts +42 -0
- package/dist/entryState.js +69 -0
- package/dist/entryState.js.map +1 -0
- package/dist/index.cjs +12 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/openExternal.cjs +41 -0
- package/dist/openExternal.cjs.map +1 -0
- package/dist/openExternal.d.cts +29 -0
- package/dist/openExternal.d.ts +29 -0
- package/dist/openExternal.js +18 -0
- package/dist/openExternal.js.map +1 -0
- package/dist/platformLink.cjs.map +1 -1
- package/dist/platformLink.d.cts +6 -2
- package/dist/platformLink.d.ts +6 -2
- package/dist/platformLink.js.map +1 -1
- package/dist/protocolSchemes.cjs +1 -0
- package/dist/protocolSchemes.cjs.map +1 -1
- package/dist/protocolSchemes.d.cts +1 -0
- package/dist/protocolSchemes.d.ts +1 -0
- package/dist/protocolSchemes.js +2 -0
- package/dist/protocolSchemes.js.map +1 -1
- package/dist/routing.cjs +3 -0
- package/dist/routing.cjs.map +1 -1
- package/dist/routing.js +3 -0
- package/dist/routing.js.map +1 -1
- package/dist/scrollRestore.cjs +43 -0
- package/dist/scrollRestore.cjs.map +1 -0
- package/dist/scrollRestore.d.cts +39 -0
- package/dist/scrollRestore.d.ts +39 -0
- package/dist/scrollRestore.js +18 -0
- package/dist/scrollRestore.js.map +1 -0
- package/dist/useEntryState.cjs +41 -0
- package/dist/useEntryState.cjs.map +1 -0
- package/dist/useEntryState.d.cts +28 -0
- package/dist/useEntryState.d.ts +28 -0
- package/dist/useEntryState.js +17 -0
- package/dist/useEntryState.js.map +1 -0
- 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 +3 -3
package/dist/boot.cjs
CHANGED
|
@@ -43,6 +43,7 @@ var import_sandboxUtils = require("./sandboxUtils");
|
|
|
43
43
|
var import_TinkerableContext = require("./TinkerableContext");
|
|
44
44
|
var import_urlUtils = require("./urlUtils");
|
|
45
45
|
var import_protocol = require("./generated/protocol");
|
|
46
|
+
var import_entryState = require("./entryState");
|
|
46
47
|
const resolveMdxComponents = (mdxComponents) => mdxComponents === void 0 ? import_MDXComponents.DEFAULT_MDX_COMPONENTS : typeof mdxComponents === "function" ? mdxComponents(import_MDXComponents.DEFAULT_MDX_COMPONENTS) : { ...import_MDXComponents.DEFAULT_MDX_COMPONENTS, ...mdxComponents };
|
|
47
48
|
const updateAlreadyApplied = (filesMetadata, update) => {
|
|
48
49
|
for (let [key, value] of Object.entries(update)) {
|
|
@@ -55,7 +56,11 @@ const updateAlreadyApplied = (filesMetadata, update) => {
|
|
|
55
56
|
const TinkerableApp = ({ routingSpec, children }) => {
|
|
56
57
|
const [context, setContext] = (0, import_react.useState)((0, import_contextUtils.getInitialContext)(routingSpec));
|
|
57
58
|
(0, import_react.useEffect)(() => {
|
|
58
|
-
const removeListener = (0, import_sandboxUtils.addListener)(import_protocol.URLCHANGE, ({ url }) => {
|
|
59
|
+
const removeListener = (0, import_sandboxUtils.addListener)(import_protocol.URLCHANGE, ({ url, entryState, back, forward }) => {
|
|
60
|
+
(0, import_entryState.receiveNavigation)({
|
|
61
|
+
state: entryState ?? void 0,
|
|
62
|
+
direction: back ? "back" : forward ? "forward" : "push"
|
|
63
|
+
});
|
|
59
64
|
setContext((context2) => {
|
|
60
65
|
const updatedContext = (0, import_contextUtils.updateContext)(context2, url);
|
|
61
66
|
if (updatedContext !== context2) {
|
package/dist/boot.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { ScrollAfterNavigation } from './components/ScrollAfterNavigation';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\nimport { METADATA_UPDATE, URLCHANGE } from './generated/protocol';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents = Record<string, FC> | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({ routingSpec, children }: { routingSpec: RoutingSpec; children?: ReactNode }) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener(URLCHANGE, ({ url }) => {\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`,\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n METADATA_UPDATE,\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n },\n );\n },\n source.event,\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {/* Capability C: lands cross-page `#fragment` deep-links on their section\n after the destination tree mounts, uniform for every MDX app (§13.5). */}\n <ScrollAfterNavigation />\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({ mdxComponents, routingSpec, children }: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>,\n );\n};\n"],"mappings":"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","names":["context"]}
|
|
1
|
+
{"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { ScrollAfterNavigation } from './components/ScrollAfterNavigation';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\nimport { METADATA_UPDATE, URLCHANGE } from './generated/protocol';\nimport { receiveNavigation } from './entryState';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents = Record<string, FC> | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({ routingSpec, children }: { routingSpec: RoutingSpec; children?: ReactNode }) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener(URLCHANGE, ({ url, entryState, back, forward }) => {\n // R3-627: record what the host says about this arrival BEFORE the route state\n // updates, so a consumer reacting to the new route already sees the scratch\n // and the direction that produced it.\n receiveNavigation({\n state: (entryState ?? undefined) as Record<string, unknown> | undefined,\n direction: back ? 'back' : forward ? 'forward' : 'push',\n });\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`,\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n METADATA_UPDATE,\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n },\n );\n },\n source.event,\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {/* Capability C: lands cross-page `#fragment` deep-links on their section\n after the destination tree mounts, uniform for every MDX app (§13.5). */}\n <ScrollAfterNavigation />\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({ mdxComponents, routingSpec, children }: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>,\n );\n};\n"],"mappings":"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","names":["context"]}
|
package/dist/boot.js
CHANGED
|
@@ -17,6 +17,7 @@ import { addListener } from "./sandboxUtils";
|
|
|
17
17
|
import { TinkerableContext } from "./TinkerableContext";
|
|
18
18
|
import { FILES_PREFIX } from "./urlUtils";
|
|
19
19
|
import { METADATA_UPDATE, URLCHANGE } from "./generated/protocol";
|
|
20
|
+
import { receiveNavigation } from "./entryState";
|
|
20
21
|
const resolveMdxComponents = (mdxComponents) => mdxComponents === void 0 ? DEFAULT_MDX_COMPONENTS : typeof mdxComponents === "function" ? mdxComponents(DEFAULT_MDX_COMPONENTS) : { ...DEFAULT_MDX_COMPONENTS, ...mdxComponents };
|
|
21
22
|
const updateAlreadyApplied = (filesMetadata, update) => {
|
|
22
23
|
for (let [key, value] of Object.entries(update)) {
|
|
@@ -29,7 +30,11 @@ const updateAlreadyApplied = (filesMetadata, update) => {
|
|
|
29
30
|
const TinkerableApp = ({ routingSpec, children }) => {
|
|
30
31
|
const [context, setContext] = useState(getInitialContext(routingSpec));
|
|
31
32
|
useEffect(() => {
|
|
32
|
-
const removeListener = addListener(URLCHANGE, ({ url }) => {
|
|
33
|
+
const removeListener = addListener(URLCHANGE, ({ url, entryState, back, forward }) => {
|
|
34
|
+
receiveNavigation({
|
|
35
|
+
state: entryState ?? void 0,
|
|
36
|
+
direction: back ? "back" : forward ? "forward" : "push"
|
|
37
|
+
});
|
|
33
38
|
setContext((context2) => {
|
|
34
39
|
const updatedContext = updateContext(context2, url);
|
|
35
40
|
if (updatedContext !== context2) {
|
package/dist/boot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { ScrollAfterNavigation } from './components/ScrollAfterNavigation';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\nimport { METADATA_UPDATE, URLCHANGE } from './generated/protocol';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents = Record<string, FC> | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({ routingSpec, children }: { routingSpec: RoutingSpec; children?: ReactNode }) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener(URLCHANGE, ({ url }) => {\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`,\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n METADATA_UPDATE,\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n },\n );\n },\n source.event,\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {/* Capability C: lands cross-page `#fragment` deep-links on their section\n after the destination tree mounts, uniform for every MDX app (§13.5). */}\n <ScrollAfterNavigation />\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({ mdxComponents, routingSpec, children }: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>,\n );\n};\n"],"mappings":"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","names":["context"]}
|
|
1
|
+
{"version":3,"sources":["../src/boot.tsx"],"sourcesContent":["import { FC, ReactNode, StrictMode, useEffect, useLayoutEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport { emitMarkerOnce } from './markers';\n\nimport { ErrorNotFound } from './components/errors';\nimport { FileRouter } from './components/FileRouter';\nimport { MainContent } from './components/MainContent';\nimport { DEFAULT_MDX_COMPONENTS } from './components/MDXComponents';\nimport { ScrollAfterNavigation } from './components/ScrollAfterNavigation';\nimport { getInitialContext, updateContext } from './contextUtils';\nimport { getInjectedMetadataEmitter, resolveMetadataSource } from './injectedBundler';\nimport { MDXProvider } from './MDXProvider';\nimport { ModuleCache, ModuleCacheContextProvider } from './moduleCache';\nimport { Router } from './routing';\nimport type { RoutingSpec } from './RoutingSpec';\nimport { FilesMetadata } from './sandboxTypes';\nimport { addListener } from './sandboxUtils';\nimport { TinkerableContext, TinkerableState } from './TinkerableContext';\nimport { FILES_PREFIX } from './urlUtils';\nimport { METADATA_UPDATE, URLCHANGE } from './generated/protocol';\nimport { receiveNavigation } from './entryState';\n\n/** A map of MDX component overrides, or a function that receives the platform\n * {@link DEFAULT_MDX_COMPONENTS} and returns the full map to use. */\nexport type MdxComponents = Record<string, FC> | ((defaults: Record<string, FC>) => Record<string, FC>);\n\n/**\n * Resolve the effective MDX component map from a {@link BootProps.mdxComponents}\n * value (MARKDOWN_SYNTAX_SPEC §11.3):\n * - `undefined` → the platform {@link DEFAULT_MDX_COMPONENTS} (same reference).\n * - a **function** → the full-replace escape hatch, handed the defaults.\n * - a **map** → merged *over* the defaults (`{ ...defaults, ...map }`), so\n * overriding one component keeps the rest — the phantom-defaults invariant\n * (§11.2) that stops the MDX missing-reference guard from firing.\n */\nexport const resolveMdxComponents = (mdxComponents?: MdxComponents): Record<string, FC> =>\n mdxComponents === undefined\n ? (DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : typeof mdxComponents === 'function'\n ? mdxComponents(DEFAULT_MDX_COMPONENTS as Record<string, FC>)\n : { ...(DEFAULT_MDX_COMPONENTS as Record<string, FC>), ...mdxComponents };\n\n/** Options for {@link boot}: MDX overrides, a route table, or an app root. */\nexport type BootProps = {\n /**\n * MDX component overrides. A **map** is *merged over* the platform defaults\n * ({@link DEFAULT_MDX_COMPONENTS}) — so overriding `WikiLink` alone keeps the\n * default `a` and `Admonition` (MARKDOWN_SYNTAX_SPEC §11.3). Pass a **function**\n * `(defaults) => map` as the full-replace escape hatch when you want complete\n * control over the set.\n */\n mdxComponents?: MdxComponents;\n routingSpec?: RoutingSpec;\n /**\n * App root rendered directly inside the providers (with full navigation\n * context), instead of dispatching through a `routingSpec`. Render\n * `<Routes>`/`<Route>` here for fully dynamic routing — no catch-all rule\n * boilerplate. Takes precedence over `routingSpec` for what is rendered.\n */\n children?: ReactNode;\n};\n\nconst updateAlreadyApplied = (filesMetadata: FilesMetadata, update: FilesMetadata) => {\n for (let [key, value] of Object.entries(update)) {\n if (filesMetadata[key] !== value) {\n return false;\n }\n }\n return true;\n};\n\n/** The app shell {@link boot} renders: holds navigation state, subscribes to host\n * URL + metadata pushes, and renders `children` or the route `<Router />`. */\nexport const TinkerableApp = ({ routingSpec, children }: { routingSpec: RoutingSpec; children?: ReactNode }) => {\n const [context, setContext] = useState<TinkerableState>(getInitialContext(routingSpec));\n useEffect(() => {\n const removeListener = addListener(URLCHANGE, ({ url, entryState, back, forward }) => {\n // R3-627: record what the host says about this arrival BEFORE the route state\n // updates, so a consumer reacting to the new route already sees the scratch\n // and the direction that produced it.\n receiveNavigation({\n state: (entryState ?? undefined) as Record<string, unknown> | undefined,\n direction: back ? 'back' : forward ? 'forward' : 'push',\n });\n setContext((context) => {\n const updatedContext = updateContext(context, url);\n if (updatedContext !== context) {\n console.log(\n `[Sandbox] Updating path from ${context.navigationState.sandboxPath} to ${updatedContext.navigationState.sandboxPath}`,\n );\n }\n return updatedContext;\n });\n });\n return removeListener;\n }, [setContext]);\n useEffect(() => {\n // Phase 5 dual-mode (SDK_PACKAGING_SPEC §4/§8): prefer the injected bundler's\n // metadata emitter (the live path, byte-identical); when the SDK is npm-fetched\n // with no injection, `event` is undefined so `addListener` receives\n // 'metadata-update' over the §4 transport instead, and `enable` is a no-op.\n const source = resolveMetadataSource(getInjectedMetadataEmitter());\n const dispose = addListener(\n METADATA_UPDATE,\n ({ update }: Record<string, any>) => {\n setContext((prevContext) =>\n updateAlreadyApplied(prevContext.filesMetadata, update)\n ? prevContext\n : {\n ...prevContext,\n filesMetadata: {\n // TODO: file deletion!\n ...prevContext.filesMetadata,\n ...update,\n },\n },\n );\n },\n source.event,\n );\n source.enable();\n return dispose;\n }, [setContext]);\n\n return (\n <TinkerableContext value={context}>\n {/* Capability C: lands cross-page `#fragment` deep-links on their section\n after the destination tree mounts, uniform for every MDX app (§13.5). */}\n <ScrollAfterNavigation />\n {children ?? <Router />}\n </TinkerableContext>\n );\n};\n\n// Boot marker emitter (LOAD_PROFILING_SPEC §3, R3-46). Rendered at the top of the\n// app tree so its layout effect fires on the FIRST root-render commit: that instant\n// is `ir.fmp` (the content is in the DOM, about to paint) and the baseline for\n// `ir.interactive` (the host treats a forwarded `ir.interactive` as the root-commit\n// signal and resolves `max(commit, reportReady)` — LP2-3 — so this can only ever be\n// delayed by an app's `reportReady()`, never advanced). Emitted in canonical stream\n// order (fmp then interactive); idempotent per name (StrictMode-safe). Renders null.\nconst BootMarkers = (): null => {\n useLayoutEffect(() => {\n emitMarkerOnce('ir.fmp');\n emitMarkerOnce('ir.interactive');\n }, []);\n return null;\n};\n\n/** The default route table when `boot` is called with no `routingSpec`/`children`:\n * `/` → main content, `/files/<path>` → the file router, else not-found.\n * Re-expressed with path templates (SANDBOX_ROUTING_SPEC §7); the file path\n * surfaces under the `*` wildcard. The catch-all stays a raw RegExp — the\n * escape hatch — so it anchors `.+` (a non-empty path) exactly as before. */\nexport const DEFAULT_ROUTING_SPEC: RoutingSpec = {\n routes: [\n { name: 'MainContent', pattern: '/', element: <MainContent /> },\n { name: 'FileRouter', pattern: `${FILES_PREFIX}/*`, element: <FileRouter /> },\n { name: 'ErrorNotFound', pattern: /^(?<path>.+)$/, element: <ErrorNotFound /> },\n ],\n};\n\n/**\n * Matches any `sandboxPath` so navigation context can be built without a route\n * table. Used when {@link boot} is given `children` (the app owns dispatch via\n * `<Routes>`); the catch-all's `element` is never rendered (children are).\n */\nexport const CATCH_ALL_ROUTING_SPEC: RoutingSpec = {\n routes: [{ name: 'AppRoot', pattern: /^.*$/, element: null }],\n};\n\n/**\n * Mount an immediately.run app into the sandbox `#root`. The entry point every\n * app calls from `index.tsx`: wires the MDX, module-cache, and navigation\n * providers, then renders the route table (`routingSpec`) or your `children`.\n */\nexport const boot = ({ mdxComponents, routingSpec, children }: BootProps = {}) => {\n const rootElement = document.getElementById('root');\n if (!rootElement) {\n throw new Error('boot requires root HTML element to exist');\n }\n const resolvedComponents = resolveMdxComponents(mdxComponents);\n // `children` own dispatch, so a catch-all keeps navigation working without a\n // table; otherwise fall back to the default file/main-content routes.\n const spec = routingSpec ?? (children ? CATCH_ALL_ROUTING_SPEC : DEFAULT_ROUTING_SPEC);\n const moduleCache = new ModuleCache();\n const root = createRoot(rootElement);\n root.render(\n <StrictMode>\n <ModuleCacheContextProvider moduleCache={moduleCache}>\n <MDXProvider components={resolvedComponents}>\n <BootMarkers />\n <TinkerableApp routingSpec={spec}>{children}</TinkerableApp>\n </MDXProvider>\n </ModuleCacheContextProvider>\n </StrictMode>,\n );\n};\n"],"mappings":"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","names":["context"]}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var ScrollRestoration_exports = {};
|
|
20
|
+
__export(ScrollRestoration_exports, {
|
|
21
|
+
ScrollRestoration: () => ScrollRestoration
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(ScrollRestoration_exports);
|
|
24
|
+
var import_react = require("react");
|
|
25
|
+
var import_entryState = require("../entryState");
|
|
26
|
+
var import_scrollRestore = require("../scrollRestore");
|
|
27
|
+
var import_TinkerableContext = require("../TinkerableContext");
|
|
28
|
+
var import_useEntryState = require("../useEntryState");
|
|
29
|
+
const SCROLL_KEY = "ir.scroll";
|
|
30
|
+
const documentScroller = () => {
|
|
31
|
+
if (typeof document === "undefined" || typeof window === "undefined") return null;
|
|
32
|
+
const el = () => document.scrollingElement ?? document.documentElement;
|
|
33
|
+
return {
|
|
34
|
+
offset: () => window.scrollY,
|
|
35
|
+
scrollTo: (top) => window.scrollTo(0, top),
|
|
36
|
+
scrollHeight: () => el().scrollHeight,
|
|
37
|
+
clientHeight: () => window.innerHeight,
|
|
38
|
+
listen: (onScroll) => {
|
|
39
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
40
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const elementScroller = (el) => ({
|
|
45
|
+
offset: () => el.scrollTop,
|
|
46
|
+
scrollTo: (top) => {
|
|
47
|
+
el.scrollTop = top;
|
|
48
|
+
},
|
|
49
|
+
scrollHeight: () => el.scrollHeight,
|
|
50
|
+
clientHeight: () => el.clientHeight,
|
|
51
|
+
listen: (onScroll) => {
|
|
52
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
53
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const ScrollRestoration = ({ scroller } = {}) => {
|
|
57
|
+
const { value: rememberedOffset } = (0, import_useEntryState.useEntryState)(SCROLL_KEY);
|
|
58
|
+
const direction = (0, import_useEntryState.useNavigationDirection)();
|
|
59
|
+
const { navigationState } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
|
|
60
|
+
const hasFragment = Boolean(navigationState.hash);
|
|
61
|
+
const scrollerRef = (0, import_react.useRef)(scroller);
|
|
62
|
+
scrollerRef.current = scroller;
|
|
63
|
+
const resolve = () => {
|
|
64
|
+
const el = scrollerRef.current?.current;
|
|
65
|
+
return el ? elementScroller(el) : documentScroller();
|
|
66
|
+
};
|
|
67
|
+
(0, import_react.useEffect)(() => {
|
|
68
|
+
return (0, import_entryState.registerEntryStateCollector)(SCROLL_KEY, () => {
|
|
69
|
+
const s = resolve();
|
|
70
|
+
const offset = s?.offset() ?? 0;
|
|
71
|
+
return offset > 0 ? Math.round(offset) : void 0;
|
|
72
|
+
});
|
|
73
|
+
}, []);
|
|
74
|
+
(0, import_react.useEffect)(() => {
|
|
75
|
+
if (direction === "push") return;
|
|
76
|
+
if (hasFragment) return;
|
|
77
|
+
if (typeof rememberedOffset !== "number") return;
|
|
78
|
+
const s = resolve();
|
|
79
|
+
if (!s) return;
|
|
80
|
+
const startedAt = Date.now();
|
|
81
|
+
let userScrolled = false;
|
|
82
|
+
let done = false;
|
|
83
|
+
const finish = () => {
|
|
84
|
+
done = true;
|
|
85
|
+
stopListening();
|
|
86
|
+
observer?.disconnect();
|
|
87
|
+
timers.forEach(clearTimeout);
|
|
88
|
+
};
|
|
89
|
+
const attempt = () => {
|
|
90
|
+
if (done) return;
|
|
91
|
+
const action = (0, import_scrollRestore.nextRestoreAction)({
|
|
92
|
+
target: rememberedOffset,
|
|
93
|
+
scrollHeight: s.scrollHeight(),
|
|
94
|
+
clientHeight: s.clientHeight(),
|
|
95
|
+
current: s.offset(),
|
|
96
|
+
elapsedMs: Date.now() - startedAt,
|
|
97
|
+
userScrolled
|
|
98
|
+
});
|
|
99
|
+
if (action === "wait") return;
|
|
100
|
+
if (action === "apply") s.scrollTo(rememberedOffset);
|
|
101
|
+
finish();
|
|
102
|
+
};
|
|
103
|
+
const stopListening = s.listen(() => {
|
|
104
|
+
if (!done) userScrolled = true;
|
|
105
|
+
});
|
|
106
|
+
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => attempt());
|
|
107
|
+
observer?.observe(document.body, { childList: true, subtree: true });
|
|
108
|
+
const timers = [0, 120, 300, 600, import_scrollRestore.RESTORE_DEADLINE_MS].map((ms) => setTimeout(attempt, ms));
|
|
109
|
+
return finish;
|
|
110
|
+
}, [rememberedOffset, direction, hasFragment]);
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
114
|
+
0 && (module.exports = {
|
|
115
|
+
ScrollRestoration
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=ScrollRestoration.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/ScrollRestoration.tsx"],"sourcesContent":["// Remember where the reader was, and put them back there on Back (R3-627).\n//\n// Mount once, near the root, beside `ScrollAfterNavigation`. With no props it\n// remembers the document's scroll offset, which is right for an app whose page\n// scrolls; an app that scrolls its own container passes a ref to it.\n//\n// What it does NOT do: fight the reader, fight a deep link, or scroll a page the\n// reader arrived at by clicking a link. Only a history traversal restores, only if\n// the entry carries an offset, and only until the reader touches the scroller.\n\nimport { type RefObject, use, useEffect, useRef } from 'react';\n\nimport { registerEntryStateCollector } from '../entryState';\nimport { nextRestoreAction, RESTORE_DEADLINE_MS } from '../scrollRestore';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { useEntryState, useNavigationDirection } from '../useEntryState';\n\n/** The key this component owns in the entry scratch. One owner per key. */\nconst SCROLL_KEY = 'ir.scroll';\n\n/** The geometry of whatever is scrolling, read uniformly for the document and for a\n * container, so the decision module never has to care which it is. */\ninterface Scroller {\n offset(): number;\n scrollTo(top: number): void;\n scrollHeight(): number;\n clientHeight(): number;\n listen(onScroll: () => void): () => void;\n}\n\nconst documentScroller = (): Scroller | null => {\n if (typeof document === 'undefined' || typeof window === 'undefined') return null;\n const el = () => document.scrollingElement ?? document.documentElement;\n return {\n offset: () => window.scrollY,\n scrollTo: (top) => window.scrollTo(0, top),\n scrollHeight: () => el().scrollHeight,\n clientHeight: () => window.innerHeight,\n listen: (onScroll) => {\n window.addEventListener('scroll', onScroll, { passive: true });\n return () => window.removeEventListener('scroll', onScroll);\n },\n };\n};\n\nconst elementScroller = (el: HTMLElement): Scroller => ({\n offset: () => el.scrollTop,\n scrollTo: (top) => {\n el.scrollTop = top;\n },\n scrollHeight: () => el.scrollHeight,\n clientHeight: () => el.clientHeight,\n listen: (onScroll) => {\n el.addEventListener('scroll', onScroll, { passive: true });\n return () => el.removeEventListener('scroll', onScroll);\n },\n});\n\nexport interface ScrollRestorationProps {\n /** The element that scrolls. Omit when the document scrolls. */\n scroller?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Null-rendering. Remembers the scroller's offset on the entry being left, and\n * restores it when the reader returns to that entry.\n */\nexport const ScrollRestoration = ({ scroller }: ScrollRestorationProps = {}): null => {\n const { value: rememberedOffset } = useEntryState<number>(SCROLL_KEY);\n const direction = useNavigationDirection();\n const { navigationState } = use(TinkerableContext);\n const hasFragment = Boolean(navigationState.hash);\n // Read through a ref so the collector registered below is stable for the life of\n // the component: re-registering per render would churn the one-owner-per-key map.\n const scrollerRef = useRef(scroller);\n scrollerRef.current = scroller;\n\n const resolve = (): Scroller | null => {\n const el = scrollerRef.current?.current;\n return el ? elementScroller(el) : documentScroller();\n };\n\n // Save: asked at navigation time, from inside `navigate()`.\n useEffect(() => {\n return registerEntryStateCollector(SCROLL_KEY, () => {\n const s = resolve();\n const offset = s?.offset() ?? 0;\n // Nothing to remember about the top of a page; leaving the key out keeps the\n // scratch empty in the common case, so most entries carry no state at all.\n return offset > 0 ? Math.round(offset) : undefined;\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Restore: only on a traversal, only with an offset, and never over a deep link.\n useEffect(() => {\n if (direction === 'push') return;\n if (hasFragment) return; // an explicit #fragment outranks a remembered position\n if (typeof rememberedOffset !== 'number') return;\n const s = resolve();\n if (!s) return;\n\n const startedAt = Date.now();\n let userScrolled = false;\n let done = false;\n\n const finish = () => {\n done = true;\n stopListening();\n observer?.disconnect();\n timers.forEach(clearTimeout);\n };\n\n const attempt = () => {\n if (done) return;\n const action = nextRestoreAction({\n target: rememberedOffset,\n scrollHeight: s.scrollHeight(),\n clientHeight: s.clientHeight(),\n current: s.offset(),\n elapsedMs: Date.now() - startedAt,\n userScrolled,\n });\n if (action === 'wait') return;\n if (action === 'apply') s.scrollTo(rememberedOffset);\n finish();\n };\n\n // The reader wins: the first scroll they make ends the attempt. Registered\n // before the first attempt so a scroll during that frame is not missed.\n const stopListening = s.listen(() => {\n // Our own `scrollTo` fires this too; `done` is set before the event lands\n // because `finish()` runs synchronously after `apply`.\n if (!done) userScrolled = true;\n });\n\n // The content arrives asynchronously, so sample as it grows: on DOM changes, on\n // the same ladder `ScrollAfterNavigation` uses, and once at the deadline.\n const observer = typeof MutationObserver === 'undefined' ? null : new MutationObserver(() => attempt());\n observer?.observe(document.body, { childList: true, subtree: true });\n const timers = [0, 120, 300, 600, RESTORE_DEADLINE_MS].map((ms) => setTimeout(attempt, ms));\n\n return finish;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rememberedOffset, direction, hasFragment]);\n\n return null;\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ScrollRestorationProps {
|
|
4
|
+
/** The element that scrolls. Omit when the document scrolls. */
|
|
5
|
+
scroller?: RefObject<HTMLElement | null>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Null-rendering. Remembers the scroller's offset on the entry being left, and
|
|
9
|
+
* restores it when the reader returns to that entry.
|
|
10
|
+
*/
|
|
11
|
+
declare const ScrollRestoration: ({ scroller }?: ScrollRestorationProps) => null;
|
|
12
|
+
|
|
13
|
+
export { ScrollRestoration, type ScrollRestorationProps };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ScrollRestorationProps {
|
|
4
|
+
/** The element that scrolls. Omit when the document scrolls. */
|
|
5
|
+
scroller?: RefObject<HTMLElement | null>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Null-rendering. Remembers the scroller's offset on the entry being left, and
|
|
9
|
+
* restores it when the reader returns to that entry.
|
|
10
|
+
*/
|
|
11
|
+
declare const ScrollRestoration: ({ scroller }?: ScrollRestorationProps) => null;
|
|
12
|
+
|
|
13
|
+
export { ScrollRestoration, type ScrollRestorationProps };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import "../chunk-VHAA22YE.js";
|
|
2
|
+
import { use, useEffect, useRef } from "react";
|
|
3
|
+
import { registerEntryStateCollector } from "../entryState";
|
|
4
|
+
import { nextRestoreAction, RESTORE_DEADLINE_MS } from "../scrollRestore";
|
|
5
|
+
import { TinkerableContext } from "../TinkerableContext";
|
|
6
|
+
import { useEntryState, useNavigationDirection } from "../useEntryState";
|
|
7
|
+
const SCROLL_KEY = "ir.scroll";
|
|
8
|
+
const documentScroller = () => {
|
|
9
|
+
if (typeof document === "undefined" || typeof window === "undefined") return null;
|
|
10
|
+
const el = () => document.scrollingElement ?? document.documentElement;
|
|
11
|
+
return {
|
|
12
|
+
offset: () => window.scrollY,
|
|
13
|
+
scrollTo: (top) => window.scrollTo(0, top),
|
|
14
|
+
scrollHeight: () => el().scrollHeight,
|
|
15
|
+
clientHeight: () => window.innerHeight,
|
|
16
|
+
listen: (onScroll) => {
|
|
17
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
18
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const elementScroller = (el) => ({
|
|
23
|
+
offset: () => el.scrollTop,
|
|
24
|
+
scrollTo: (top) => {
|
|
25
|
+
el.scrollTop = top;
|
|
26
|
+
},
|
|
27
|
+
scrollHeight: () => el.scrollHeight,
|
|
28
|
+
clientHeight: () => el.clientHeight,
|
|
29
|
+
listen: (onScroll) => {
|
|
30
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
31
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const ScrollRestoration = ({ scroller } = {}) => {
|
|
35
|
+
const { value: rememberedOffset } = useEntryState(SCROLL_KEY);
|
|
36
|
+
const direction = useNavigationDirection();
|
|
37
|
+
const { navigationState } = use(TinkerableContext);
|
|
38
|
+
const hasFragment = Boolean(navigationState.hash);
|
|
39
|
+
const scrollerRef = useRef(scroller);
|
|
40
|
+
scrollerRef.current = scroller;
|
|
41
|
+
const resolve = () => {
|
|
42
|
+
const el = scrollerRef.current?.current;
|
|
43
|
+
return el ? elementScroller(el) : documentScroller();
|
|
44
|
+
};
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
return registerEntryStateCollector(SCROLL_KEY, () => {
|
|
47
|
+
const s = resolve();
|
|
48
|
+
const offset = s?.offset() ?? 0;
|
|
49
|
+
return offset > 0 ? Math.round(offset) : void 0;
|
|
50
|
+
});
|
|
51
|
+
}, []);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (direction === "push") return;
|
|
54
|
+
if (hasFragment) return;
|
|
55
|
+
if (typeof rememberedOffset !== "number") return;
|
|
56
|
+
const s = resolve();
|
|
57
|
+
if (!s) return;
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
let userScrolled = false;
|
|
60
|
+
let done = false;
|
|
61
|
+
const finish = () => {
|
|
62
|
+
done = true;
|
|
63
|
+
stopListening();
|
|
64
|
+
observer?.disconnect();
|
|
65
|
+
timers.forEach(clearTimeout);
|
|
66
|
+
};
|
|
67
|
+
const attempt = () => {
|
|
68
|
+
if (done) return;
|
|
69
|
+
const action = nextRestoreAction({
|
|
70
|
+
target: rememberedOffset,
|
|
71
|
+
scrollHeight: s.scrollHeight(),
|
|
72
|
+
clientHeight: s.clientHeight(),
|
|
73
|
+
current: s.offset(),
|
|
74
|
+
elapsedMs: Date.now() - startedAt,
|
|
75
|
+
userScrolled
|
|
76
|
+
});
|
|
77
|
+
if (action === "wait") return;
|
|
78
|
+
if (action === "apply") s.scrollTo(rememberedOffset);
|
|
79
|
+
finish();
|
|
80
|
+
};
|
|
81
|
+
const stopListening = s.listen(() => {
|
|
82
|
+
if (!done) userScrolled = true;
|
|
83
|
+
});
|
|
84
|
+
const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => attempt());
|
|
85
|
+
observer?.observe(document.body, { childList: true, subtree: true });
|
|
86
|
+
const timers = [0, 120, 300, 600, RESTORE_DEADLINE_MS].map((ms) => setTimeout(attempt, ms));
|
|
87
|
+
return finish;
|
|
88
|
+
}, [rememberedOffset, direction, hasFragment]);
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
export {
|
|
92
|
+
ScrollRestoration
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=ScrollRestoration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/ScrollRestoration.tsx"],"sourcesContent":["// Remember where the reader was, and put them back there on Back (R3-627).\n//\n// Mount once, near the root, beside `ScrollAfterNavigation`. With no props it\n// remembers the document's scroll offset, which is right for an app whose page\n// scrolls; an app that scrolls its own container passes a ref to it.\n//\n// What it does NOT do: fight the reader, fight a deep link, or scroll a page the\n// reader arrived at by clicking a link. Only a history traversal restores, only if\n// the entry carries an offset, and only until the reader touches the scroller.\n\nimport { type RefObject, use, useEffect, useRef } from 'react';\n\nimport { registerEntryStateCollector } from '../entryState';\nimport { nextRestoreAction, RESTORE_DEADLINE_MS } from '../scrollRestore';\nimport { TinkerableContext } from '../TinkerableContext';\nimport { useEntryState, useNavigationDirection } from '../useEntryState';\n\n/** The key this component owns in the entry scratch. One owner per key. */\nconst SCROLL_KEY = 'ir.scroll';\n\n/** The geometry of whatever is scrolling, read uniformly for the document and for a\n * container, so the decision module never has to care which it is. */\ninterface Scroller {\n offset(): number;\n scrollTo(top: number): void;\n scrollHeight(): number;\n clientHeight(): number;\n listen(onScroll: () => void): () => void;\n}\n\nconst documentScroller = (): Scroller | null => {\n if (typeof document === 'undefined' || typeof window === 'undefined') return null;\n const el = () => document.scrollingElement ?? document.documentElement;\n return {\n offset: () => window.scrollY,\n scrollTo: (top) => window.scrollTo(0, top),\n scrollHeight: () => el().scrollHeight,\n clientHeight: () => window.innerHeight,\n listen: (onScroll) => {\n window.addEventListener('scroll', onScroll, { passive: true });\n return () => window.removeEventListener('scroll', onScroll);\n },\n };\n};\n\nconst elementScroller = (el: HTMLElement): Scroller => ({\n offset: () => el.scrollTop,\n scrollTo: (top) => {\n el.scrollTop = top;\n },\n scrollHeight: () => el.scrollHeight,\n clientHeight: () => el.clientHeight,\n listen: (onScroll) => {\n el.addEventListener('scroll', onScroll, { passive: true });\n return () => el.removeEventListener('scroll', onScroll);\n },\n});\n\nexport interface ScrollRestorationProps {\n /** The element that scrolls. Omit when the document scrolls. */\n scroller?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Null-rendering. Remembers the scroller's offset on the entry being left, and\n * restores it when the reader returns to that entry.\n */\nexport const ScrollRestoration = ({ scroller }: ScrollRestorationProps = {}): null => {\n const { value: rememberedOffset } = useEntryState<number>(SCROLL_KEY);\n const direction = useNavigationDirection();\n const { navigationState } = use(TinkerableContext);\n const hasFragment = Boolean(navigationState.hash);\n // Read through a ref so the collector registered below is stable for the life of\n // the component: re-registering per render would churn the one-owner-per-key map.\n const scrollerRef = useRef(scroller);\n scrollerRef.current = scroller;\n\n const resolve = (): Scroller | null => {\n const el = scrollerRef.current?.current;\n return el ? elementScroller(el) : documentScroller();\n };\n\n // Save: asked at navigation time, from inside `navigate()`.\n useEffect(() => {\n return registerEntryStateCollector(SCROLL_KEY, () => {\n const s = resolve();\n const offset = s?.offset() ?? 0;\n // Nothing to remember about the top of a page; leaving the key out keeps the\n // scratch empty in the common case, so most entries carry no state at all.\n return offset > 0 ? Math.round(offset) : undefined;\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Restore: only on a traversal, only with an offset, and never over a deep link.\n useEffect(() => {\n if (direction === 'push') return;\n if (hasFragment) return; // an explicit #fragment outranks a remembered position\n if (typeof rememberedOffset !== 'number') return;\n const s = resolve();\n if (!s) return;\n\n const startedAt = Date.now();\n let userScrolled = false;\n let done = false;\n\n const finish = () => {\n done = true;\n stopListening();\n observer?.disconnect();\n timers.forEach(clearTimeout);\n };\n\n const attempt = () => {\n if (done) return;\n const action = nextRestoreAction({\n target: rememberedOffset,\n scrollHeight: s.scrollHeight(),\n clientHeight: s.clientHeight(),\n current: s.offset(),\n elapsedMs: Date.now() - startedAt,\n userScrolled,\n });\n if (action === 'wait') return;\n if (action === 'apply') s.scrollTo(rememberedOffset);\n finish();\n };\n\n // The reader wins: the first scroll they make ends the attempt. Registered\n // before the first attempt so a scroll during that frame is not missed.\n const stopListening = s.listen(() => {\n // Our own `scrollTo` fires this too; `done` is set before the event lands\n // because `finish()` runs synchronously after `apply`.\n if (!done) userScrolled = true;\n });\n\n // The content arrives asynchronously, so sample as it grows: on DOM changes, on\n // the same ladder `ScrollAfterNavigation` uses, and once at the deadline.\n const observer = typeof MutationObserver === 'undefined' ? null : new MutationObserver(() => attempt());\n observer?.observe(document.body, { childList: true, subtree: true });\n const timers = [0, 120, 300, 600, RESTORE_DEADLINE_MS].map((ms) => setTimeout(attempt, ms));\n\n return finish;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rememberedOffset, direction, hasFragment]);\n\n return null;\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var entryState_exports = {};
|
|
20
|
+
__export(entryState_exports, {
|
|
21
|
+
ENTRY_STATE_MAX_BYTES: () => ENTRY_STATE_MAX_BYTES,
|
|
22
|
+
getArrivedNavigation: () => getArrivedNavigation,
|
|
23
|
+
receiveNavigation: () => receiveNavigation,
|
|
24
|
+
registerEntryStateCollector: () => registerEntryStateCollector,
|
|
25
|
+
resetEntryState: () => resetEntryState,
|
|
26
|
+
saveEntryState: () => saveEntryState,
|
|
27
|
+
subscribeNavigation: () => subscribeNavigation,
|
|
28
|
+
takeQueuedEntryState: () => takeQueuedEntryState
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(entryState_exports);
|
|
31
|
+
const ENTRY_STATE_MAX_BYTES = 4096;
|
|
32
|
+
const collectors = /* @__PURE__ */ new Map();
|
|
33
|
+
let queued = {};
|
|
34
|
+
let arrived = { state: void 0, direction: "push" };
|
|
35
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
36
|
+
const notify = () => {
|
|
37
|
+
for (const l of [...listeners]) l();
|
|
38
|
+
};
|
|
39
|
+
const saveEntryState = (key, value) => {
|
|
40
|
+
queued[key] = value;
|
|
41
|
+
};
|
|
42
|
+
const registerEntryStateCollector = (key, collect) => {
|
|
43
|
+
collectors.set(key, collect);
|
|
44
|
+
return () => {
|
|
45
|
+
if (collectors.get(key) === collect) collectors.delete(key);
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const takeQueuedEntryState = () => {
|
|
49
|
+
const out = { ...queued };
|
|
50
|
+
queued = {};
|
|
51
|
+
for (const [key, collect] of collectors) {
|
|
52
|
+
try {
|
|
53
|
+
const value = collect();
|
|
54
|
+
if (value !== void 0) out[key] = value;
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (Object.keys(out).length === 0) return void 0;
|
|
59
|
+
let size = 0;
|
|
60
|
+
try {
|
|
61
|
+
size = JSON.stringify(out).length;
|
|
62
|
+
} catch {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
if (size > ENTRY_STATE_MAX_BYTES) {
|
|
66
|
+
console.warn(
|
|
67
|
+
`[Sandbox] entry state is ${size} bytes, over the ${ENTRY_STATE_MAX_BYTES}-byte cap \u2014 dropped. Keep per-entry scratch small (a scroll offset, a few ids), not a cache.`
|
|
68
|
+
);
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
const receiveNavigation = (next) => {
|
|
74
|
+
arrived = next;
|
|
75
|
+
notify();
|
|
76
|
+
};
|
|
77
|
+
const getArrivedNavigation = () => arrived;
|
|
78
|
+
const subscribeNavigation = (listener) => {
|
|
79
|
+
listeners.add(listener);
|
|
80
|
+
return () => listeners.delete(listener);
|
|
81
|
+
};
|
|
82
|
+
const resetEntryState = () => {
|
|
83
|
+
collectors.clear();
|
|
84
|
+
queued = {};
|
|
85
|
+
arrived = { state: void 0, direction: "push" };
|
|
86
|
+
listeners.clear();
|
|
87
|
+
};
|
|
88
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
89
|
+
0 && (module.exports = {
|
|
90
|
+
ENTRY_STATE_MAX_BYTES,
|
|
91
|
+
getArrivedNavigation,
|
|
92
|
+
receiveNavigation,
|
|
93
|
+
registerEntryStateCollector,
|
|
94
|
+
resetEntryState,
|
|
95
|
+
saveEntryState,
|
|
96
|
+
subscribeNavigation,
|
|
97
|
+
takeQueuedEntryState
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=entryState.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/entryState.ts"],"sourcesContent":["// Per-history-entry scratch (R3-627): a small opaque value an app attaches to the\n// history entry it is LEAVING, and reads back when the browser returns to that entry.\n//\n// Why this exists. The app frame's realm is replaced by an in-app navigation, so\n// nothing in app memory survives it; the host owns the history stack but is at an\n// opaque origin and cannot look inside the frame. So the only way an app can leave\n// itself a bookmark is to hand the host a value to hold against the entry. The host\n// never parses it — it is the app's own value coming back to the app, which is why\n// this does not weaken `EDITOR_AS_APP_SPEC §2` (\"view state … never crosses the\n// boundary\"): the host is a courier, not a reader.\n//\n// Collected at navigation time, not continuously. The app is the one calling\n// `navigate()`, so the scratch is gathered synchronously inside that call — no scroll\n// reporting channel, no throttling, no traffic for a value needed at one instant.\n\n/** How the browser reached the current entry. `push` is an ordinary forward\n * navigation; `back`/`forward` are traversals, and only a traversal restores. */\nexport type NavigationDirection = 'push' | 'back' | 'forward';\n\n/** Serialized cap for the whole scratch. Past this the scratch is dropped with a\n * dev-time warning rather than silently truncated: a half-written bookmark that\n * restores to the wrong place is worse than no bookmark. */\nexport const ENTRY_STATE_MAX_BYTES = 4096;\n\nexport interface ArrivedNavigation {\n /** The scratch the leaving app left on this entry, if any. */\n readonly state: Readonly<Record<string, unknown>> | undefined;\n /** How this entry was reached. */\n readonly direction: NavigationDirection;\n}\n\ntype Collector = () => unknown;\n\nconst collectors = new Map<string, Collector>();\nlet queued: Record<string, unknown> = {};\nlet arrived: ArrivedNavigation = { state: undefined, direction: 'push' };\nconst listeners = new Set<() => void>();\n\nconst notify = (): void => {\n for (const l of [...listeners]) l();\n};\n\n/**\n * Queue a value for the entry being left. The last call before a navigation wins.\n * Prefer {@link registerEntryStateCollector} for values that are only knowable at the\n * instant of navigating (a scroll offset is the motivating case).\n */\nexport const saveEntryState = (key: string, value: unknown): void => {\n queued[key] = value;\n};\n\n/** Register a callback asked for its value at navigation time. Returns its remover.\n * A second registration for the same key replaces the first — one owner per key. */\nexport const registerEntryStateCollector = (key: string, collect: Collector): (() => void) => {\n collectors.set(key, collect);\n return () => {\n if (collectors.get(key) === collect) collectors.delete(key);\n };\n};\n\n/**\n * Gather the scratch for the entry being left and clear the queue. Called by\n * `navigate()` — not part of the app-facing surface.\n *\n * A collector that throws is skipped: a bookmark is a convenience, and it must never\n * be able to break a navigation.\n */\nexport const takeQueuedEntryState = (): Record<string, unknown> | undefined => {\n const out: Record<string, unknown> = { ...queued };\n queued = {};\n for (const [key, collect] of collectors) {\n try {\n const value = collect();\n if (value !== undefined) out[key] = value;\n } catch {\n /* a collector must not break navigation */\n }\n }\n if (Object.keys(out).length === 0) return undefined;\n let size = 0;\n try {\n size = JSON.stringify(out).length;\n } catch {\n return undefined; // not serializable → nothing to hand the host\n }\n if (size > ENTRY_STATE_MAX_BYTES) {\n console.warn(\n `[Sandbox] entry state is ${size} bytes, over the ${ENTRY_STATE_MAX_BYTES}-byte cap — dropped. ` +\n `Keep per-entry scratch small (a scroll offset, a few ids), not a cache.`,\n );\n return undefined;\n }\n return out;\n};\n\n/** Record what the host said about the entry just arrived at. Called by the boot\n * shell's `urlchange` listener — not part of the app-facing surface. */\nexport const receiveNavigation = (next: ArrivedNavigation): void => {\n arrived = next;\n notify();\n};\n\n/** The current arrival, as one stable object so `useSyncExternalStore` can compare\n * by identity. */\nexport const getArrivedNavigation = (): ArrivedNavigation => arrived;\n\nexport const subscribeNavigation = (listener: () => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n};\n\n/** Test seam: forget every collector, queued value and arrival. */\nexport const resetEntryState = (): void => {\n collectors.clear();\n queued = {};\n arrived = { state: undefined, direction: 'push' };\n listeners.clear();\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBO,MAAM,wBAAwB;AAWrC,MAAM,aAAa,oBAAI,IAAuB;AAC9C,IAAI,SAAkC,CAAC;AACvC,IAAI,UAA6B,EAAE,OAAO,QAAW,WAAW,OAAO;AACvE,MAAM,YAAY,oBAAI,IAAgB;AAEtC,MAAM,SAAS,MAAY;AACzB,aAAW,KAAK,CAAC,GAAG,SAAS,EAAG,GAAE;AACpC;AAOO,MAAM,iBAAiB,CAAC,KAAa,UAAyB;AACnE,SAAO,GAAG,IAAI;AAChB;AAIO,MAAM,8BAA8B,CAAC,KAAa,YAAqC;AAC5F,aAAW,IAAI,KAAK,OAAO;AAC3B,SAAO,MAAM;AACX,QAAI,WAAW,IAAI,GAAG,MAAM,QAAS,YAAW,OAAO,GAAG;AAAA,EAC5D;AACF;AASO,MAAM,uBAAuB,MAA2C;AAC7E,QAAM,MAA+B,EAAE,GAAG,OAAO;AACjD,WAAS,CAAC;AACV,aAAW,CAAC,KAAK,OAAO,KAAK,YAAY;AACvC,QAAI;AACF,YAAM,QAAQ,QAAQ;AACtB,UAAI,UAAU,OAAW,KAAI,GAAG,IAAI;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,MAAI,OAAO;AACX,MAAI;AACF,WAAO,KAAK,UAAU,GAAG,EAAE;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,uBAAuB;AAChC,YAAQ;AAAA,MACN,4BAA4B,IAAI,oBAAoB,qBAAqB;AAAA,IAE3E;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAIO,MAAM,oBAAoB,CAAC,SAAkC;AAClE,YAAU;AACV,SAAO;AACT;AAIO,MAAM,uBAAuB,MAAyB;AAEtD,MAAM,sBAAsB,CAAC,aAAuC;AACzE,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM,UAAU,OAAO,QAAQ;AACxC;AAGO,MAAM,kBAAkB,MAAY;AACzC,aAAW,MAAM;AACjB,WAAS,CAAC;AACV,YAAU,EAAE,OAAO,QAAW,WAAW,OAAO;AAChD,YAAU,MAAM;AAClB;","names":[]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** How the browser reached the current entry. `push` is an ordinary forward
|
|
2
|
+
* navigation; `back`/`forward` are traversals, and only a traversal restores. */
|
|
3
|
+
type NavigationDirection = 'push' | 'back' | 'forward';
|
|
4
|
+
/** Serialized cap for the whole scratch. Past this the scratch is dropped with a
|
|
5
|
+
* dev-time warning rather than silently truncated: a half-written bookmark that
|
|
6
|
+
* restores to the wrong place is worse than no bookmark. */
|
|
7
|
+
declare const ENTRY_STATE_MAX_BYTES = 4096;
|
|
8
|
+
interface ArrivedNavigation {
|
|
9
|
+
/** The scratch the leaving app left on this entry, if any. */
|
|
10
|
+
readonly state: Readonly<Record<string, unknown>> | undefined;
|
|
11
|
+
/** How this entry was reached. */
|
|
12
|
+
readonly direction: NavigationDirection;
|
|
13
|
+
}
|
|
14
|
+
type Collector = () => unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Queue a value for the entry being left. The last call before a navigation wins.
|
|
17
|
+
* Prefer {@link registerEntryStateCollector} for values that are only knowable at the
|
|
18
|
+
* instant of navigating (a scroll offset is the motivating case).
|
|
19
|
+
*/
|
|
20
|
+
declare const saveEntryState: (key: string, value: unknown) => void;
|
|
21
|
+
/** Register a callback asked for its value at navigation time. Returns its remover.
|
|
22
|
+
* A second registration for the same key replaces the first — one owner per key. */
|
|
23
|
+
declare const registerEntryStateCollector: (key: string, collect: Collector) => (() => void);
|
|
24
|
+
/**
|
|
25
|
+
* Gather the scratch for the entry being left and clear the queue. Called by
|
|
26
|
+
* `navigate()` — not part of the app-facing surface.
|
|
27
|
+
*
|
|
28
|
+
* A collector that throws is skipped: a bookmark is a convenience, and it must never
|
|
29
|
+
* be able to break a navigation.
|
|
30
|
+
*/
|
|
31
|
+
declare const takeQueuedEntryState: () => Record<string, unknown> | undefined;
|
|
32
|
+
/** Record what the host said about the entry just arrived at. Called by the boot
|
|
33
|
+
* shell's `urlchange` listener — not part of the app-facing surface. */
|
|
34
|
+
declare const receiveNavigation: (next: ArrivedNavigation) => void;
|
|
35
|
+
/** The current arrival, as one stable object so `useSyncExternalStore` can compare
|
|
36
|
+
* by identity. */
|
|
37
|
+
declare const getArrivedNavigation: () => ArrivedNavigation;
|
|
38
|
+
declare const subscribeNavigation: (listener: () => void) => (() => void);
|
|
39
|
+
/** Test seam: forget every collector, queued value and arrival. */
|
|
40
|
+
declare const resetEntryState: () => void;
|
|
41
|
+
|
|
42
|
+
export { type ArrivedNavigation, ENTRY_STATE_MAX_BYTES, type NavigationDirection, getArrivedNavigation, receiveNavigation, registerEntryStateCollector, resetEntryState, saveEntryState, subscribeNavigation, takeQueuedEntryState };
|