acrylblends 0.6.2 → 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 +4 -1
- package/docs/framework/README.md +3 -2
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +6 -0
- package/docs/framework/getting-started/basic.md +1 -0
- package/docs/framework/getting-started/desktop.md +41 -0
- package/docs/framework/guides/jsx-dom.md +0 -3
- package/docs/framework/guides/jsx.md +0 -3
- package/docs/framework/helpers/css.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-BdrVHBlJ.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 +1 -1
- 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-B5Y1lLha.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-D9_nQbsM.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 +2 -2
- 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-m12zcmph.d.ts → index-CUTfU69R.d.ts} +3 -3
- package/lib/{index-DtEoak1i.d.ts → index-Cqcdluyn.d.ts} +2 -2
- package/lib/{index-Kqou8Fjh.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-BA65QhnJ.d.ts → jwt-BARhotDz.d.ts} +1 -1
- package/lib/jwt.d.ts +4 -4
- package/lib/language.d.ts +1 -1
- 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 +1 -1
- 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/powered-by.d.ts +1 -1
- package/lib/pretty-json.d.ts +1 -1
- package/lib/proxy.d.ts +1 -1
- package/lib/quick.d.ts +1 -1
- package/lib/request-id.d.ts +1 -1
- package/lib/request.d.ts +1 -1
- package/lib/route.d.ts +1 -1
- 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 +1 -1
- package/lib/trailing-slash.d.ts +1 -1
- package/lib/{types-Bd29gh2o.d.ts → types-CQiGmsWW.d.ts} +3 -3
- package/lib/{types-D9DEXMjE.d.ts → types-Lqx5BxQF.d.ts} +8 -1
- package/lib/{types-ZM9gIiKh.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 +45 -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/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/utils/types.ts +6 -0
- package/plugins/acryl-router/src/helpers/index.ts +1 -0
- 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,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* This module enables JSX to supports streaming Response.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { raw } from '../helper/html/index.ts'
|
|
7
|
+
import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html.ts'
|
|
8
|
+
import type { HtmlEscapedString } from '../utils/html.ts'
|
|
9
|
+
import type { Child, FC, PropsWithChildren, Context as JSXContext } from './index.ts'
|
|
10
|
+
import { isUntrustedObject, JSXNode, renderChildren, renderUntrustedObject } from './base.ts'
|
|
11
|
+
import { childrenToString } from './components.ts'
|
|
12
|
+
import { DOM_RENDERER, DOM_STASH } from './constants.ts'
|
|
13
|
+
import { captureRenderContext, createContext, useContext } from './context.ts'
|
|
14
|
+
import { Suspense as SuspenseDomRenderer } from './dom/components.ts'
|
|
15
|
+
import { buildDataStack } from './dom/render.ts'
|
|
16
|
+
import type { HasRenderToDom, NodeObject } from './dom/render.ts'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Used to specify nonce for scripts generated by `Suspense` and `ErrorBoundary`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <StreamingContext.Provider value={{ scriptNonce: 'test-nonce' }}>
|
|
24
|
+
* <Suspense fallback={<p>Loading...</p>}>
|
|
25
|
+
* <Content />
|
|
26
|
+
* </Suspense>
|
|
27
|
+
* </StreamingContext.Provider>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export const StreamingContext: JSXContext<{ scriptNonce: string } | null> = createContext<{
|
|
31
|
+
scriptNonce: string
|
|
32
|
+
} | null>(null)
|
|
33
|
+
|
|
34
|
+
let suspenseCounter = 0
|
|
35
|
+
|
|
36
|
+
const serializeBoundaryChild = (child: Child): string | Promise<string> =>
|
|
37
|
+
typeof child === 'string' || Array.isArray(child)
|
|
38
|
+
? renderChildren([child])
|
|
39
|
+
: child == null || typeof child === 'boolean'
|
|
40
|
+
? ''
|
|
41
|
+
: isUntrustedObject(child)
|
|
42
|
+
? renderUntrustedObject(child)
|
|
43
|
+
: child.toString()
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @experimental
|
|
47
|
+
* `Suspense` is an experimental feature.
|
|
48
|
+
* The API might be changed.
|
|
49
|
+
*/
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = async ({
|
|
52
|
+
children,
|
|
53
|
+
fallback,
|
|
54
|
+
}) => {
|
|
55
|
+
if (!Array.isArray(children)) {
|
|
56
|
+
children = [children]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const nonce = useContext(StreamingContext)?.scriptNonce
|
|
60
|
+
|
|
61
|
+
let resArray: HtmlEscapedString[] | Promise<HtmlEscapedString[]>[] = []
|
|
62
|
+
|
|
63
|
+
// for use() hook
|
|
64
|
+
const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject
|
|
65
|
+
const popNodeStack = () => {
|
|
66
|
+
buildDataStack.pop()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
stackNode[DOM_STASH][0] = 0
|
|
71
|
+
buildDataStack.push([[], stackNode])
|
|
72
|
+
resArray = children.map(serializeBoundaryChild) as HtmlEscapedString[]
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e instanceof Promise) {
|
|
75
|
+
// Capture before deferring: on the fallback path the render context is
|
|
76
|
+
// observable here but not in the `.then()` below.
|
|
77
|
+
const resume = captureRenderContext()
|
|
78
|
+
resArray = [
|
|
79
|
+
e.then(() =>
|
|
80
|
+
resume(() => {
|
|
81
|
+
stackNode[DOM_STASH][0] = 0
|
|
82
|
+
buildDataStack.push([[], stackNode])
|
|
83
|
+
return childrenToString(children as Child[]).finally(popNodeStack)
|
|
84
|
+
})
|
|
85
|
+
),
|
|
86
|
+
] as Promise<HtmlEscapedString[]>[]
|
|
87
|
+
} else {
|
|
88
|
+
throw e
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
popNodeStack()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (resArray.some((res) => (res as {}) instanceof Promise)) {
|
|
95
|
+
const index = suspenseCounter++
|
|
96
|
+
const fallbackStr = await (typeof fallback === 'string' || Array.isArray(fallback)
|
|
97
|
+
? renderChildren([fallback])
|
|
98
|
+
: isUntrustedObject(fallback)
|
|
99
|
+
? renderUntrustedObject(fallback)
|
|
100
|
+
: fallback.toString())
|
|
101
|
+
return raw(`<template id="H:${index}"></template>${fallbackStr}<!--/$-->`, [
|
|
102
|
+
...(fallbackStr.callbacks || []),
|
|
103
|
+
({ phase, buffer, context }) => {
|
|
104
|
+
if (phase === HtmlEscapedCallbackPhase.BeforeStream) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
return Promise.all(resArray).then(async (htmlArray) => {
|
|
108
|
+
htmlArray = htmlArray.flat()
|
|
109
|
+
const content = htmlArray.join('')
|
|
110
|
+
if (buffer) {
|
|
111
|
+
buffer[0] = buffer[0].replace(
|
|
112
|
+
new RegExp(`<template id="H:${index}"></template>.*?<!--/\\$-->`, 's'),
|
|
113
|
+
() => content
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
let html = buffer
|
|
117
|
+
? ''
|
|
118
|
+
: `<template data-acryl-target="H:${index}">${content}</template><script${
|
|
119
|
+
nonce ? ` nonce="${nonce}"` : ''
|
|
120
|
+
}>
|
|
121
|
+
((d,c,n) => {
|
|
122
|
+
c=d.currentScript.previousSibling
|
|
123
|
+
d=d.getElementById('H:${index}')
|
|
124
|
+
if(!d)return
|
|
125
|
+
do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
|
|
126
|
+
d.replaceWith(c.content)
|
|
127
|
+
})(document)
|
|
128
|
+
</script>`
|
|
129
|
+
|
|
130
|
+
const callbacks = htmlArray
|
|
131
|
+
.map((html) => (html as HtmlEscapedString).callbacks || [])
|
|
132
|
+
.flat()
|
|
133
|
+
if (!callbacks.length) {
|
|
134
|
+
return html
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (phase === HtmlEscapedCallbackPhase.Stream) {
|
|
138
|
+
html = await resolveCallback(html, HtmlEscapedCallbackPhase.BeforeStream, true, context)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return raw(html, callbacks)
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
])
|
|
145
|
+
} else {
|
|
146
|
+
return raw(resArray.join(''))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
;(Suspense as HasRenderToDom)[DOM_RENDERER] = SuspenseDomRenderer
|
|
150
|
+
|
|
151
|
+
const textEncoder = new TextEncoder()
|
|
152
|
+
/**
|
|
153
|
+
* @experimental
|
|
154
|
+
* `renderToReadableStream()` is an experimental feature.
|
|
155
|
+
* The API might be changed.
|
|
156
|
+
*/
|
|
157
|
+
export const renderToReadableStream = (
|
|
158
|
+
content: HtmlEscapedString | JSXNode | Promise<HtmlEscapedString>,
|
|
159
|
+
onError: (e: unknown) => string | void = console.trace
|
|
160
|
+
): ReadableStream<Uint8Array> => {
|
|
161
|
+
let cancelled = false
|
|
162
|
+
const reader = new ReadableStream<Uint8Array>({
|
|
163
|
+
async start(controller) {
|
|
164
|
+
try {
|
|
165
|
+
if (content instanceof JSXNode) {
|
|
166
|
+
// aJSXNode.toString() returns a string or Promise<string> and string is already escaped
|
|
167
|
+
content = content.toString() as HtmlEscapedString | Promise<HtmlEscapedString>
|
|
168
|
+
}
|
|
169
|
+
const context = typeof content === 'object' ? content : {}
|
|
170
|
+
const resolved = await resolveCallback(
|
|
171
|
+
content,
|
|
172
|
+
HtmlEscapedCallbackPhase.BeforeStream,
|
|
173
|
+
true,
|
|
174
|
+
context
|
|
175
|
+
)
|
|
176
|
+
if (!cancelled) {
|
|
177
|
+
controller.enqueue(textEncoder.encode(resolved))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let resolvedCount = 0
|
|
181
|
+
const callbacks: Promise<void>[] = []
|
|
182
|
+
const then = (promise: Promise<string>) => {
|
|
183
|
+
callbacks.push(
|
|
184
|
+
promise
|
|
185
|
+
.catch((err) => {
|
|
186
|
+
console.log(err)
|
|
187
|
+
onError(err)
|
|
188
|
+
return ''
|
|
189
|
+
})
|
|
190
|
+
.then(async (res) => {
|
|
191
|
+
res = await resolveCallback(
|
|
192
|
+
res,
|
|
193
|
+
HtmlEscapedCallbackPhase.BeforeStream,
|
|
194
|
+
true,
|
|
195
|
+
context
|
|
196
|
+
)
|
|
197
|
+
;(res as HtmlEscapedString).callbacks
|
|
198
|
+
?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context }))
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
|
+
.filter<Promise<string>>(Boolean as any)
|
|
201
|
+
.forEach(then)
|
|
202
|
+
resolvedCount++
|
|
203
|
+
if (!cancelled) {
|
|
204
|
+
controller.enqueue(textEncoder.encode(res))
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
;(resolved as HtmlEscapedString).callbacks
|
|
210
|
+
?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context }))
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
.filter<Promise<string>>(Boolean as any)
|
|
213
|
+
.forEach(then)
|
|
214
|
+
while (resolvedCount !== callbacks.length) {
|
|
215
|
+
await Promise.all(callbacks)
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// maybe the connection was closed
|
|
219
|
+
onError(e)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!cancelled) {
|
|
223
|
+
controller.close()
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
cancel() {
|
|
227
|
+
cancelled = true
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
return reader
|
|
231
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All types exported from "acryl-router/jsx" are in this file.
|
|
3
|
+
*/
|
|
4
|
+
import type { Child, JSXNode } from './base.ts'
|
|
5
|
+
import type { JSX } from './intrinsic-elements.ts'
|
|
6
|
+
import type { DomEvent } from '../utils/types.ts'
|
|
7
|
+
|
|
8
|
+
export type { Child, JSXNode, FC } from './base.ts'
|
|
9
|
+
export type { RefObject } from './hooks/index.ts'
|
|
10
|
+
export type { Context } from './context.ts'
|
|
11
|
+
|
|
12
|
+
export type PropsWithChildren<P = unknown> = P & { children?: Child | undefined }
|
|
13
|
+
export type CSSProperties = JSX.CSSProperties
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* React types
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
type ReactElement<P = any, T = string | Function> = JSXNode & {
|
|
21
|
+
type: T
|
|
22
|
+
props: P
|
|
23
|
+
key: string | null
|
|
24
|
+
}
|
|
25
|
+
type ReactNode = ReactElement | string | number | boolean | null | undefined
|
|
26
|
+
type ComponentClass<_P = {}, _S = {}> = unknown
|
|
27
|
+
|
|
28
|
+
export type { ReactElement, ReactNode, ComponentClass }
|
|
29
|
+
|
|
30
|
+
export type Event = globalThis.Event
|
|
31
|
+
export type MouseEvent = DomEvent<'MouseEvent'>
|
|
32
|
+
export type KeyboardEvent = DomEvent<'KeyboardEvent'>
|
|
33
|
+
export type FocusEvent = DomEvent<'FocusEvent'>
|
|
34
|
+
export type ClipboardEvent = DomEvent<'ClipboardEvent'>
|
|
35
|
+
export type InputEvent = DomEvent<'InputEvent'>
|
|
36
|
+
export type PointerEvent = DomEvent<'PointerEvent'>
|
|
37
|
+
export type TouchEvent = DomEvent<'TouchEvent'>
|
|
38
|
+
export type WheelEvent = DomEvent<'WheelEvent'>
|
|
39
|
+
export type AnimationEvent = DomEvent<'AnimationEvent'>
|
|
40
|
+
export type TransitionEvent = DomEvent<'TransitionEvent'>
|
|
41
|
+
export type DragEvent = DomEvent<'DragEvent'>
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isValidAttributeName,
|
|
3
|
+
isValidTagName,
|
|
4
|
+
normalizeIntrinsicElementKey,
|
|
5
|
+
styleObjectForEach,
|
|
6
|
+
} from './utils.ts'
|
|
7
|
+
|
|
8
|
+
describe('normalizeIntrinsicElementKey', () => {
|
|
9
|
+
test.each`
|
|
10
|
+
key | expected
|
|
11
|
+
${'className'} | ${'class'}
|
|
12
|
+
${'htmlFor'} | ${'for'}
|
|
13
|
+
${'crossOrigin'} | ${'crossorigin'}
|
|
14
|
+
${'httpEquiv'} | ${'http-equiv'}
|
|
15
|
+
${'itemProp'} | ${'itemprop'}
|
|
16
|
+
${'fetchPriority'} | ${'fetchpriority'}
|
|
17
|
+
${'noModule'} | ${'nomodule'}
|
|
18
|
+
${'formAction'} | ${'formaction'}
|
|
19
|
+
${'href'} | ${'href'}
|
|
20
|
+
`('should convert $key to $expected', ({ key, expected }) => {
|
|
21
|
+
expect(normalizeIntrinsicElementKey(key)).toBe(expected)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('isValidAttributeName', () => {
|
|
26
|
+
test.each`
|
|
27
|
+
name
|
|
28
|
+
${'class'}
|
|
29
|
+
${'id'}
|
|
30
|
+
${'href'}
|
|
31
|
+
${'data-foo'}
|
|
32
|
+
${'aria-label'}
|
|
33
|
+
${'onclick'}
|
|
34
|
+
${'viewBox'}
|
|
35
|
+
${'xml:lang'}
|
|
36
|
+
`('should return true for valid name "$name"', ({ name }) => {
|
|
37
|
+
expect(isValidAttributeName(name)).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test.each`
|
|
41
|
+
name | description
|
|
42
|
+
${''} | ${'empty string'}
|
|
43
|
+
${'" onfocus="alert(1)'} | ${'double quote'}
|
|
44
|
+
${"' onfocus='alert(1)"} | ${'single quote'}
|
|
45
|
+
${'foo<bar'} | ${'less than'}
|
|
46
|
+
${'"><script>alert(1)</script>'} | ${'greater than'}
|
|
47
|
+
${'foo bar'} | ${'space'}
|
|
48
|
+
${'foo=bar'} | ${'equals sign'}
|
|
49
|
+
${'foo/bar'} | ${'slash'}
|
|
50
|
+
${'foo\\bar'} | ${'backslash'}
|
|
51
|
+
${'foo`bar'} | ${'backtick'}
|
|
52
|
+
${'a\x00b'} | ${'null byte'}
|
|
53
|
+
${'a\x1fb'} | ${'control character'}
|
|
54
|
+
${'a\x7fb'} | ${'DEL character'}
|
|
55
|
+
`('should return false for "$description"', ({ name }) => {
|
|
56
|
+
expect(isValidAttributeName(name)).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test.each`
|
|
60
|
+
name | description
|
|
61
|
+
${'xlink:href'} | ${'namespace separator'}
|
|
62
|
+
${'data.foo'} | ${'dot'}
|
|
63
|
+
${'data_foo'} | ${'underscore'}
|
|
64
|
+
${'é'} | ${'non-ascii'}
|
|
65
|
+
`('should return true for valid "$description" names', ({ name }) => {
|
|
66
|
+
expect(isValidAttributeName(name)).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should keep validating names after the valid attribute name cache is reset', () => {
|
|
70
|
+
for (let i = 0; i < 1025; i++) {
|
|
71
|
+
expect(isValidAttributeName(`data-k${i}`)).toBe(true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(isValidAttributeName('class')).toBe(true)
|
|
75
|
+
expect(isValidAttributeName('" onfocus="alert(1)')).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('isValidTagName', () => {
|
|
80
|
+
test.each`
|
|
81
|
+
name
|
|
82
|
+
${''}
|
|
83
|
+
${'div'}
|
|
84
|
+
${'h1'}
|
|
85
|
+
${'custom-element'}
|
|
86
|
+
${'clipPath'}
|
|
87
|
+
${'foo:bar'}
|
|
88
|
+
${'x.foo'}
|
|
89
|
+
${'x_bar'}
|
|
90
|
+
${'é'}
|
|
91
|
+
`('should return true for valid tag name "$name"', ({ name }) => {
|
|
92
|
+
expect(isValidTagName(name)).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test.each`
|
|
96
|
+
name | description
|
|
97
|
+
${'foo bar'} | ${'space'}
|
|
98
|
+
${'foo\nbar'} | ${'newline'}
|
|
99
|
+
${'" onfocus="alert(1)'} | ${'double quote'}
|
|
100
|
+
${"' onfocus='alert(1)"} | ${'single quote'}
|
|
101
|
+
${'foo<bar'} | ${'less than'}
|
|
102
|
+
${'"><script>alert(1)</script>'} | ${'greater than'}
|
|
103
|
+
${'foo=bar'} | ${'equals sign'}
|
|
104
|
+
${'foo/bar'} | ${'slash'}
|
|
105
|
+
${'foo\\bar'} | ${'backslash'}
|
|
106
|
+
${'foo`bar'} | ${'backtick'}
|
|
107
|
+
${'a\x00b'} | ${'null byte'}
|
|
108
|
+
${'a\x1fb'} | ${'control character'}
|
|
109
|
+
${'a\x7fb'} | ${'DEL character'}
|
|
110
|
+
${'!'} | ${'single bang'}
|
|
111
|
+
${'!--'} | ${'parser-control bang prefix'}
|
|
112
|
+
${'!DOCTYPE'} | ${'doctype-like name'}
|
|
113
|
+
${'?'} | ${'single question mark'}
|
|
114
|
+
${'?xml'} | ${'processing instruction'}
|
|
115
|
+
`('should return false for "$description"', ({ name }) => {
|
|
116
|
+
expect(isValidTagName(name)).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should keep validating names after the valid tag name cache is reset', () => {
|
|
120
|
+
for (let i = 0; i < 257; i++) {
|
|
121
|
+
expect(isValidTagName(`custom-${i}`)).toBe(true)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
expect(isValidTagName('div')).toBe(true)
|
|
125
|
+
expect(isValidTagName('div onmouseover="alert(1)"')).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('styleObjectForEach', () => {
|
|
130
|
+
describe('Should output the number as it is, when a number type is passed', () => {
|
|
131
|
+
test.each`
|
|
132
|
+
property
|
|
133
|
+
${'animationIterationCount'}
|
|
134
|
+
${'aspectRatio'}
|
|
135
|
+
${'borderImageOutset'}
|
|
136
|
+
${'borderImageSlice'}
|
|
137
|
+
${'borderImageWidth'}
|
|
138
|
+
${'columnCount'}
|
|
139
|
+
${'columns'}
|
|
140
|
+
${'flex'}
|
|
141
|
+
${'flexGrow'}
|
|
142
|
+
${'flexPositive'}
|
|
143
|
+
${'flexShrink'}
|
|
144
|
+
${'flexNegative'}
|
|
145
|
+
${'flexOrder'}
|
|
146
|
+
${'gridArea'}
|
|
147
|
+
${'gridRow'}
|
|
148
|
+
${'gridRowEnd'}
|
|
149
|
+
${'gridRowSpan'}
|
|
150
|
+
${'gridRowStart'}
|
|
151
|
+
${'gridColumn'}
|
|
152
|
+
${'gridColumnEnd'}
|
|
153
|
+
${'gridColumnSpan'}
|
|
154
|
+
${'gridColumnStart'}
|
|
155
|
+
${'fontWeight'}
|
|
156
|
+
${'lineClamp'}
|
|
157
|
+
${'lineHeight'}
|
|
158
|
+
${'opacity'}
|
|
159
|
+
${'order'}
|
|
160
|
+
${'orphans'}
|
|
161
|
+
${'scale'}
|
|
162
|
+
${'tabSize'}
|
|
163
|
+
${'widows'}
|
|
164
|
+
${'zIndex'}
|
|
165
|
+
${'zoom'}
|
|
166
|
+
${'fillOpacity'}
|
|
167
|
+
${'floodOpacity'}
|
|
168
|
+
${'stopOpacity'}
|
|
169
|
+
${'strokeDasharray'}
|
|
170
|
+
${'strokeDashoffset'}
|
|
171
|
+
${'strokeMiterlimit'}
|
|
172
|
+
${'strokeOpacity'}
|
|
173
|
+
${'strokeWidth'}
|
|
174
|
+
`('$property', ({ property }) => {
|
|
175
|
+
const fn = vi.fn()
|
|
176
|
+
styleObjectForEach({ [property]: 1 }, fn)
|
|
177
|
+
expect(fn).toBeCalledWith(
|
|
178
|
+
property.replace(/[A-Z]/g, (m: string) => `-${m.toLowerCase()}`),
|
|
179
|
+
'1'
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
describe('Should output with px suffix, when a number type is passed', () => {
|
|
184
|
+
test.each`
|
|
185
|
+
property
|
|
186
|
+
${'borderBottomWidth'}
|
|
187
|
+
${'borderLeftWidth'}
|
|
188
|
+
${'borderRightWidth'}
|
|
189
|
+
${'borderTopWidth'}
|
|
190
|
+
${'borderWidth'}
|
|
191
|
+
${'bottom'}
|
|
192
|
+
${'fontSize'}
|
|
193
|
+
${'height'}
|
|
194
|
+
${'left'}
|
|
195
|
+
${'margin'}
|
|
196
|
+
${'marginBottom'}
|
|
197
|
+
${'marginLeft'}
|
|
198
|
+
${'marginRight'}
|
|
199
|
+
${'marginTop'}
|
|
200
|
+
${'padding'}
|
|
201
|
+
${'paddingBottom'}
|
|
202
|
+
${'paddingLeft'}
|
|
203
|
+
${'paddingRight'}
|
|
204
|
+
${'paddingTop'}
|
|
205
|
+
${'right'}
|
|
206
|
+
${'top'}
|
|
207
|
+
${'width'}
|
|
208
|
+
${'gridGap'}
|
|
209
|
+
${'gridRowGap'}
|
|
210
|
+
${'gridColumnGap'}
|
|
211
|
+
`('$property', ({ property }) => {
|
|
212
|
+
const fn = vi.fn()
|
|
213
|
+
styleObjectForEach({ [property]: 1 }, fn)
|
|
214
|
+
expect(fn).toBeCalledWith(
|
|
215
|
+
property.replace(/[A-Z]/g, (m: string) => `-${m.toLowerCase()}`),
|
|
216
|
+
'1px'
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('Should skip values containing ";" to prevent CSS injection', () => {
|
|
222
|
+
it('skips a value with ";"', () => {
|
|
223
|
+
const fn = vi.fn()
|
|
224
|
+
styleObjectForEach({ color: 'red;background:blue' }, fn)
|
|
225
|
+
expect(fn).not.toBeCalled()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('keeps safe siblings while skipping unsafe ones', () => {
|
|
229
|
+
const fn = vi.fn()
|
|
230
|
+
styleObjectForEach({ color: 'red;background:blue', backgroundColor: 'white' }, fn)
|
|
231
|
+
expect(fn).toBeCalledTimes(1)
|
|
232
|
+
expect(fn).toBeCalledWith('background-color', 'white')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('does not throw for unsafe values', () => {
|
|
236
|
+
expect(() => styleObjectForEach({ color: 'a;b' }, () => {})).not.toThrow()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('keeps semicolons inside quoted strings', () => {
|
|
240
|
+
const fn = vi.fn()
|
|
241
|
+
styleObjectForEach({ fontFamily: '"a;b", sans-serif' }, fn)
|
|
242
|
+
expect(fn).toBeCalledWith('font-family', '"a;b", sans-serif')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test.each([
|
|
246
|
+
['LF', '\n'],
|
|
247
|
+
['CR', '\r'],
|
|
248
|
+
['FF', '\f'],
|
|
249
|
+
])('skips quoted strings that contain %s before a declaration separator', (_, newline) => {
|
|
250
|
+
const fn = vi.fn()
|
|
251
|
+
styleObjectForEach(
|
|
252
|
+
{ fontFamily: `"${newline};background:url(https://example.com/a.png)` },
|
|
253
|
+
fn
|
|
254
|
+
)
|
|
255
|
+
expect(fn).not.toBeCalled()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('keeps semicolons inside CSS functions', () => {
|
|
259
|
+
const fn = vi.fn()
|
|
260
|
+
styleObjectForEach({ backgroundImage: 'url("data:image/svg+xml;utf8,<svg></svg>")' }, fn)
|
|
261
|
+
expect(fn).toBeCalledWith('background-image', 'url("data:image/svg+xml;utf8,<svg></svg>")')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test.each([['square brackets', 'red[;background:blue]']])(
|
|
265
|
+
'keeps semicolons inside CSS simple blocks with %s',
|
|
266
|
+
(_, value) => {
|
|
267
|
+
const fn = vi.fn()
|
|
268
|
+
styleObjectForEach({ color: value }, fn)
|
|
269
|
+
expect(fn).toBeCalledWith('color', value)
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
test.each([
|
|
274
|
+
['opening curly block', 'red{;background:blue}'],
|
|
275
|
+
['closing curly block', 'red};background:blue'],
|
|
276
|
+
])('skips CSS values containing %s', (_, value) => {
|
|
277
|
+
const fn = vi.fn()
|
|
278
|
+
styleObjectForEach({ color: value, backgroundColor: 'white' }, fn)
|
|
279
|
+
expect(fn).toBeCalledTimes(1)
|
|
280
|
+
expect(fn).toBeCalledWith('background-color', 'white')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
test.each([
|
|
284
|
+
['square brackets', 'red[;background:blue];position:fixed'],
|
|
285
|
+
['curly braces', 'red{;background:blue};position:fixed'],
|
|
286
|
+
])('skips top-level semicolons after CSS simple blocks with %s', (_, value) => {
|
|
287
|
+
const fn = vi.fn()
|
|
288
|
+
styleObjectForEach({ color: value }, fn)
|
|
289
|
+
expect(fn).not.toBeCalled()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('skips top-level semicolons after CSS comments', () => {
|
|
293
|
+
const fn = vi.fn()
|
|
294
|
+
styleObjectForEach({ color: 'red/*(*/;background:blue;position:fixed;top:0' }, fn)
|
|
295
|
+
expect(fn).not.toBeCalled()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test.each([
|
|
299
|
+
['comment', 'red/*'],
|
|
300
|
+
['double-quoted string', '"abc'],
|
|
301
|
+
['single-quoted string', "'abc"],
|
|
302
|
+
['function block', 'red('],
|
|
303
|
+
['square block', 'red['],
|
|
304
|
+
['curly block', 'red{'],
|
|
305
|
+
['unmatched function closer', 'red)'],
|
|
306
|
+
['unmatched square closer', 'red]'],
|
|
307
|
+
['unmatched curly closer', 'red}'],
|
|
308
|
+
['mismatched simple block closer', 'red[)'],
|
|
309
|
+
['escape', 'red\\'],
|
|
310
|
+
])('skips unterminated CSS %s that can swallow following declarations', (_, value) => {
|
|
311
|
+
const fn = vi.fn()
|
|
312
|
+
styleObjectForEach({ color: value, display: 'none' }, fn)
|
|
313
|
+
expect(fn).toBeCalledTimes(1)
|
|
314
|
+
expect(fn).toBeCalledWith('display', 'none')
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test.each([
|
|
318
|
+
['comment that closes exactly at end of value', 'red/* hi */'],
|
|
319
|
+
['CSS hex escape that decodes to a delimiter byte', 'red\\3b '],
|
|
320
|
+
['vertical tab inside a quoted string', '"\vfoo"'],
|
|
321
|
+
['trailing solidus that is not a comment start', 'a/'],
|
|
322
|
+
])('keeps CSS-safe edge case — %s', (_, value) => {
|
|
323
|
+
const fn = vi.fn()
|
|
324
|
+
styleObjectForEach({ color: value }, fn)
|
|
325
|
+
expect(fn).toBeCalledWith('color', value)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('skips style property names that can break declaration boundaries', () => {
|
|
329
|
+
const fn = vi.fn()
|
|
330
|
+
styleObjectForEach({ 'color;background-image': 'url(https://example.com/a.png)' }, fn)
|
|
331
|
+
styleObjectForEach({ 'color:background': 'red' }, fn)
|
|
332
|
+
expect(fn).not.toBeCalled()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('keeps safe property names that use digits', () => {
|
|
336
|
+
const fn = vi.fn()
|
|
337
|
+
styleObjectForEach({ '--my-var-1': '15px', '--myVar-2': '20px' }, fn)
|
|
338
|
+
expect(fn).toHaveBeenNthCalledWith(1, '--my-var-1', '15px')
|
|
339
|
+
expect(fn).toHaveBeenNthCalledWith(2, '--myVar-2', '20px')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('keeps vendor-prefixed property names', () => {
|
|
343
|
+
const fn = vi.fn()
|
|
344
|
+
styleObjectForEach({ WebkitLineClamp: '2' }, fn)
|
|
345
|
+
expect(fn).toBeCalledWith('-webkit-line-clamp', '2')
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('keeps validating style property names after the valid style property name cache is reset', () => {
|
|
349
|
+
for (let i = 0; i < 1025; i++) {
|
|
350
|
+
styleObjectForEach({ [`--cache-${i}`]: '1px' }, () => {})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const fn = vi.fn()
|
|
354
|
+
styleObjectForEach({ '--after-reset-1': '2px', 'color:background': 'red' }, fn)
|
|
355
|
+
expect(fn).toBeCalledTimes(1)
|
|
356
|
+
expect(fn).toBeCalledWith('--after-reset-1', '2px')
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('skips unsupported runtime values without throwing', () => {
|
|
360
|
+
const fn = vi.fn()
|
|
361
|
+
expect(() => styleObjectForEach({ color: false, backgroundColor: 'white' }, fn)).not.toThrow()
|
|
362
|
+
expect(fn).toBeCalledTimes(1)
|
|
363
|
+
expect(fn).toBeCalledWith('background-color', 'white')
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
})
|