@tinvar/react 0.0.0-stage → 0.4.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +168 -4
  3. package/src/action-server.js +41 -0
  4. package/src/action.d.ts +68 -0
  5. package/src/action.js +167 -0
  6. package/src/bootstrap.js +153 -0
  7. package/src/channel.d.ts +46 -0
  8. package/src/channel.js +132 -0
  9. package/src/client-modules.d.ts +8 -0
  10. package/src/client-modules.js +88 -0
  11. package/src/client.d.ts +36 -0
  12. package/src/client.js +42 -0
  13. package/src/compose-route.d.ts +33 -0
  14. package/src/compose-route.js +107 -0
  15. package/src/csrf.d.ts +11 -0
  16. package/src/csrf.js +49 -0
  17. package/src/deferred-boundary.d.ts +61 -0
  18. package/src/deferred-boundary.js +646 -0
  19. package/src/deferred-groups.d.ts +24 -0
  20. package/src/deferred-groups.js +198 -0
  21. package/src/deferred-revalidation.d.ts +37 -0
  22. package/src/deferred-revalidation.js +108 -0
  23. package/src/deferred-triggers.d.ts +26 -0
  24. package/src/deferred-triggers.js +172 -0
  25. package/src/deferred.d.ts +24 -0
  26. package/src/deferred.jsx +100 -0
  27. package/src/document-load.js +95 -0
  28. package/src/endpoint.d.ts +87 -0
  29. package/src/endpoint.js +304 -0
  30. package/src/form-data.js +219 -0
  31. package/src/form.d.ts +177 -0
  32. package/src/form.js +615 -0
  33. package/src/forms.d.ts +4 -0
  34. package/src/forms.js +10 -0
  35. package/src/fragment-client.d.ts +70 -0
  36. package/src/fragment-client.js +510 -0
  37. package/src/head.d.ts +85 -0
  38. package/src/head.js +360 -0
  39. package/src/inbox-view.d.ts +17 -0
  40. package/src/inbox-view.js +105 -0
  41. package/src/inbox.d.ts +108 -0
  42. package/src/inbox.js +293 -0
  43. package/src/index.combined.d.ts +31 -0
  44. package/src/index.d.ts +5 -0
  45. package/src/index.js +29 -0
  46. package/src/index.server.d.ts +4 -0
  47. package/src/index.server.js +8 -0
  48. package/src/infinite-scroll.d.ts +108 -0
  49. package/src/infinite-scroll.js +715 -0
  50. package/src/layout-props.d.ts +30 -0
  51. package/src/layout-props.js +143 -0
  52. package/src/link-click.d.ts +3 -0
  53. package/src/link-click.js +68 -0
  54. package/src/link.d.ts +63 -0
  55. package/src/link.js +274 -0
  56. package/src/live.d.ts +65 -0
  57. package/src/live.js +977 -0
  58. package/src/merge-slot.d.ts +41 -0
  59. package/src/merge-slot.js +253 -0
  60. package/src/merge.d.ts +111 -0
  61. package/src/merge.js +149 -0
  62. package/src/once-slot.d.ts +14 -0
  63. package/src/once-slot.js +75 -0
  64. package/src/once-store.d.ts +26 -0
  65. package/src/once-store.js +211 -0
  66. package/src/once.d.ts +44 -0
  67. package/src/once.js +65 -0
  68. package/src/optimistic.d.ts +14 -0
  69. package/src/optimistic.js +159 -0
  70. package/src/optional.d.ts +10 -0
  71. package/src/optional.js +31 -0
  72. package/src/page-root.d.ts +56 -0
  73. package/src/page-root.js +455 -0
  74. package/src/payload.d.ts +15 -0
  75. package/src/payload.js +101 -0
  76. package/src/poll.d.ts +55 -0
  77. package/src/poll.js +224 -0
  78. package/src/precognition.js +190 -0
  79. package/src/prefetch-cache.d.ts +46 -0
  80. package/src/prefetch-cache.js +256 -0
  81. package/src/progress.d.ts +38 -0
  82. package/src/progress.js +243 -0
  83. package/src/remember-store.d.ts +50 -0
  84. package/src/remember-store.js +486 -0
  85. package/src/rescue.d.ts +10 -0
  86. package/src/rescue.js +30 -0
  87. package/src/resumable.js +822 -0
  88. package/src/resume-store.js +364 -0
  89. package/src/route.d.ts +50 -0
  90. package/src/route.js +227 -0
  91. package/src/router-hooks.js +234 -0
  92. package/src/router.d.ts +586 -0
  93. package/src/router.js +3555 -0
  94. package/src/segment.d.ts +36 -0
  95. package/src/segment.js +116 -0
  96. package/src/server.d.ts +71 -0
  97. package/src/server.js +242 -0
  98. package/src/submit.js +333 -0
  99. package/src/telemetry.d.ts +71 -0
  100. package/src/telemetry.js +851 -0
  101. package/src/testing.d.ts +89 -0
  102. package/src/testing.js +634 -0
  103. package/src/transfer.d.ts +48 -0
  104. package/src/transfer.js +135 -0
  105. package/src/transport.js +390 -0
  106. package/src/upload.d.ts +342 -0
  107. package/src/upload.js +1240 -0
  108. package/src/use-channel.d.ts +21 -0
  109. package/src/use-channel.js +51 -0
  110. package/src/use-form.d.ts +86 -0
  111. package/src/use-form.js +432 -0
  112. package/src/use-http.d.ts +56 -0
  113. package/src/use-http.js +248 -0
  114. package/src/use-inbox.d.ts +41 -0
  115. package/src/use-inbox.js +201 -0
  116. package/src/use-remember.d.ts +10 -0
  117. package/src/use-remember.js +132 -0
  118. package/src/when-mounted.d.ts +15 -0
  119. package/src/when-mounted.js +27 -0
  120. package/README.md +0 -3
