@ultimat3/render 1.2.0 → 2.0.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/CLAUDE.md +74 -0
- package/README.md +179 -11
- package/package.json +7 -4
- package/src/css-modules.ts +138 -0
- package/src/errors.ts +98 -0
- package/src/head.ts +49 -9
- package/src/html.ts +141 -0
- package/src/hydrate.ts +20 -4
- package/src/index.ts +57 -1
- package/src/island-collector.ts +148 -0
- package/src/island-props.ts +144 -0
- package/src/island.ts +218 -0
- package/src/islands.ts +8 -1
- package/src/jsx.ts +46 -0
- package/src/modes.ts +28 -3
- package/src/module-loader.ts +144 -0
- package/src/registry.ts +77 -11
- package/src/render-html.ts +149 -0
- package/src/render-isr.ts +64 -4
- package/src/render-stream.ts +80 -17
- package/src/route-component.ts +36 -0
- package/src/route-data.ts +59 -0
- package/src/route.ts +137 -15
- package/src/surfaces.ts +22 -3
- package/src/type-pins.tsx +101 -0
package/src/route.ts
CHANGED
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
* offline strategy, hydration timing and metadata, and it hands back a descriptor that is
|
|
4
4
|
* already normalized: `meta` always awaits, `budget` is always there.
|
|
5
5
|
*
|
|
6
|
-
* `offline
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `offline` and `meta` are REQUIRED BY THE TYPE. That is axiom 3 — enforced, not documented —
|
|
7
|
+
* expressed in the type system: a route that forgets its offline strategy or its `<head>` is a
|
|
8
|
+
* compile error, not a checklist item nobody reads.
|
|
9
|
+
*
|
|
10
|
+
* `hydrate` is not on that list, since 1.2.0: it is the one key the framework can work out from
|
|
11
|
+
* the page's own declarations, and requiring a value it already knows is not enforcement, it is a
|
|
12
|
+
* second place to get one thing wrong.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import type { CacheTag } from '@ultimat3/cache';
|
|
12
16
|
import { serializeTags } from '@ultimat3/cache';
|
|
17
|
+
import type { Translator } from '@ultimat3/i18n';
|
|
13
18
|
import type { RouteMeta } from '@ultimat3/seo';
|
|
14
|
-
import { RouteMetaMissingError, RouteOfflineMissingError } from './errors';
|
|
19
|
+
import { RouteLoadInvalidError, RouteMetaMissingError, RouteOfflineMissingError } from './errors';
|
|
20
|
+
import type { IslandSpec } from './island';
|
|
21
|
+
import { drainDeclaredIslands } from './island';
|
|
15
22
|
import { assertModeShape } from './modes';
|
|
16
23
|
|
|
17
24
|
export type RenderMode = 'static' | 'isr' | 'ssr' | 'stream' | 'spa';
|
|
@@ -21,6 +28,15 @@ export type HydrateStrategy = 'idle' | 'visible' | 'interaction' | 'never';
|
|
|
21
28
|
export const OFFLINE_STRATEGIES = ['precache', 'runtime', 'network-only'] as const;
|
|
22
29
|
export const HYDRATE_STRATEGIES = ['idle', 'visible', 'interaction', 'never'] as const;
|
|
23
30
|
|
|
31
|
+
/**
|
|
32
|
+
* What a page that declares an island hydrates as when it says nothing. The most conservative of
|
|
33
|
+
* the three that ship JavaScript: nothing runs until the visitor acts, and `interaction` is the
|
|
34
|
+
* only one that also replays the event that woke the island, so the first click is answered rather
|
|
35
|
+
* than swallowed. An island wanting `visible` (an infinite scroll) still says so once — on the
|
|
36
|
+
* route, the same key that overrides this one, never a second declaration on the island itself.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_ISLAND_HYDRATE: HydrateStrategy = 'interaction';
|
|
39
|
+
|
|
24
40
|
export type RouteParams = Readonly<Record<string, string>>;
|
|
25
41
|
export type RouteData = Readonly<Record<string, unknown>>;
|
|
26
42
|
|
|
@@ -50,25 +66,102 @@ export interface RouteGuard {
|
|
|
50
66
|
readonly permission: string;
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/**
|
|
70
|
+
* What a loader and a `meta` function are told about the request. `url` is a string because that
|
|
71
|
+
* is what `ld.*` embeds and what `meta` already received before `load` existed.
|
|
72
|
+
*
|
|
73
|
+
* An alias rather than an `interface`, deliberately: absent a `load` this object IS the route's
|
|
74
|
+
* data, and only an alias carries the implicit index signature that makes it a `RouteData`. As an
|
|
75
|
+
* interface the compiler could not see that, so the one place it mattered — `routeDataFor`'s
|
|
76
|
+
* fallback — laundered it through `as unknown as TData` and checked nothing at all.
|
|
77
|
+
*/
|
|
78
|
+
export type RouteContext = {
|
|
79
|
+
readonly params: RouteParams;
|
|
80
|
+
readonly url: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The route's server-side data, resolved ONCE per render and handed to both `meta` and the page.
|
|
85
|
+
*
|
|
86
|
+
* This is not a ninth concern bolted onto the contract — it completes the one `meta` already
|
|
87
|
+
* declares. `RouteMetaFn<TData>` has always taken a `TData`, and until this existed nothing could
|
|
88
|
+
* ever supply one richer than `{ url, params }`: the page component was passed the same two
|
|
89
|
+
* fields, so a page that fetched its own data could not share it with `meta`. The consequence was
|
|
90
|
+
* silent and severe — a `site/` route's `<title>` and `description` could never reflect its
|
|
91
|
+
* content, on the surface whose entire purpose is SEO, and a page written against `props.data`
|
|
92
|
+
* rendered an empty list with a 200 rather than an error.
|
|
93
|
+
*
|
|
94
|
+
* Sync or async, whichever the page's data needs.
|
|
95
|
+
*/
|
|
96
|
+
export type RouteLoadFn<TData = RouteData> = (ctx: RouteContext) => TData | Promise<TData>;
|
|
97
|
+
|
|
98
|
+
/** What the descriptor hands back. One shape, so no consumer branches on a thenable. */
|
|
99
|
+
export type RouteLoadAsyncFn<TData = RouteData> = (ctx: RouteContext) => Promise<TData>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What `meta` is given: the loaded data, the request, and the translator.
|
|
103
|
+
*
|
|
104
|
+
* A context rather than the bare data, because a `<title>` needs all three — the data for the
|
|
105
|
+
* content, `url` for the canonical, and `t` because no user-facing string may be hardcoded. It is
|
|
106
|
+
* a strict SUPERSET of what `meta` received before `load` existed (`{ params, url }`), and both
|
|
107
|
+
* keys keep their names, so every route that reads `data.url` or `data.params` keeps working.
|
|
108
|
+
*/
|
|
109
|
+
export interface RouteMetaContext<TData = RouteData> {
|
|
110
|
+
readonly data: TData;
|
|
111
|
+
readonly params: RouteParams;
|
|
112
|
+
readonly url: string;
|
|
113
|
+
/** The request's own translator. Never a hardcoded string in a `<title>`. */
|
|
114
|
+
readonly t: Translator;
|
|
115
|
+
}
|
|
116
|
+
|
|
53
117
|
/** What an author writes. Sync or async, whichever the page's data needs. */
|
|
54
|
-
export type RouteMetaFn<TData = RouteData> = (
|
|
118
|
+
export type RouteMetaFn<TData = RouteData> = (
|
|
119
|
+
ctx: RouteMetaContext<TData>,
|
|
120
|
+
) => RouteMeta | Promise<RouteMeta>;
|
|
55
121
|
|
|
56
122
|
/** What the descriptor hands back. One shape, so no consumer branches on a thenable. */
|
|
57
|
-
export type RouteMetaAsyncFn<TData = RouteData> = (
|
|
123
|
+
export type RouteMetaAsyncFn<TData = RouteData> = (
|
|
124
|
+
ctx: RouteMetaContext<TData>,
|
|
125
|
+
) => Promise<RouteMeta>;
|
|
58
126
|
|
|
59
127
|
/** Returns the params to build at deploy time. Bare strings fill a single dynamic param. */
|
|
60
128
|
export type PrerenderFn = () =>
|
|
61
129
|
| readonly (string | RouteParams)[]
|
|
62
130
|
| Promise<readonly (string | RouteParams)[]>;
|
|
63
131
|
|
|
64
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* The rule that makes the no-`load` fallback true: a route that loads nothing renders
|
|
134
|
+
* `{ params, url }`, so its `meta` may only read what the context itself supplies. Any richer
|
|
135
|
+
* `TData` has to come from a loader, and this intersection is what says so — `nothing` when the
|
|
136
|
+
* context already satisfies `TData`, a required `load` when it does not.
|
|
137
|
+
*
|
|
138
|
+
* Enforced, not documented (axiom 3): a `meta` reading `data.post` off a route that declares no
|
|
139
|
+
* `load` is a compile error here, rather than `undefined` in a `<title>` on the surface whose
|
|
140
|
+
* entire purpose is SEO.
|
|
141
|
+
*/
|
|
142
|
+
export type LoadRequirement<TData> = RouteContext extends TData
|
|
143
|
+
? unknown
|
|
144
|
+
: { readonly load: RouteLoadFn<TData> };
|
|
145
|
+
|
|
146
|
+
/** The input shape of `defineRoute` — exactly the contract's nine keys, nothing else. */
|
|
65
147
|
export interface RouteDefinition<TData = RouteData> {
|
|
66
148
|
readonly render: RenderMode;
|
|
67
149
|
readonly revalidate?: RevalidateConfig;
|
|
68
150
|
readonly prerender?: PrerenderFn;
|
|
69
151
|
readonly offline: OfflineStrategy;
|
|
70
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Optional since 1.2.0, and derived when omitted: a page that declares an island hydrates
|
|
154
|
+
* (`DEFAULT_ISLAND_HYDRATE`), a page that declares none ships nothing (`'never'`). Stating it is
|
|
155
|
+
* still the one override, and still the only way to say `visible` or `idle`.
|
|
156
|
+
*
|
|
157
|
+
* It was required, and the two failures that made it worth deriving were both the framework
|
|
158
|
+
* asking for a value it could already work out: an island on a route still at `'never'` is
|
|
159
|
+
* `X_ISLAND_NOT_HYDRATED`, and a `site/` route off `'never'` with no `budget.js` is refused at
|
|
160
|
+
* registration. Two punishments for one omission the declaration above already answered.
|
|
161
|
+
*/
|
|
162
|
+
readonly hydrate?: HydrateStrategy;
|
|
71
163
|
readonly budget?: RouteBudget;
|
|
164
|
+
readonly load?: RouteLoadFn<TData>;
|
|
72
165
|
readonly meta: RouteMetaFn<TData>;
|
|
73
166
|
readonly policy?: RouteGuard;
|
|
74
167
|
}
|
|
@@ -76,15 +169,24 @@ export interface RouteDefinition<TData = RouteData> {
|
|
|
76
169
|
/**
|
|
77
170
|
* The frozen descriptor. `kind` lets the registry reject non-route exports.
|
|
78
171
|
*
|
|
79
|
-
*
|
|
80
|
-
* `meta` always returns a promise,
|
|
81
|
-
*
|
|
82
|
-
*
|
|
172
|
+
* Three fields are narrower here than in the declaration so every consumer reads one shape:
|
|
173
|
+
* `meta` always returns a promise, `budget` is always an object, and `hydrate` is always one of
|
|
174
|
+
* the four strategies — resolved, so nothing downstream repeats the derivation. `budget`'s
|
|
175
|
+
* *fields* stay optional; `budget.js === undefined` still means "no JS budget declared", which is
|
|
176
|
+
* what `registry.ts` fills in for an island route and `modes.ts` fails a hydrating `site/` route on.
|
|
83
177
|
*/
|
|
84
178
|
export interface RouteConfig<TData = RouteData> extends RouteDefinition<TData> {
|
|
85
179
|
readonly kind: 'route';
|
|
86
180
|
readonly meta: RouteMetaAsyncFn<TData>;
|
|
181
|
+
readonly load?: RouteLoadAsyncFn<TData>;
|
|
182
|
+
readonly hydrate: HydrateStrategy;
|
|
87
183
|
readonly budget: RouteBudget;
|
|
184
|
+
/**
|
|
185
|
+
* The islands this module declared, in declaration order. Never written by an author — drained
|
|
186
|
+
* from `island()`, and the reason `hydrate` and `budget.js` do not have to be. Also what finally
|
|
187
|
+
* populates `RouteEntry.islands`, which `routeJsBytes` has always read and nothing ever filled.
|
|
188
|
+
*/
|
|
189
|
+
readonly islands: readonly IslandSpec[];
|
|
88
190
|
}
|
|
89
191
|
|
|
90
192
|
/** What every render mode hands back to `@ultimat3/http`'s `html()` / `stream()`. */
|
|
@@ -106,9 +208,12 @@ export function tagKeys(tags: readonly CacheTag[] | undefined): readonly string[
|
|
|
106
208
|
* Declare a route. Validates the shape (for JS callers who bypass the types) and the
|
|
107
209
|
* mode-local invariants immediately, so a bad route fails at module evaluation — build
|
|
108
210
|
* time — rather than on the first request in production.
|
|
211
|
+
*
|
|
212
|
+
* `LoadRequirement` is the half the compiler owns: a `meta` reading data the context cannot
|
|
213
|
+
* supply forces a `load`, which is what lets `routeDataFor` hand the context back as the data.
|
|
109
214
|
*/
|
|
110
215
|
export function defineRoute<TData = RouteData>(
|
|
111
|
-
definition: RouteDefinition<TData>,
|
|
216
|
+
definition: RouteDefinition<TData> & LoadRequirement<TData>,
|
|
112
217
|
): RouteConfig<TData> {
|
|
113
218
|
const def = definition as Partial<RouteDefinition<TData>>;
|
|
114
219
|
|
|
@@ -131,19 +236,36 @@ export function defineRoute<TData = RouteData>(
|
|
|
131
236
|
);
|
|
132
237
|
}
|
|
133
238
|
|
|
239
|
+
if (def.load !== undefined && typeof def.load !== 'function') {
|
|
240
|
+
throw new RouteLoadInvalidError(
|
|
241
|
+
`load: ${JSON.stringify(def.load)} is not a function`,
|
|
242
|
+
'make load a function of ({ params, url }) returning the page data, or remove it',
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
134
246
|
const declaredMeta = def.meta;
|
|
247
|
+
const declaredLoad = def.load;
|
|
248
|
+
// Drained unconditionally, even when `hydrate` is stated: the list must not survive into the
|
|
249
|
+
// next route defined in this process, and `RouteEntry.islands` wants it either way.
|
|
250
|
+
const islands = drainDeclaredIslands();
|
|
135
251
|
const config: RouteConfig<TData> = {
|
|
136
252
|
kind: 'route',
|
|
137
253
|
render: def.render as RenderMode,
|
|
138
254
|
offline: def.offline,
|
|
139
|
-
|
|
255
|
+
islands,
|
|
256
|
+
// Declared wins, always — including `hydrate: 'never'` on a page that has an island, which is
|
|
257
|
+
// a contradiction an author stated on purpose and `X_ISLAND_NOT_HYDRATED` still refuses.
|
|
258
|
+
hydrate: def.hydrate ?? (islands.length > 0 ? DEFAULT_ISLAND_HYDRATE : 'never'),
|
|
140
259
|
// Wrapped rather than stored: the declaration may be sync, the descriptor never is.
|
|
141
260
|
// A meta that throws synchronously becomes a rejection here, so `await config.meta(d)`
|
|
142
261
|
// is the one way to fail as well as the one way to succeed.
|
|
143
|
-
meta: async (
|
|
262
|
+
meta: async (metaCtx: RouteMetaContext<TData>) => declaredMeta(metaCtx),
|
|
144
263
|
// Always an object. `budget.js` is the only reach a consumer needs, so an undeclared
|
|
145
264
|
// budget is `{}` instead of a second undefined-check at every call site.
|
|
146
265
|
budget: def.budget ?? {},
|
|
266
|
+
// Wrapped exactly as `meta` is, and for the same reason: the declaration may be sync, the
|
|
267
|
+
// descriptor never is, so `await config.load(ctx)` is the one way to fail as well as succeed.
|
|
268
|
+
...(declaredLoad ? { load: async (ctx: RouteContext) => declaredLoad(ctx) } : {}),
|
|
147
269
|
...(def.revalidate ? { revalidate: def.revalidate } : {}),
|
|
148
270
|
...(def.prerender ? { prerender: def.prerender } : {}),
|
|
149
271
|
...(def.policy ? { policy: def.policy } : {}),
|
package/src/surfaces.ts
CHANGED
|
@@ -61,11 +61,30 @@ export const SURFACE_SPECS: Readonly<Record<Surface, SurfaceSpec>> = Object.free
|
|
|
61
61
|
|
|
62
62
|
const SURFACE_SEGMENT = /(?:^|\/)(site|app|api|shared)\//;
|
|
63
63
|
|
|
64
|
+
/** Where the surface segment is, and what follows it — the two halves of one match. */
|
|
65
|
+
export interface SurfaceLocation {
|
|
66
|
+
readonly surface: Surface;
|
|
67
|
+
/** Everything after `<surface>/`. `apps/myapp/app/dashboard/page.tsx` → `dashboard/page.tsx`. */
|
|
68
|
+
readonly rest: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The ONE reader of the surface segment, because the two readers disagreed: this regex is
|
|
73
|
+
* anchored on a path separator, and the route table's `indexOf('app/')` was not — so
|
|
74
|
+
* `apps/myapp/app/page.tsx` took its surface from here and its URL from the `app/` inside
|
|
75
|
+
* `myapp/`, and served every route in that app one segment too deep (`/app` instead of `/`).
|
|
76
|
+
*/
|
|
77
|
+
export function locateSurface(file: string): SurfaceLocation | null {
|
|
78
|
+
const normalized = normalize(file);
|
|
79
|
+
const match = SURFACE_SEGMENT.exec(normalized);
|
|
80
|
+
const found = match?.[1];
|
|
81
|
+
if (found === undefined || match === null) return null;
|
|
82
|
+
return { surface: found as Surface, rest: normalized.slice(match.index + match[0].length) };
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/** `apps/web/site/pricing/page.tsx` → `site`. Returns null for files outside a surface. */
|
|
65
86
|
export function surfaceOf(file: string): Surface | null {
|
|
66
|
-
|
|
67
|
-
const found = match?.[1];
|
|
68
|
-
return found === undefined ? null : (found as Surface);
|
|
87
|
+
return locateSurface(file)?.surface ?? null;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
function normalize(file: string): string {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Compile-time pins for the route's data seam and for island JSX. Source, not a `.test.ts`, on
|
|
2
|
+
// purpose: `tsconfig.json` excludes `src/**/*.test.ts`, so `tsc -b` never reads a test file and a
|
|
3
|
+
// type-level claim written in one can never fail. This module emits nothing and exports nothing
|
|
4
|
+
// anybody imports — a regression here is a build error, the only enforcement that counts.
|
|
5
|
+
//
|
|
6
|
+
// `.tsx`, not `.ts`, since 1.2.0: half of what is pinned here is only decidable by writing the JSX
|
|
7
|
+
// an author writes. `jsxImportSource` is `solid-js` in `tsconfig.base.json` and in both tracked
|
|
8
|
+
// apps, so the `<ContactSales />` below is checked against the SAME `JSX.Element` a page is —
|
|
9
|
+
// which is the only way a regression to `IslandNode` fails a build rather than a reader's review.
|
|
10
|
+
|
|
11
|
+
import type { IslandComponent } from './island';
|
|
12
|
+
import { island } from './island';
|
|
13
|
+
import type { JsonValue } from './island-props';
|
|
14
|
+
import type { LoadRequirement, RouteContext, RouteData, RouteDefinition } from './route';
|
|
15
|
+
|
|
16
|
+
/** Fails to compile when `T` is anything but `true`. The whole mechanism. */
|
|
17
|
+
type Assert<T extends true> = T;
|
|
18
|
+
|
|
19
|
+
type BlogPost = { readonly post: { readonly title: string } };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* What makes `routeDataFor`'s no-`load` branch sound. `RouteContext` has to BE a `RouteData` —
|
|
23
|
+
* an `interface` is not, because only an alias gets the implicit index signature, and declaring
|
|
24
|
+
* it as one is what turned that branch's `as unknown as TData` into a checked narrowing.
|
|
25
|
+
*/
|
|
26
|
+
export type _ContextIsRouteData = Assert<RouteContext extends RouteData ? true : false>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A route that loads nothing still declares nothing extra. `LoadRequirement` collapses to
|
|
30
|
+
* `unknown` for the default `TData`, so every route that shipped before `load` existed — and
|
|
31
|
+
* every route that only reads `data.url` / `data.params` — keeps compiling untouched.
|
|
32
|
+
*/
|
|
33
|
+
export type _DefaultDataNeedsNoLoad = Assert<
|
|
34
|
+
RouteDefinition extends RouteDefinition & LoadRequirement<RouteData> ? true : false
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* …and a `meta` reading a field the context cannot supply forces one. Pinned as the required
|
|
39
|
+
* `load` key rather than "does not compile", because that is the exact thing the intersection
|
|
40
|
+
* adds: without it, `defineRoute<BlogPost>({ meta: (c) => c.data.post.title })` type-checked and
|
|
41
|
+
* rendered `undefined` in a `<title>`.
|
|
42
|
+
*/
|
|
43
|
+
export type _RicherDataRequiresALoad = Assert<
|
|
44
|
+
undefined extends LoadRequirement<BlogPost>['load'] ? false : true
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
type ContactModal = IslandComponent<readonly ['subject']>;
|
|
48
|
+
type ContactModalProps = Parameters<ContactModal>[0];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The declared prop is required and typed, so the call site that forgets it does not compile.
|
|
52
|
+
* `props: ['subject']` is the whole declaration — there is no second place to state the shape.
|
|
53
|
+
*/
|
|
54
|
+
export type _DeclaredPropIsRequired = Assert<
|
|
55
|
+
ContactModalProps extends { readonly subject: JsonValue } ? true : false
|
|
56
|
+
>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An island's props are exactly what it declared. Anything else has no type here, which is what
|
|
60
|
+
* makes `<Modal {…row} />` a compile error before it is the runtime error that names the column.
|
|
61
|
+
*/
|
|
62
|
+
export type _UndeclaredPropHasNoType = Assert<
|
|
63
|
+
'passwordHash' extends keyof ContactModalProps ? false : true
|
|
64
|
+
>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The children are the server-rendered shell and never JSON: they stay `unknown`, so nothing in
|
|
68
|
+
* the type system suggests a component or a handler could travel to the browser inside them.
|
|
69
|
+
*/
|
|
70
|
+
export type _ChildrenAreNotJson = Assert<
|
|
71
|
+
ContactModalProps['children'] extends JsonValue | undefined ? false : true
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
/** A value the browser could never receive is not a `JsonValue`, so the prop never type-checks. */
|
|
75
|
+
export type _AHandleIsNotJson = Assert<(() => void) extends JsonValue ? false : true>;
|
|
76
|
+
|
|
77
|
+
const ContactSales: ContactModal = island({
|
|
78
|
+
src: './contact-sales.island.tsx',
|
|
79
|
+
props: ['subject'],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The pin that a `.ts` file cannot carry: an island used the way every author reaches for it.
|
|
84
|
+
*
|
|
85
|
+
* `island()` returned a plain object node until 1.2.0, and every `<ContactSales />` in an app was
|
|
86
|
+
* TS2786 — "its return type 'IslandNode' is not a valid JSX element" — while `h(ContactSales, …)`
|
|
87
|
+
* compiled, which is why the framework's own island tests never saw it. The configured
|
|
88
|
+
* `JSX.Element` is `solid-js`'s, a type ALIAS and therefore unaugmentable, whose only
|
|
89
|
+
* object-shaped member is `ArrayElement`. `IslandNode` is that array, and this function is what
|
|
90
|
+
* says so in a form `tsc` reads.
|
|
91
|
+
*
|
|
92
|
+
* Not `: JSX.Element` — render must not import `solid-js`. `unknown` is enough: the error lands on
|
|
93
|
+
* the element, never on the return.
|
|
94
|
+
*/
|
|
95
|
+
export function _IslandIsAJsxComponent(): unknown {
|
|
96
|
+
return (
|
|
97
|
+
<ContactSales subject="pricing">
|
|
98
|
+
<p>the server-rendered shell</p>
|
|
99
|
+
</ContactSales>
|
|
100
|
+
);
|
|
101
|
+
}
|