acrylblends 0.6.1 → 0.6.3
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/README.md +11 -6
- package/docs/framework/README.md +3 -2
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/api/presets.md +0 -3
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +9 -0
- package/docs/framework/getting-started/basic.md +5 -4
- package/docs/framework/getting-started/desktop.md +41 -0
- package/docs/framework/guides/jsx-dom.md +0 -3
- package/docs/framework/guides/jsx.md +1 -4
- package/docs/framework/guides/middleware.md +1 -1
- package/docs/framework/helpers/cookie.md +1 -1
- package/docs/framework/helpers/css.md +0 -3
- package/docs/framework/helpers/dev.md +0 -3
- package/docs/framework/helpers/jwt.md +1 -1
- package/docs/framework/helpers/proxy.md +0 -3
- package/docs/framework/middleware/builtin/basic-auth.md +6 -6
- package/docs/framework/middleware/builtin/cache.md +0 -3
- package/docs/framework/middleware/builtin/combine.md +1 -1
- package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
- package/docs/framework/middleware/builtin/logger.md +1 -1
- package/docs/framework/middleware/builtin/mount.md +0 -3
- package/docs/framework/upstream.json +3 -3
- package/lib/accepts.d.ts +1 -1
- package/lib/acryl-base.d.ts +1 -1
- package/lib/adapter.d.ts +1 -1
- package/lib/{app-CmAYW1Al.d.ts → app-wFwpg-bt.d.ts} +1 -1
- package/lib/base-BpQy2Rk7.d.ts +783 -0
- package/lib/base-ddiVmiE7.js +721 -0
- package/lib/basic-auth.d.ts +1 -1
- package/lib/bearer-auth.d.ts +1 -1
- package/lib/body-limit.d.ts +1 -1
- package/lib/cache.d.ts +43 -0
- package/lib/cache.js +169 -0
- package/lib/client.d.ts +2 -2
- package/lib/combine.d.ts +1 -1
- package/lib/common-D8Z4JRCw.d.ts +46 -0
- package/lib/components-Ce5FRyHU.js +268 -0
- package/lib/components-DYZdxJIJ.js +24 -0
- package/lib/compress.d.ts +1 -1
- package/lib/conninfo.d.ts +2 -2
- package/lib/constants-BR9jM9BC.js +9 -0
- package/lib/{context-Bm7rI-oK.d.ts → context-DVQBjuns.d.ts} +1 -1
- package/lib/context-DxFdY_un.d.ts +250 -0
- package/lib/context-storage.d.ts +1 -1
- package/lib/{cookie-ClMaKDQf.d.ts → cookie-CCelEqky.d.ts} +1 -1
- package/lib/cookie.d.ts +3 -3
- package/lib/cors.d.ts +1 -1
- package/lib/csrf.d.ts +1 -1
- package/lib/css-D2jb7X8b.js +320 -0
- package/lib/css.d.ts +82 -0
- package/lib/css.js +134 -0
- package/lib/dev.d.ts +19 -0
- package/lib/dev.js +50 -0
- package/lib/etag.d.ts +1 -1
- package/lib/factory.d.ts +2 -2
- package/lib/hooks-Bq5q9IMl.js +762 -0
- package/lib/html-SllAe0YT.d.ts +46 -0
- package/lib/html.d.ts +1 -1
- package/lib/index-C850MeBA.d.ts +94 -0
- package/lib/{index-oOClm_Xk.d.ts → index-CUTfU69R.d.ts} +3 -3
- package/lib/{index-B-3F4wt2.d.ts → index-Cqcdluyn.d.ts} +2 -2
- package/lib/{index-UTBmM3GX.d.ts → index-CsvB-F9j.d.ts} +2 -2
- package/lib/index.d.ts +4 -4
- package/lib/ip-restriction.d.ts +2 -2
- package/lib/jsx/dom/client.d.ts +30 -0
- package/lib/jsx/dom/client.js +49 -0
- package/lib/jsx/dom/css.d.ts +84 -0
- package/lib/jsx/dom/css.js +2 -0
- package/lib/jsx/dom/jsx-dev-runtime.d.ts +3 -0
- package/lib/jsx/dom/jsx-dev-runtime.js +2 -0
- package/lib/jsx/dom/jsx-runtime.d.ts +3 -0
- package/lib/jsx/dom/jsx-runtime.js +2 -0
- package/lib/jsx/dom/server.d.ts +105 -0
- package/lib/jsx/dom/server.js +37 -0
- package/lib/jsx/dom.d.ts +5 -0
- package/lib/jsx/dom.js +74 -0
- package/lib/jsx/jsx-dev-runtime.d.ts +6 -0
- package/lib/jsx/jsx-dev-runtime.js +14 -0
- package/lib/jsx/jsx-runtime.d.ts +10 -0
- package/lib/jsx/jsx-runtime.js +30 -0
- package/lib/jsx/streaming.d.ts +2 -0
- package/lib/jsx/streaming.js +2 -0
- package/lib/jsx-dev-runtime-B22O-Npk.d.ts +7 -0
- package/lib/jsx-dev-runtime-kAhR_4C7.js +354 -0
- package/lib/jsx-runtime-BVVgDSdq.d.ts +1 -0
- package/lib/jsx.d.ts +3 -0
- package/lib/jsx.js +47 -0
- package/lib/jwk.d.ts +4 -4
- package/lib/{jwt-Dg493Tai.d.ts → jwt-BARhotDz.d.ts} +1 -1
- package/lib/jwt.d.ts +4 -4
- package/lib/language.d.ts +1 -1
- package/lib/linear-router-C3FPib6h.js +96 -0
- package/lib/logger.d.ts +1 -1
- package/lib/loopback-only.d.ts +1 -1
- package/lib/method-not-allowed.d.ts +2 -2
- package/lib/method-override.d.ts +2 -2
- package/lib/mount.d.ts +44 -0
- package/lib/mount.js +67 -0
- package/lib/node-server/conninfo.d.ts +1 -1
- package/lib/node-server/serve-static.d.ts +1 -1
- package/lib/node-server/serve-static.js +1 -1
- package/lib/node-server.d.ts +1 -1
- package/lib/node-server.js +1 -1
- package/lib/pattern-router-HZxjDGEQ.js +41 -0
- package/lib/powered-by.d.ts +1 -1
- package/lib/pretty-json.d.ts +1 -1
- package/lib/proxy.d.ts +72 -0
- package/lib/proxy.js +121 -0
- package/lib/quick.d.ts +8 -0
- package/lib/quick.js +17 -0
- package/lib/request-id.d.ts +1 -1
- package/lib/request.d.ts +1 -1
- package/lib/route.d.ts +1 -1
- package/lib/router/linear-router.js +1 -95
- package/lib/router/pattern-router.js +1 -40
- package/lib/schema-validator.d.ts +1 -1
- package/lib/secure-headers.d.ts +1 -1
- package/lib/{serve-static-Dw_m6KxR.js → serve-static-Cw23shXv.js} +1 -0
- package/lib/serve-static.d.ts +1 -1
- package/lib/streaming.d.ts +1 -1
- package/lib/testing.d.ts +4 -4
- package/lib/timeout.d.ts +1 -1
- package/lib/timing.d.ts +1 -1
- package/lib/tiny.d.ts +8 -0
- package/lib/tiny.js +15 -0
- package/lib/trailing-slash.d.ts +1 -1
- package/lib/{types-CHesD1KG.d.ts → types-CQiGmsWW.d.ts} +3 -3
- package/lib/{types-CwrQjlaM.d.ts → types-Lqx5BxQF.d.ts} +10 -1
- package/lib/{types-CDO6I3B0.d.ts → types-nEL8VPDC.d.ts} +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/utils/body.d.ts +1 -1
- package/lib/utils/cookie.d.ts +1 -1
- package/lib/utils/crypto.d.ts +1 -1
- package/lib/utils/html.d.ts +1 -45
- package/lib/utils/ipaddr.d.ts +1 -1
- package/lib/utils/jwt.d.ts +1 -1
- package/lib/utils/types.d.ts +2 -2
- package/lib/validator.d.ts +2 -2
- package/lib/ws.d.ts +1 -1
- package/package.json +69 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/src/core/helper/css/common.case.test.tsx +506 -0
- package/plugins/acryl-router/src/core/helper/css/common.ts +337 -0
- package/plugins/acryl-router/src/core/helper/css/index.test.tsx +678 -0
- package/plugins/acryl-router/src/core/helper/css/index.ts +262 -0
- package/plugins/acryl-router/src/core/helper/dev/index.test.ts +181 -0
- package/plugins/acryl-router/src/core/helper/dev/index.ts +79 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +356 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.ts +203 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.test.tsx +557 -0
- package/plugins/acryl-router/src/core/jsx/base.test.tsx +97 -0
- package/plugins/acryl-router/src/core/jsx/base.ts +490 -0
- package/plugins/acryl-router/src/core/jsx/children.test.ts +52 -0
- package/plugins/acryl-router/src/core/jsx/children.ts +20 -0
- package/plugins/acryl-router/src/core/jsx/components.test.tsx +936 -0
- package/plugins/acryl-router/src/core/jsx/components.ts +257 -0
- package/plugins/acryl-router/src/core/jsx/constants.ts +6 -0
- package/plugins/acryl-router/src/core/jsx/context-isolation.test.ts +655 -0
- package/plugins/acryl-router/src/core/jsx/context.ts +266 -0
- package/plugins/acryl-router/src/core/jsx/dom/client.test.tsx +128 -0
- package/plugins/acryl-router/src/core/jsx/dom/client.ts +89 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.test.tsx +330 -0
- package/plugins/acryl-router/src/core/jsx/dom/components.ts +39 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.test.tsx +184 -0
- package/plugins/acryl-router/src/core/jsx/dom/context.ts +52 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.test.tsx +149 -0
- package/plugins/acryl-router/src/core/jsx/dom/css.ts +263 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.test.tsx +172 -0
- package/plugins/acryl-router/src/core/jsx/dom/hooks/index.ts +91 -0
- package/plugins/acryl-router/src/core/jsx/dom/index.test.tsx +3681 -0
- package/plugins/acryl-router/src/core/jsx/dom/index.ts +170 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.test.tsx +1344 -0
- package/plugins/acryl-router/src/core/jsx/dom/intrinsic-element/components.ts +438 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-dev-runtime.ts +23 -0
- package/plugins/acryl-router/src/core/jsx/dom/jsx-runtime.ts +8 -0
- package/plugins/acryl-router/src/core/jsx/dom/render.ts +879 -0
- package/plugins/acryl-router/src/core/jsx/dom/server.test.tsx +199 -0
- package/plugins/acryl-router/src/core/jsx/dom/server.ts +76 -0
- package/plugins/acryl-router/src/core/jsx/dom/utils.ts +7 -0
- package/plugins/acryl-router/src/core/jsx/hooks/dom.test.tsx +859 -0
- package/plugins/acryl-router/src/core/jsx/hooks/index.ts +437 -0
- package/plugins/acryl-router/src/core/jsx/hooks/string.test.tsx +40 -0
- package/plugins/acryl-router/src/core/jsx/index.test.tsx +1744 -0
- package/plugins/acryl-router/src/core/jsx/index.ts +115 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/common.ts +23 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.test.tsx +666 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-element/components.ts +210 -0
- package/plugins/acryl-router/src/core/jsx/intrinsic-elements.ts +938 -0
- package/plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts +26 -0
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.test.tsx +85 -0
- package/plugins/acryl-router/src/core/jsx/jsx-runtime.ts +51 -0
- package/plugins/acryl-router/src/core/jsx/streaming.test.tsx +1281 -0
- package/plugins/acryl-router/src/core/jsx/streaming.ts +231 -0
- package/plugins/acryl-router/src/core/jsx/types.ts +41 -0
- package/plugins/acryl-router/src/core/jsx/utils.test.ts +366 -0
- package/plugins/acryl-router/src/core/jsx/utils.ts +202 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +1211 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.ts +323 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +334 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.ts +108 -0
- package/plugins/acryl-router/src/core/preset/quick.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/quick.ts +24 -0
- package/plugins/acryl-router/src/core/preset/tiny.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/tiny.ts +20 -0
- package/plugins/acryl-router/src/core/utils/types.ts +9 -0
- package/plugins/acryl-router/src/helpers/index.ts +3 -0
- package/plugins/acryl-router/src/middleware/index.ts +2 -0
- package/plugins/acryl-router/src/service.ts +22 -5
- package/plugins/acryl-router/tests/jsx.spec.tsx +63 -0
- package/plugins/acryl-router/upstream.json +3 -3
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/save/plugin.json +1 -1
- package/templates/todo/upstream.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { raw } from '../helper/html/index.ts'
|
|
2
|
+
import type { HtmlEscapedString } from '../utils/html.ts'
|
|
3
|
+
import type { FC, PropsWithChildren } from './index.ts'
|
|
4
|
+
import { isUntrustedObject, JSXFragmentNode, renderChildren, renderUntrustedObject } from './base.ts'
|
|
5
|
+
import { DOM_RENDERER } from './constants.ts'
|
|
6
|
+
import { createContextProviderFunction } from './dom/context.ts'
|
|
7
|
+
|
|
8
|
+
export interface Context<T> extends FC<PropsWithChildren<{ value: T }>> {
|
|
9
|
+
values: T[]
|
|
10
|
+
Provider: FC<PropsWithChildren<{ value: T }>>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const globalContexts: Context<unknown>[] = []
|
|
14
|
+
|
|
15
|
+
/** Per-render context store, isolated per request so values never leak across renders. */
|
|
16
|
+
type RenderStore = WeakMap<Context<unknown>, unknown[]>
|
|
17
|
+
|
|
18
|
+
type AsyncLocalStorageLike<T> = {
|
|
19
|
+
getStore(): T | undefined
|
|
20
|
+
run<R>(store: T, callback: () => R): R
|
|
21
|
+
}
|
|
22
|
+
type AsyncLocalStorageClass = new <T>() => AsyncLocalStorageLike<T>
|
|
23
|
+
type AsyncHooksModule = { AsyncLocalStorage?: AsyncLocalStorageClass }
|
|
24
|
+
|
|
25
|
+
let alsProbed = false
|
|
26
|
+
let asyncLocalStorage: AsyncLocalStorageLike<RenderStore> | undefined
|
|
27
|
+
let fallbackStore: RenderStore | undefined
|
|
28
|
+
// Unsettled async renders on the fallback path. While > 0, a store-less
|
|
29
|
+
// context access is a post-`await` read that degraded to the default value
|
|
30
|
+
// — worth a one-time warning. Stays 0 in the browser DOM renderer.
|
|
31
|
+
let fallbackRendersInFlight = 0
|
|
32
|
+
let warnedFallbackDefault = false
|
|
33
|
+
|
|
34
|
+
const loadAsyncLocalStorage = (): AsyncLocalStorageLike<RenderStore> | undefined => {
|
|
35
|
+
if (alsProbed) {
|
|
36
|
+
return asyncLocalStorage
|
|
37
|
+
}
|
|
38
|
+
alsProbed = true
|
|
39
|
+
|
|
40
|
+
const global = globalThis as unknown as {
|
|
41
|
+
process?: {
|
|
42
|
+
getBuiltinModule?: (specifier: string) => AsyncHooksModule
|
|
43
|
+
mainModule?: { require?: (specifier: string) => AsyncHooksModule }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let AsyncLocalStorage: AsyncLocalStorageClass | undefined
|
|
47
|
+
for (const probe of [
|
|
48
|
+
// Node.js >= 20.16, Deno, Bun, Cloudflare Workers (nodejs_compat). Property
|
|
49
|
+
// access only, so bundlers don't statically resolve `node:async_hooks`.
|
|
50
|
+
() => global.process?.getBuiltinModule?.('node:async_hooks')?.AsyncLocalStorage,
|
|
51
|
+
// Node.js < 20.16 has no `process.getBuiltinModule`, but a CJS entrypoint
|
|
52
|
+
// exposes the main module's `require` here.
|
|
53
|
+
() => global.process?.mainModule?.require?.('node:async_hooks')?.AsyncLocalStorage,
|
|
54
|
+
]) {
|
|
55
|
+
try {
|
|
56
|
+
AsyncLocalStorage = probe()
|
|
57
|
+
} catch {}
|
|
58
|
+
if (AsyncLocalStorage) {
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (AsyncLocalStorage) {
|
|
64
|
+
asyncLocalStorage = new AsyncLocalStorage<RenderStore>()
|
|
65
|
+
}
|
|
66
|
+
return asyncLocalStorage
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const getCurrentStore = (): RenderStore | undefined => {
|
|
70
|
+
return loadAsyncLocalStorage()?.getStore() || fallbackStore
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const warnIfStorelessAccess = (): void => {
|
|
74
|
+
if (fallbackRendersInFlight > 0 && !warnedFallbackDefault) {
|
|
75
|
+
warnedFallbackDefault = true
|
|
76
|
+
console.warn(
|
|
77
|
+
'acryl-router/jsx: AsyncLocalStorage is unavailable in this runtime, so useContext() after an await in an async component falls back to the context default value during server-side rendering. To get provided values across await boundaries, use a runtime with AsyncLocalStorage (Node.js >= 20.16, Deno, Bun, or Cloudflare Workers with the nodejs_compat flag).'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Write path: materializes the store entry so a Provider's push/pop pair
|
|
83
|
+
// operates on a per-render array.
|
|
84
|
+
const getContextValuesIn = <T>(store: RenderStore | undefined, context: Context<T>): T[] => {
|
|
85
|
+
if (!store) {
|
|
86
|
+
warnIfStorelessAccess()
|
|
87
|
+
return context.values
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let values = store.get(context as Context<unknown>) as T[] | undefined
|
|
91
|
+
if (!values) {
|
|
92
|
+
values = [context.values[0]]
|
|
93
|
+
store.set(context as Context<unknown>, values)
|
|
94
|
+
}
|
|
95
|
+
return values
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Read path: never materializes a store entry — a missing entry always
|
|
99
|
+
// resolves to the default value, so reads stay allocation-free.
|
|
100
|
+
const readContextValueIn = <T>(store: RenderStore | undefined, context: Context<T>): T => {
|
|
101
|
+
if (!store) {
|
|
102
|
+
warnIfStorelessAccess()
|
|
103
|
+
return context.values.at(-1) as T
|
|
104
|
+
}
|
|
105
|
+
const values = store.get(context as Context<unknown>) as T[] | undefined
|
|
106
|
+
return values?.length ? (values.at(-1) as T) : context.values[0]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type LocalContexts = [Context<unknown>, unknown][]
|
|
110
|
+
|
|
111
|
+
const captureContextValues = (store: RenderStore | undefined): LocalContexts =>
|
|
112
|
+
(store ? globalContexts.filter((c) => store.has(c)) : globalContexts).map((c) => [
|
|
113
|
+
c,
|
|
114
|
+
readContextValueIn(store, c),
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
const resumeWithContextValues = <T>(
|
|
118
|
+
callback: () => T,
|
|
119
|
+
store: RenderStore | undefined,
|
|
120
|
+
contexts: LocalContexts
|
|
121
|
+
): T =>
|
|
122
|
+
runWithRenderContext(() => {
|
|
123
|
+
// Resolve each target array once so every pop operates on the same array
|
|
124
|
+
// as its push, as the Provider in `createContext` does.
|
|
125
|
+
const currentStore = getCurrentStore()
|
|
126
|
+
const valuesPerContext = contexts.map(([context, value]) => {
|
|
127
|
+
const values = getContextValuesIn(currentStore, context)
|
|
128
|
+
values.push(value)
|
|
129
|
+
return values
|
|
130
|
+
})
|
|
131
|
+
const popContextValues = () => {
|
|
132
|
+
valuesPerContext.forEach((values) => {
|
|
133
|
+
values.pop()
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const result = callback()
|
|
138
|
+
if (result instanceof Promise) {
|
|
139
|
+
return result.finally(popContextValues) as T
|
|
140
|
+
}
|
|
141
|
+
popContextValues()
|
|
142
|
+
return result
|
|
143
|
+
} catch (e) {
|
|
144
|
+
popContextValues()
|
|
145
|
+
throw e
|
|
146
|
+
}
|
|
147
|
+
}, store)
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Establish the request-scoped context store for a render.
|
|
151
|
+
*
|
|
152
|
+
* `resumeStore` continues a suspended subtree in the same store on the fallback
|
|
153
|
+
* path (ignored when `AsyncLocalStorage` is available, where isolation is
|
|
154
|
+
* automatic).
|
|
155
|
+
*
|
|
156
|
+
* Without `AsyncLocalStorage` a render can't be followed across `await`, so the
|
|
157
|
+
* store lives in `fallbackStore` only during synchronous work (mirroring
|
|
158
|
+
* React's request storage). Reading context after `await` then finds no store
|
|
159
|
+
* and falls back to the default value — never another request's value.
|
|
160
|
+
*/
|
|
161
|
+
export const runWithRenderContext = <T>(callback: () => T, resumeStore?: RenderStore): T => {
|
|
162
|
+
if (getCurrentStore()) {
|
|
163
|
+
// Already inside a render (synchronously nested or resumed subtree).
|
|
164
|
+
return callback()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const store = resumeStore ?? new WeakMap()
|
|
168
|
+
const storage = loadAsyncLocalStorage()
|
|
169
|
+
if (storage) {
|
|
170
|
+
// AsyncLocalStorage path: request isolation propagates across `await`.
|
|
171
|
+
return storage.run(store, callback)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Fallback path (no AsyncLocalStorage).
|
|
175
|
+
fallbackStore = store
|
|
176
|
+
let result: T
|
|
177
|
+
try {
|
|
178
|
+
result = callback()
|
|
179
|
+
} finally {
|
|
180
|
+
fallbackStore = undefined
|
|
181
|
+
}
|
|
182
|
+
if (!warnedFallbackDefault && result instanceof Promise) {
|
|
183
|
+
fallbackRendersInFlight++
|
|
184
|
+
result = result.finally(() => {
|
|
185
|
+
fallbackRendersInFlight--
|
|
186
|
+
}) as T
|
|
187
|
+
}
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Capture the current render store and return a resumer that re-establishes it
|
|
193
|
+
* around a deferred continuation (e.g. a re-render after a suspended promise
|
|
194
|
+
* settles). Shared by every suspension point so none reimplements it.
|
|
195
|
+
*/
|
|
196
|
+
export const captureRenderContext = (): (<T>(callback: () => T) => T) => {
|
|
197
|
+
const store = getCurrentStore()
|
|
198
|
+
const contexts = captureContextValues(store)
|
|
199
|
+
return (callback) => resumeWithContextValues(callback, store, contexts)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create a context whose value can be provided with `<Context.Provider>` and
|
|
204
|
+
* read with {@link useContext}.
|
|
205
|
+
*
|
|
206
|
+
* Server-side renders are isolated per request, so a provided value never leaks
|
|
207
|
+
* into a concurrent request — even across `await` in an async component, when
|
|
208
|
+
* `AsyncLocalStorage` is available (Node.js >= 20.16, Deno, Bun, Cloudflare
|
|
209
|
+
* Workers with `nodejs_compat`). Without it, reading context after `await`
|
|
210
|
+
* returns the default value; synchronous components and `use()`-based
|
|
211
|
+
* suspension are unaffected.
|
|
212
|
+
*/
|
|
213
|
+
export const createContext = <T>(defaultValue: T): Context<T> => {
|
|
214
|
+
const values = [defaultValue]
|
|
215
|
+
const context: Context<T> = ((props): HtmlEscapedString | Promise<HtmlEscapedString> => {
|
|
216
|
+
// Resolve the target array once, synchronously: the pop in `.finally()`
|
|
217
|
+
// below may run later and must operate on the same array as the push.
|
|
218
|
+
const contextValues = getContextValuesIn(getCurrentStore(), context)
|
|
219
|
+
contextValues.push(props.value)
|
|
220
|
+
let rendered
|
|
221
|
+
try {
|
|
222
|
+
rendered =
|
|
223
|
+
typeof props.children === 'string'
|
|
224
|
+
? renderChildren([props.children])
|
|
225
|
+
: isUntrustedObject(props.children)
|
|
226
|
+
? renderUntrustedObject(props.children)
|
|
227
|
+
: props.children
|
|
228
|
+
? (Array.isArray(props.children)
|
|
229
|
+
? new JSXFragmentNode('', {}, props.children)
|
|
230
|
+
: props.children
|
|
231
|
+
).toString()
|
|
232
|
+
: raw('')
|
|
233
|
+
} catch (e) {
|
|
234
|
+
contextValues.pop()
|
|
235
|
+
throw e
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (rendered instanceof Promise) {
|
|
239
|
+
return rendered
|
|
240
|
+
.finally(() => contextValues.pop())
|
|
241
|
+
.then((resString) => raw(resString, (resString as HtmlEscapedString).callbacks))
|
|
242
|
+
} else {
|
|
243
|
+
contextValues.pop()
|
|
244
|
+
return raw(rendered)
|
|
245
|
+
}
|
|
246
|
+
}) as Context<T>
|
|
247
|
+
context.values = values
|
|
248
|
+
context.Provider = context
|
|
249
|
+
|
|
250
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
251
|
+
;(context as any)[DOM_RENDERER] = createContextProviderFunction(values)
|
|
252
|
+
|
|
253
|
+
globalContexts.push(context as Context<unknown>)
|
|
254
|
+
|
|
255
|
+
return context
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Read the current value of a context created with {@link createContext}.
|
|
260
|
+
*
|
|
261
|
+
* Safe to call from async components after `await`. See {@link createContext}
|
|
262
|
+
* for the per-runtime isolation guarantees.
|
|
263
|
+
*/
|
|
264
|
+
export const useContext = <T>(context: Context<T>): T => {
|
|
265
|
+
return readContextValueIn(getCurrentStore(), context)
|
|
266
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** @jsxImportSource ../ */
|
|
2
|
+
import { JSDOM } from 'jsdom'
|
|
3
|
+
import { useEffect } from './index.ts'
|
|
4
|
+
import DefaultExport, { createRoot, hydrateRoot } from './client.ts'
|
|
5
|
+
|
|
6
|
+
describe('createRoot', () => {
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
global.requestAnimationFrame = (cb) => setTimeout(cb)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
let dom: JSDOM
|
|
12
|
+
let rootElement: HTMLElement
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
dom = new JSDOM('<html><body><div id="root"></div></body></html>', {
|
|
15
|
+
runScripts: 'dangerously',
|
|
16
|
+
})
|
|
17
|
+
global.document = dom.window.document
|
|
18
|
+
global.HTMLElement = dom.window.HTMLElement
|
|
19
|
+
global.SVGElement = dom.window.SVGElement
|
|
20
|
+
global.Text = dom.window.Text
|
|
21
|
+
rootElement = document.getElementById('root') as HTMLElement
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('render / unmount', async () => {
|
|
25
|
+
const cleanup = vi.fn()
|
|
26
|
+
const App = () => {
|
|
27
|
+
useEffect(() => cleanup, [])
|
|
28
|
+
return <h1>Hello</h1>
|
|
29
|
+
}
|
|
30
|
+
const root = createRoot(rootElement)
|
|
31
|
+
root.render(<App />)
|
|
32
|
+
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>')
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve))
|
|
34
|
+
root.unmount()
|
|
35
|
+
await Promise.resolve()
|
|
36
|
+
expect(rootElement.innerHTML).toBe('')
|
|
37
|
+
expect(cleanup).toHaveBeenCalled()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('call render twice', async () => {
|
|
41
|
+
const App = <h1>Hello</h1>
|
|
42
|
+
const App2 = <h1>World</h1>
|
|
43
|
+
const root = createRoot(rootElement)
|
|
44
|
+
root.render(App)
|
|
45
|
+
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>')
|
|
46
|
+
|
|
47
|
+
const createElementSpy = vi.spyOn(dom.window.document, 'createElement')
|
|
48
|
+
|
|
49
|
+
root.render(App2)
|
|
50
|
+
await Promise.resolve()
|
|
51
|
+
expect(rootElement.innerHTML).toBe('<h1>World</h1>')
|
|
52
|
+
|
|
53
|
+
expect(createElementSpy).not.toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('call render after unmount', async () => {
|
|
57
|
+
const App = <h1>Hello</h1>
|
|
58
|
+
const App2 = <h1>World</h1>
|
|
59
|
+
const root = createRoot(rootElement)
|
|
60
|
+
root.render(App)
|
|
61
|
+
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>')
|
|
62
|
+
root.unmount()
|
|
63
|
+
expect(() => root.render(App2)).toThrow('Cannot update an unmounted root')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('hydrateRoot', () => {
|
|
68
|
+
let dom: JSDOM
|
|
69
|
+
let rootElement: HTMLElement
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
dom = new JSDOM('<html><body><div id="root"></div></body></html>', {
|
|
72
|
+
runScripts: 'dangerously',
|
|
73
|
+
})
|
|
74
|
+
global.document = dom.window.document
|
|
75
|
+
global.HTMLElement = dom.window.HTMLElement
|
|
76
|
+
global.SVGElement = dom.window.SVGElement
|
|
77
|
+
global.Text = dom.window.Text
|
|
78
|
+
rootElement = document.getElementById('root') as HTMLElement
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should return root object', async () => {
|
|
82
|
+
const cleanup = vi.fn()
|
|
83
|
+
const App = () => {
|
|
84
|
+
useEffect(() => cleanup, [])
|
|
85
|
+
return <h1>Hello</h1>
|
|
86
|
+
}
|
|
87
|
+
const root = hydrateRoot(rootElement, <App />)
|
|
88
|
+
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>')
|
|
89
|
+
await new Promise((resolve) => setTimeout(resolve))
|
|
90
|
+
root.unmount()
|
|
91
|
+
await Promise.resolve()
|
|
92
|
+
expect(rootElement.innerHTML).toBe('')
|
|
93
|
+
expect(cleanup).toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('call render', async () => {
|
|
97
|
+
const App = <h1>Hello</h1>
|
|
98
|
+
const App2 = <h1>World</h1>
|
|
99
|
+
const root = hydrateRoot(rootElement, App)
|
|
100
|
+
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>')
|
|
101
|
+
|
|
102
|
+
const createElementSpy = vi.spyOn(dom.window.document, 'createElement')
|
|
103
|
+
|
|
104
|
+
root.render(App2)
|
|
105
|
+
await Promise.resolve()
|
|
106
|
+
expect(rootElement.innerHTML).toBe('<h1>World</h1>')
|
|
107
|
+
|
|
108
|
+
expect(createElementSpy).not.toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('call render after unmount', async () => {
|
|
112
|
+
const App = <h1>Hello</h1>
|
|
113
|
+
const App2 = <h1>World</h1>
|
|
114
|
+
const root = hydrateRoot(rootElement, App)
|
|
115
|
+
expect(rootElement.innerHTML).toBe('<h1>Hello</h1>')
|
|
116
|
+
root.unmount()
|
|
117
|
+
expect(() => root.render(App2)).toThrow('Cannot update an unmounted root')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('default export', () => {
|
|
122
|
+
;['createRoot', 'hydrateRoot'].forEach((key) => {
|
|
123
|
+
it(key, () => {
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
expect((DefaultExport as any)[key]).toBeDefined()
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* This module provides APIs for `acryl-router/jsx/dom/client`, which is compatible with `react-dom/client`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Child } from '../base.ts'
|
|
7
|
+
import { useState } from '../hooks/index.ts'
|
|
8
|
+
import { buildNode, renderNode } from './render.ts'
|
|
9
|
+
import type { NodeObject } from './render.ts'
|
|
10
|
+
|
|
11
|
+
export interface Root {
|
|
12
|
+
render(children: Child): void
|
|
13
|
+
unmount(): void
|
|
14
|
+
}
|
|
15
|
+
export type RootOptions = Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a root object for rendering
|
|
19
|
+
* @param element Render target
|
|
20
|
+
* @param options Options for createRoot (not supported yet)
|
|
21
|
+
* @returns Root object has `render` and `unmount` methods
|
|
22
|
+
*/
|
|
23
|
+
export const createRoot = (
|
|
24
|
+
element: HTMLElement | DocumentFragment,
|
|
25
|
+
options: RootOptions = {}
|
|
26
|
+
): Root => {
|
|
27
|
+
let setJsxNode:
|
|
28
|
+
| undefined // initial state
|
|
29
|
+
| ((jsxNode: unknown) => void) // rendered
|
|
30
|
+
| null = // unmounted
|
|
31
|
+
undefined
|
|
32
|
+
|
|
33
|
+
if (Object.keys(options).length > 0) {
|
|
34
|
+
console.warn('createRoot options are not supported yet')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
render(jsxNode: unknown) {
|
|
39
|
+
if (setJsxNode === null) {
|
|
40
|
+
// unmounted
|
|
41
|
+
throw new Error('Cannot update an unmounted root')
|
|
42
|
+
}
|
|
43
|
+
if (setJsxNode) {
|
|
44
|
+
// rendered
|
|
45
|
+
setJsxNode(jsxNode)
|
|
46
|
+
} else {
|
|
47
|
+
renderNode(
|
|
48
|
+
buildNode({
|
|
49
|
+
tag: () => {
|
|
50
|
+
const [_jsxNode, _setJsxNode] = useState(jsxNode)
|
|
51
|
+
setJsxNode = _setJsxNode
|
|
52
|
+
return _jsxNode
|
|
53
|
+
},
|
|
54
|
+
props: {},
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
} as any) as NodeObject,
|
|
57
|
+
element
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
unmount() {
|
|
62
|
+
setJsxNode?.(null)
|
|
63
|
+
setJsxNode = null
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a root object and hydrate app to the target element.
|
|
70
|
+
* In acryl-router/jsx/dom, hydrate is equivalent to render.
|
|
71
|
+
* @param element Render target
|
|
72
|
+
* @param reactNode A JSXNode to render
|
|
73
|
+
* @param options Options for createRoot (not supported yet)
|
|
74
|
+
* @returns Root object has `render` and `unmount` methods
|
|
75
|
+
*/
|
|
76
|
+
export const hydrateRoot = (
|
|
77
|
+
element: HTMLElement | DocumentFragment,
|
|
78
|
+
reactNode: Child,
|
|
79
|
+
options: RootOptions = {}
|
|
80
|
+
): Root => {
|
|
81
|
+
const root = createRoot(element, options)
|
|
82
|
+
root.render(reactNode)
|
|
83
|
+
return root
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default {
|
|
87
|
+
createRoot,
|
|
88
|
+
hydrateRoot,
|
|
89
|
+
}
|