@@ -0,0 +1,36 @@
1
+ import type { Context, ReactNode } from 'react';
2
+ import type { LayoutPropsStore } from './layout-props';
3
+
4
+ export interface SegmentStore {
5
+ /** What partial reloads have put in each segment since the page was last loaded whole. */
6
+ overrides: Readonly<Record<string, ReactNode>>;
7
+ /** What each segment last committed. */
8
+ recorded: Map<string, ReactNode>;
9
+ /** The whole pages the root has shown. */
10
+ generation?: number;
11
+ /** By segment, the times it was committed fresh: its content is keyed by this. */
12
+ keys?: Readonly<Record<string, number>>;
13
+ /** The instant visit showing, if one is: the layout whose loading boundary stands in below it. */
14
+ instant?: { under: string; token: object } | null;
15
+ /** The layouts on screen that have a loading boundary, each naming the segment it stands in for. */
16
+ boundaries?: Map<string, string>;
17
+ /** What pages hand their layouts with setLayoutProps(). */
18
+ layoutProps?: LayoutPropsStore;
19
+ }
20
+
21
+ export declare const SegmentContext: Context<SegmentStore | null>;
22
+
23
+ /** One route segment -- a layout or the page -- as a slot a partial reload can refill. */
24
+ export declare function Segment(props: {
25
+ id: string;
26
+ /** The layout this segment sits in, when that layout gave a loading boundary. */
27
+ parent?: string;
28
+ /** That layout's loading boundary, shown here during an instant visit under it (ADR 0092). */
29
+ loading?: ReactNode;
30
+ children?: ReactNode;
31
+ }): ReactNode;
32
+
33
+ /** How many segments deep a component sits: 0 outside them, the most in the page. */
34
+ export declare const SegmentDepthContext: Context<number>;
35
+
36
+ export declare function useSegmentDepth(): number;
package/src/segment.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ // One route segment -- a layout or the page -- as a slot a partial reload
4
+ // can refill.
5
+ //
6
+ // ADR 0015: a partial reload re-renders only the segments it recomputes, and
7
+ // the browser puts each into the page it already holds. Server components
8
+ // are rendered away by the time a payload arrives, so the page on screen has
9
+ // no layout or page left to find and replace; what it has is this client
10
+ // component around each segment, which the server renders with the
11
+ // segment's content as its children. A partial payload's segments go to the
12
+ // page root, which hands them down here in place of those children.
13
+ //
14
+ // A layout re-rendered alone has its child segment as an empty slot -- the
15
+ // server did not recompute it, so it has nothing to put there -- and the
16
+ // slot shows what it last showed. Each segment records what reached the
17
+ // screen as it commits, which is what makes that possible.
18
+ //
19
+ // A layout given a loading boundary (ADR 0092) hands it to the slot its
20
+ // child sits in, rendered by the server with the rest of the page. An
21
+ // instant visit under that layout shows it there while the next page is
22
+ // fetched: the layout as it is on screen, drawn from data it was already
23
+ // given, and the loading boundary in place of what is below it. The next
24
+ // page itself is never drawn before its data arrives.
25
+
26
+ import { createContext, createElement, useContext, useLayoutEffect } from 'react';
27
+
28
+ /**
29
+ * What the page root hands every segment: the content a partial reload put
30
+ * in each, by segment, and what each last committed.
31
+ *
32
+ * @typedef {{
33
+ * overrides: Readonly<Record<string, unknown>>,
34
+ * recorded: Map<string, unknown>,
35
+ * generation?: number,
36
+ * keys?: Readonly<Record<string, number>>,
37
+ * instant?: { under: string, token: object } | null,
38
+ * boundaries?: Map<string, string>,
39
+ * layoutProps?: import('./layout-props.js').LayoutPropsStore,
40
+ * }} SegmentStore
41
+ *
42
+ * `generation` counts the whole pages the root has shown, for what must
43
+ * start again on one but not on a partial reload (<MergeSlot>).
44
+ *
45
+ * `keys` counts, by segment, the times the root committed it fresh -- a
46
+ * visit that does not preserve state (ADR 0045). The segment's content is
47
+ * keyed by it, so React mounts it again instead of reconciling it with
48
+ * what it replaces.
49
+ *
50
+ * `instant` is the instant visit showing, if one is: the layout under which
51
+ * its loading boundary stands in for what the layout holds. `boundaries`
52
+ * is which layouts on screen have one, by the layout's key, each naming the
53
+ * segment its boundary stands in for; the page root reads it to tell
54
+ * whether an instant visit has anything to show.
55
+ *
56
+ * `layoutProps` is what pages hand their layouts with setLayoutProps().
57
+ */
58
+
59
+ export const SegmentContext = createContext(/** @type {SegmentStore | null} */ (null));
60
+
61
+ /**
62
+ * How many segments deep a component sits: 0 in the document outside them,
63
+ * 1 in the outermost layout, the most in the page. What the head tags read,
64
+ * so a page's <Title> wins over its layout's whatever order they render in.
65
+ */
66
+ export const SegmentDepthContext = createContext(0);
67
+
68
+ /** @returns {number} see SegmentDepthContext */
69
+ export function useSegmentDepth() {
70
+ return useContext(SegmentDepthContext);
71
+ }
72
+
73
+ /**
74
+ * @param {{ id: string, parent?: string, loading?: unknown, children?: unknown }} props
75
+ * `parent` and `loading`: the layout this segment sits in, and the loading
76
+ * boundary it gave for an instant visit under it -- both or neither
77
+ */
78
+ export function Segment({ id, parent, loading, children }) {
79
+ const store = useContext(SegmentContext);
80
+ const boundary = parent !== undefined && loading !== undefined;
81
+
82
+ // An instant visit under this segment's layout: its loading boundary,
83
+ // until the page arrives or the visit is given up.
84
+ const pending = boundary && store?.instant?.under === parent;
85
+
86
+ // Outside a page root -- the server render that makes the HTML -- a
87
+ // segment is exactly its children.
88
+ const content = pending
89
+ ? loading
90
+ : store !== null && Object.hasOwn(store.overrides, id)
91
+ ? store.overrides[id]
92
+ : children !== undefined
93
+ ? children
94
+ : (store?.recorded.get(id) ?? null);
95
+
96
+ // What reached the screen is the page's, never the stand-in: a layout
97
+ // re-rendered alone later puts back what was here before.
98
+ useLayoutEffect(() => {
99
+ if (!pending) store?.recorded.set(id, content);
100
+ });
101
+
102
+ const boundaries = store?.boundaries;
103
+ useLayoutEffect(() => {
104
+ if (!boundary || boundaries === undefined || parent === undefined) return undefined;
105
+ boundaries.set(parent, id);
106
+ return () => {
107
+ if (boundaries.get(parent) === id) boundaries.delete(parent);
108
+ };
109
+ }, [boundary, boundaries, parent, id]);
110
+
111
+ return createElement(
112
+ SegmentDepthContext.Provider,
113
+ { key: store?.keys?.[id] ?? 0, value: useSegmentDepth() + 1 },
114
+ content,
115
+ );
116
+ }
@@ -0,0 +1,71 @@
1
+ /** Types for the server helpers components import. */
2
+
3
+ export interface QueryClientLike {
4
+ /**
5
+ * A trailing call leaves with the next one that is not, or alone once the
6
+ * event loop turns. `by` names the component making the call and its
7
+ * owners, in development only, for the dev toolbar.
8
+ */
9
+ call(
10
+ name: string,
11
+ args: Record<string, unknown>,
12
+ options?: { trailing?: boolean; by?: string[] },
13
+ ): Promise<unknown>;
14
+ }
15
+
16
+ export interface MintClientLike {
17
+ mint(component: string, props: Record<string, unknown>): Promise<string>;
18
+ }
19
+
20
+ export interface RenderContext {
21
+ requestId: string;
22
+ nonce: string;
23
+ allowed: string[];
24
+ client: QueryClientLike;
25
+ /** Absent in a fragment render: a deferred boundary does not nest. */
26
+ mints?: MintClientLike;
27
+ }
28
+
29
+ export declare function runInRenderContext<T>(context: RenderContext, fn: () => T): T;
30
+ export declare function getRenderContext(): RenderContext;
31
+
32
+ /** The per-request CSP nonce PHP minted, for any inline script the app emits. */
33
+ export declare function nonce(): string;
34
+ export declare function requestId(): string;
35
+
36
+ /**
37
+ * Calls a PHP #[Query] over the request's own socket.
38
+ *
39
+ * Calls made in the same microtask tick leave as one frame; identical calls
40
+ * collapse into one.
41
+ */
42
+ export declare function query<T = unknown>(name: string, args?: Record<string, unknown>): Promise<T>;
43
+
44
+ /**
45
+ * Every key the application shares, with its type. Empty here; `tinvar
46
+ * codegen` fills it in, from the PHP `#[Shared]` methods, by augmenting this
47
+ * module in the generated types.
48
+ */
49
+ export interface SharedDataMap {}
50
+
51
+ /**
52
+ * Reads a value every page shares: a PHP `#[Shared]` method, run once for
53
+ * this render the first time any component asks. Server components only.
54
+ */
55
+ export declare function shared<K extends keyof SharedDataMap & string>(key: K): Promise<SharedDataMap[K]>;
56
+
57
+ /**
58
+ * The components a <Deferred> boundary may name, as the rsc entry keeps them.
59
+ * Returns them frozen; the build reads the call, so each value must be a
60
+ * component imported from its own module, under a name written out.
61
+ */
62
+ export declare function deferredRegistry<T extends Record<string, unknown>>(components: T): Readonly<T>;
63
+
64
+ /**
65
+ * Asks PHP to sign a fragment token for one <Deferred> boundary. The renderer
66
+ * holds no key; see ADR 0009.
67
+ */
68
+ export declare function mintFragmentToken(
69
+ component: string,
70
+ props?: Record<string, unknown>,
71
+ ): Promise<string>;
package/src/server.js ADDED
@@ -0,0 +1,242 @@
1
+ // Server-side helpers for Tinvar components.
2
+ //
3
+ // This module is bundled into the `rsc` environment alongside the app's server
4
+ // components, so the AsyncLocalStorage instance here is the same one the
5
+ // components see. The renderer process never touches it directly; it hands the
6
+ // query client to the rsc entry, which opens the context.
7
+
8
+ import { AsyncLocalStorage } from 'node:async_hooks';
9
+ import * as React from 'react';
10
+
11
+ /** @typedef {{ requestId: string, nonce: string, allowed: string[], client: QueryClientLike, mints?: MintClientLike }} RenderContext */
12
+ /** @typedef {{ mint(component: string, props: Record<string, unknown>): Promise<string> }} MintClientLike */
13
+ /** @typedef {{ call(name: string, args: Record<string, unknown>, options?: { trailing?: boolean, by?: string[] }): Promise<unknown> }} QueryClientLike */
14
+
15
+ const storage = new AsyncLocalStorage();
16
+
17
+ /**
18
+ * "Each render runs in its own AsyncLocalStorage context. The renderer never
19
+ * caches query results across requests."
20
+ *
21
+ * @template T
22
+ * @param {RenderContext} context
23
+ * @param {() => T} fn
24
+ * @returns {T}
25
+ */
26
+ export function runInRenderContext(context, fn) {
27
+ return storage.run(context, fn);
28
+ }
29
+
30
+ /** @returns {RenderContext} */
31
+ export function getRenderContext() {
32
+ const context = storage.getStore();
33
+ if (!context) {
34
+ throw new Error(
35
+ 'No Tinvar render context. A query was called outside a render, or from a client component.',
36
+ );
37
+ }
38
+ return context;
39
+ }
40
+
41
+ /** The per-request CSP nonce PHP minted, for any inline script the app emits. */
42
+ export function nonce() {
43
+ return getRenderContext().nonce;
44
+ }
45
+
46
+ export function requestId() {
47
+ return getRenderContext().requestId;
48
+ }
49
+
50
+ /**
51
+ * Calls a PHP #[Query] over the request's own socket.
52
+ *
53
+ * Generated `@queries/*` functions wrap this. Calls made in the same microtask
54
+ * tick leave as one frame; identical calls collapse into one.
55
+ *
56
+ * @param {string} name
57
+ * @param {Record<string, unknown>} [args]
58
+ * @returns {Promise<unknown>}
59
+ */
60
+ export function query(name, args = {}) {
61
+ const context = getRenderContext();
62
+
63
+ // The allowlist is enforced in PHP, which is what actually matters. Failing
64
+ // here too turns a silent server-side rejection into an error the component
65
+ // author sees during development. An empty list allows nothing, as it does
66
+ // in PHP: read as "no list", it let through every call PHP then refused.
67
+ if (!context.allowed.includes(name)) {
68
+ throw new Error(
69
+ `Query '${name}' is not on this route's allowlist. ` +
70
+ `Allowed here: ${context.allowed.join(', ') || '(none)'}.`,
71
+ );
72
+ }
73
+
74
+ const by = callers();
75
+ return by === undefined ? context.client.call(name, args) : context.client.call(name, args, { by });
76
+ }
77
+
78
+ /** What a shared key's call is named: no query's name can start with `$`. */
79
+ const SHARED_PREFIX = '$shared.';
80
+
81
+ /** A shared key is an identifier, as PHP's registry refuses any other. */
82
+ const SHARED_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
83
+
84
+ /**
85
+ * Reads a value every page shares: a PHP `#[Shared]` method, run once for
86
+ * this render the first time any component asks (ADR 0060).
87
+ *
88
+ * It travels as a query call does -- in the same tick's frame, answered
89
+ * beside the queries and held to the same budget -- under a name no query
90
+ * can have. It is on no allowlist: PHP answers only the keys of the page's
91
+ * own panel and of none, and an unknown key fails as an unknown query does.
92
+ * Codegen types it, through the `SharedDataMap` it declares for this module.
93
+ *
94
+ * @param {string} key
95
+ * @returns {Promise<unknown>}
96
+ */
97
+ export function shared(key) {
98
+ if (typeof key !== 'string' || !SHARED_KEY.test(key)) {
99
+ throw new Error(`'${String(key)}' is not a shared key: a key is an identifier.`);
100
+ }
101
+
102
+ const by = callers();
103
+ return getRenderContext().client.call(
104
+ `${SHARED_PREFIX}${key}`,
105
+ {},
106
+ by === undefined ? { trailing: true } : { trailing: true, by },
107
+ );
108
+ }
109
+
110
+ /** Names kept of a call's callers: the component, and the owners above it. */
111
+ const MAX_CALLERS = 8;
112
+
113
+ /** What a component's name looks like in a stack: a capital, then an identifier. */
114
+ const COMPONENT = /^[A-Z][A-Za-z0-9_$]*$/;
115
+
116
+ /**
117
+ * The component making a call, then the components that rendered it, for
118
+ * the dev toolbar's tick panel: "tick 3: 1 query for `site`, 20 racks, from
119
+ * `Stats`, `Header`" (tinvar-spec.md, "Observability (dev toolbar)").
120
+ *
121
+ * React's owner stack names the owners -- the components whose render
122
+ * created each element on the way down -- but not the component rendering
123
+ * now, which is the frame that called this, found in the call's own stack:
124
+ * a server component's function is on it, whether it has awaited yet or not.
125
+ *
126
+ * In development only, and closed twice over: nothing in production, where
127
+ * React's captureOwnerStack() answers null, and nothing where NODE_ENV says
128
+ * production, whichever React is loaded. The names travel beside the call,
129
+ * which PHP reads without them (Frame::readQueryBatch()); only the toolbar's
130
+ * recording keeps them.
131
+ *
132
+ * @returns {string[] | undefined}
133
+ */
134
+ function callers() {
135
+ if (process.env.NODE_ENV === 'production') return undefined;
136
+ const capture = /** @type {(() => string | null) | undefined} */ (
137
+ /** @type {Record<string, unknown>} */ (React).captureOwnerStack
138
+ );
139
+ const owners = typeof capture === 'function' ? capture() : null;
140
+ if (typeof owners !== 'string') return undefined;
141
+
142
+ /** @type {string[]} */
143
+ const names = [];
144
+ const own = framesOf(new Error().stack ?? '').find((name) => COMPONENT.test(name));
145
+ if (own !== undefined) names.push(own);
146
+ for (const name of framesOf(owners)) {
147
+ if (COMPONENT.test(name) && names.at(-1) !== name) names.push(name);
148
+ }
149
+ return names.length > 0 ? names.slice(0, MAX_CALLERS) : undefined;
150
+ }
151
+
152
+ /**
153
+ * The function names of a V8 stack's frames, as far as React's own frame
154
+ * below the components: `at async Stats (...)` is `Stats`, and
155
+ * `at Object.Page (...)` is `Page`.
156
+ *
157
+ * @param {string} stack
158
+ * @returns {string[]}
159
+ */
160
+ function framesOf(stack) {
161
+ const names = [];
162
+ for (const line of stack.split('\n')) {
163
+ const match = /^\s*at (?:async )?([^\s(]+)/.exec(line);
164
+ if (match === null) continue;
165
+ if (match[1].includes('react_stack_bottom_frame')) break;
166
+ names.push(match[1].slice(match[1].lastIndexOf('.') + 1));
167
+ }
168
+ return names;
169
+ }
170
+
171
+ /**
172
+ * The components a <Deferred> boundary may name, as the rsc entry keeps them:
173
+ * a fragment renders the one its token names, from here.
174
+ *
175
+ * It returns what it is given, frozen; what it adds is that the build can
176
+ * read it. A fragment renders under the query allowlist of the route that
177
+ * minted its token, and the build makes that list from what the route's
178
+ * views reach -- which a component named by a string is not among. So
179
+ * `deferredBoundaries()` in @tinvar/renderer/vite reads each call to this:
180
+ * every name, and the module its component is imported from, whose queries
181
+ * go on the lists of the routes that may name it. Each value must be a
182
+ * component imported from its own module, under a name written out;
183
+ * anything else fails the build.
184
+ *
185
+ * @template {Record<string, unknown>} T
186
+ * @param {T} components
187
+ * @returns {Readonly<T>}
188
+ */
189
+ export function deferredRegistry(components) {
190
+ return Object.freeze({ ...components });
191
+ }
192
+
193
+ /**
194
+ * Asks PHP to sign a fragment token for one <Deferred> boundary.
195
+ *
196
+ * The renderer holds no key. It knows which boundaries exist and what props
197
+ * they were given -- facts that exist only while this tree renders -- so it
198
+ * asks, and PHP chooses the route, params, panel, build, session and expiry
199
+ * itself. ADR 0009 records why it is this way round and what it costs.
200
+ *
201
+ * @param {string} component
202
+ * @param {Record<string, unknown>} [props]
203
+ * @returns {Promise<string>}
204
+ */
205
+ export function mintFragmentToken(component, props = {}) {
206
+ const context = getRenderContext();
207
+
208
+ warnAboutSecretsInProps(component, props);
209
+
210
+ if (!context.mints) {
211
+ throw new Error(
212
+ `<Deferred> cannot be used here: this render was opened without the ability to mint ` +
213
+ `fragment tokens. A page render supports deferred boundaries; a fragment render does not ` +
214
+ `nest them.`,
215
+ );
216
+ }
217
+
218
+ return context.mints.mint(component, props);
219
+ }
220
+
221
+ /**
222
+ * Props travel base64, not encrypted, so a secret in them is public.
223
+ *
224
+ * Development only, and only for names that are unambiguous -- a false
225
+ * positive on a prop called `token` is cheap, and the alternative is a
226
+ * developer discovering it from page source.
227
+ */
228
+ const SUSPICIOUS_PROP = /(secret|password|passwd|apikey|api_key|private_key|credential)/i;
229
+
230
+ /** @param {string} component @param {Record<string, unknown>} props */
231
+ function warnAboutSecretsInProps(component, props) {
232
+ if (process.env.NODE_ENV === 'production') return;
233
+
234
+ for (const name of Object.keys(props)) {
235
+ if (SUSPICIOUS_PROP.test(name)) {
236
+ console.warn(
237
+ `[tinvar] <Deferred component="${component}"> has a prop named "${name}". ` +
238
+ `Fragment token props are signed, NOT encrypted -- they are readable from page source.`,
239
+ );
240
+ }
241
+ }
242
+ }