duto 0.0.0 → 0.260808.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.
@@ -0,0 +1,186 @@
1
+ // TODO: not used yet
2
+ // import type { AstroComponentMetadata, NamedSSRLoadedRendererValue } from 'astro'
3
+ // import opts from 'astro:preact:opts'
4
+ import { Component as BaseComponent, h, type VNode } from 'preact'
5
+ import { renderToStringAsync } from 'preact-render-to-string'
6
+
7
+ import { getContext, incrementIslandId } from './context'
8
+ import { restoreSignalsOnProps, serializeSignals } from './signals'
9
+ import StaticHtml from './static-html'
10
+ import type { IslandAttrs } from './types'
11
+ import type { IslandMeta } from '@/types'
12
+ // import type { AstroPreactAttrs, RendererContext } from './types.js'
13
+ // import { createFilter } from '@astrojs/internal-helpers/create-filter'
14
+
15
+ const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase())
16
+
17
+ let originalConsoleError: typeof console.error
18
+ let consoleFilterRefs = 0
19
+
20
+ // const filter = opts?.include || opts?.exclude ? createFilter(opts.include, opts.exclude) : null;
21
+
22
+ function setVNodeMask(vNode: VNode<any>, mask: [number, number]) {
23
+ // Preact's useId derives IDs from an internal root vnode mask (`_mask`/`__m`)
24
+ // Astro renders each island as a separate root, so without seeding a unique
25
+ // mask per island, multiple islands can generate colliding IDs
26
+ // Tracked upstream: https://github.com/preactjs/preact/issues/3781
27
+ (vNode as VNode<any> & { _mask?: [number, number]; __m?: [number, number] })._mask = mask;
28
+ (vNode as VNode<any> & { _mask?: [number, number]; __m?: [number, number] }).__m = mask;
29
+ }
30
+
31
+ export async function check(
32
+ // this: RendererContext,
33
+ Component: any,
34
+ props: Record<string, any>,
35
+ children: any,
36
+ metadata?: IslandMeta,
37
+ ) {
38
+ if (typeof Component !== 'function') return false
39
+ if (Component.name === 'QwikComponent') return false
40
+
41
+ const componentUrl = metadata?.componentUrl
42
+ if (filter && componentUrl && !filter(componentUrl)) {
43
+ return false;
44
+ }
45
+
46
+ if (Component.prototype != null && typeof Component.prototype.render === 'function') {
47
+ return BaseComponent.isPrototypeOf(Component)
48
+ }
49
+
50
+ useConsoleFilter()
51
+
52
+ try {
53
+ const { html } = await renderToStaticMarkup(Component, props, children, undefined);
54
+ if (typeof html !== 'string') {
55
+ return false;
56
+ }
57
+
58
+ // There are edge cases (SolidJS) where Preact *might* render a string,
59
+ // but components would be <undefined></undefined>
60
+ // It also might render an empty string.
61
+ return html === '' ? false : !html.includes('<undefined>')
62
+ } catch {
63
+ return false
64
+ } finally {
65
+ finishUsingConsoleFilter()
66
+ }
67
+ }
68
+
69
+ function shouldHydrate(metadata: IslandMeta | undefined) {
70
+ // Adjust how this is hydrated only when the version of Astro supports `astroStaticSlot`
71
+ return metadata?.astroStaticSlot ? !!metadata.hydrate : true
72
+ }
73
+
74
+ export async function renderToStaticMarkup(
75
+ // this: RendererContext,
76
+ Component: any,
77
+ props: Record<string, any>,
78
+ { default: children, ...slotted }: Record<string, any>,
79
+ metadata: IslandMeta | undefined,
80
+ ) {
81
+ const ctx = getContext(this.result)
82
+
83
+ const slots: Record<string, ReturnType<typeof h>> = {}
84
+ for (const [key, value] of Object.entries(slotted)) {
85
+ const name = slotName(key);
86
+ slots[name] = h(StaticHtml, {
87
+ hydrate: shouldHydrate(metadata),
88
+ value,
89
+ name,
90
+ }) as VNode<any>
91
+ }
92
+
93
+ // Restore signals back onto props so that they will be passed as-is to components
94
+ let propsMap = restoreSignalsOnProps(ctx, props)
95
+
96
+ const newProps = { ...props, ...slots }
97
+
98
+ const attrs: IslandAttrs = {}
99
+ serializeSignals(ctx, props, attrs, propsMap)
100
+ const islandId = incrementIslandId(ctx)
101
+ attrs['id'] = islandId.toString()
102
+
103
+ const vNode: VNode<any> = h(
104
+ Component,
105
+ newProps,
106
+ children != null
107
+ ? h(StaticHtml, {
108
+ hydrate: shouldHydrate(metadata),
109
+ value: children,
110
+ })
111
+ : children,
112
+ )
113
+ setVNodeMask(vNode, [islandId, 0])
114
+
115
+ const html = await renderToStringAsync(vNode)
116
+ return { attrs, html }
117
+ }
118
+
119
+ /**
120
+ * Reduces console noise by filtering known non-problematic errors
121
+ *
122
+ * Performs reference counting to allow parallel usage from async code
123
+ *
124
+ * To stop filtering, please ensure that there always is a matching call
125
+ * to `finishUsingConsoleFilter` afterwards
126
+ */
127
+ function useConsoleFilter() {
128
+ consoleFilterRefs++
129
+
130
+ if (!originalConsoleError) {
131
+ originalConsoleError = console.error
132
+
133
+ try {
134
+ console.error = filteredConsoleError
135
+ } catch {
136
+ // If we're unable to hook `console.error`, just accept it
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Indicates that the filter installed by `useConsoleFilter`
143
+ * is no longer needed by the calling code.
144
+ */
145
+ function finishUsingConsoleFilter() {
146
+ consoleFilterRefs--
147
+
148
+ // Note: Instead of reverting `console.error` back to the original
149
+ // when the reference counter reaches 0, we leave our hook installed
150
+ // to prevent potential race conditions once `check` is made async
151
+ }
152
+
153
+ /**
154
+ * Hook/wrapper function for the global `console.error` function.
155
+ *
156
+ * Ignores known non-problematic errors while any code is using the console filter.
157
+ * Otherwise, simply forwards all arguments to the original function.
158
+ */
159
+ function filteredConsoleError(msg: string, ...rest: any[]) {
160
+ if (
161
+ consoleFilterRefs > 0 &&
162
+ !process.env.DUTO_INTERNAL_TEST_DISABLE_CONSOLE_FILTER &&
163
+ typeof msg === 'string'
164
+ ) {
165
+ // In `check`, we attempt to render JSX components through Preact.
166
+ // When attempting this on a React component, React may output
167
+ // the following error, which we can safely filter out:
168
+ const isKnownReactHookError =
169
+ msg.includes('Invalid hook call.') &&
170
+ // for React v18 and earlier
171
+ (msg.includes('https://reactjs.org/link/invalid-hook-call') ||
172
+ // for React v19 and later
173
+ msg.includes('https://react.dev/link/invalid-hook-call'))
174
+ if (isKnownReactHookError) return
175
+ }
176
+ originalConsoleError(msg, ...rest)
177
+ }
178
+
179
+ // const renderer: NamedSSRLoadedRendererValue = {
180
+ // name: '@astrojs/preact',
181
+ // check,
182
+ // renderToStaticMarkup,
183
+ // supportsAstroStaticSlot: true,
184
+ // }
185
+
186
+ // export default renderer
@@ -0,0 +1,89 @@
1
+ // TODO: not used yet
2
+ // import type { Context } from './context'
3
+ import { incrementId, type Context } from './context'
4
+ import type {
5
+ ArrayObjectMapping,
6
+ IslandAttrs,
7
+ PropNameToSignalMap,
8
+ SignalLike,
9
+ Signals,
10
+ SignalToKeyOrIndexMap,
11
+ } from './types'
12
+
13
+ function isSignal(x: any): x is SignalLike {
14
+ return x != null && typeof x === 'object' && typeof x.peek === 'function' && 'value' in x
15
+ }
16
+
17
+ export function restoreSignalsOnProps(ctx: Context, props: Record<string, any>) {
18
+ // Restore signal props that were mutated for serialization
19
+ let propMap: PropNameToSignalMap
20
+ if (ctx.propsToSignals.has(props)) {
21
+ propMap = ctx.propsToSignals.get(props)!
22
+ } else {
23
+ propMap = new Map()
24
+ ctx.propsToSignals.set(props, propMap)
25
+ }
26
+
27
+ for (const [key, signal] of propMap)
28
+ props[key] = signal
29
+
30
+ return propMap
31
+ }
32
+
33
+ export function serializeSignals(
34
+ ctx: Context,
35
+ props: Record<string, any>,
36
+ attrs: IslandAttrs,
37
+ map: PropNameToSignalMap,
38
+ ) {
39
+ // Check for signals
40
+ const signals: Signals = {}
41
+ for (const [key, value] of Object.entries(props)) {
42
+ const isPropArray = Array.isArray(value)
43
+ // `typeof null` is 'object' in JS, so we need to check for `null` specifically
44
+ const isPropObject =
45
+ !isSignal(value) && typeof props[key] === 'object' && props[key] !== null && !isPropArray
46
+
47
+ if (isPropObject || isPropArray) {
48
+ const values = isPropObject ? Object.keys(props[key]) : value
49
+ values.forEach((valueKey: number | string, valueIndex: number) => {
50
+ const signal = isPropObject ? props[key][valueKey] : valueKey
51
+ if (isSignal(signal)) {
52
+ const keyOrIndex = isPropObject ? valueKey.toString() : valueIndex
53
+
54
+ props[key] = isPropObject
55
+ ? Object.assign({}, props[key], { [keyOrIndex]: signal.peek() })
56
+ : props[key].map((v: SignalLike, i: number) =>
57
+ i === valueIndex ? [signal.peek(), i] : v,
58
+ )
59
+
60
+ const currentMap = (map.get(key) || []) as SignalToKeyOrIndexMap
61
+ map.set(key, [...currentMap, [signal, keyOrIndex]])
62
+
63
+ const currentSignals = (signals[key] || []) as ArrayObjectMapping
64
+ signals[key] = [...currentSignals, [getSignalId(ctx, signal), keyOrIndex]]
65
+ }
66
+ })
67
+ } else if (isSignal(value)) {
68
+ // Set the value to the current signal value
69
+ // This mutates the props on purpose, so that it will be serialized correct.
70
+ props[key] = value.peek()
71
+ map.set(key, value)
72
+
73
+ signals[key] = getSignalId(ctx, value)
74
+ }
75
+ }
76
+
77
+ if (Object.keys(signals).length)
78
+ attrs['data-set'] = JSON.stringify(signals)
79
+ }
80
+
81
+ function getSignalId(ctx: Context, item: SignalLike) {
82
+ let id = ctx.signals.get(item)
83
+ if (!id) {
84
+ id = incrementId(ctx)
85
+ ctx.signals.set(item, id)
86
+ }
87
+
88
+ return id
89
+ }
@@ -0,0 +1,45 @@
1
+
2
+
3
+ import { h } from 'preact'
4
+ import { memo } from 'preact/compat'
5
+
6
+ const StaticHtml = memo(({ value, name, hydrate = true }: {
7
+ value: string,
8
+ name?: string,
9
+ hydrate?: boolean,
10
+ }) => {
11
+ if (!value) return null
12
+ const tagName = hydrate ? 'd-b' : 'd-p'
13
+ return h(tagName, { name, dangerouslySetInnerHTML: { __html: value } })
14
+ }, () => true) // segundo argumento = areEqual, sempre true = nunca re-renderiza
15
+
16
+ export default StaticHtml
17
+
18
+ // import { h } from 'preact'
19
+ /**
20
+ * Duto passes `children` as a string of HTML, so we need
21
+ * a wrapper `div` to render that content as VNodes
22
+ *
23
+ * As a bonus, we can signal to Preact that this subtree is
24
+ * entirely static and will never change via `shouldComponentUpdate`
25
+ */
26
+ // const StaticHtml = ({ value, name, hydrate = true }: {
27
+ // value: string,
28
+ // name?: string,
29
+ // hydrate?: boolean,
30
+ // }) => {
31
+ // if (!value) return null
32
+ // const tagName = hydrate ? 'd-b': 'd-p' // 'astro-slot' : 'astro-static-slot'
33
+ // return h(tagName, { name, dangerouslySetInnerHTML: { __html: value } })
34
+ // }
35
+
36
+ // /**
37
+ // * This tells Preact to opt-out of re-rendering this subtree
38
+ // * In addition to being a performance optimization
39
+ // * this also allows other frameworks to attach to `children`
40
+ // *
41
+ // * See https://preactjs.com/guide/v8/external-dom-mutations
42
+ // */
43
+ // StaticHtml.shouldComponentUpdate = () => false
44
+
45
+ // export default StaticHtml
@@ -0,0 +1,26 @@
1
+ // import type { SSRResult } from 'astro'
2
+ import type { FilterPattern } from 'vite'
3
+
4
+ // export type RendererContext = {
5
+ // result: SSRResult,
6
+ // }
7
+
8
+ export type SignalLike = {
9
+ peek(): any,
10
+ }
11
+
12
+ export type ArrayObjectMapping = [string, number | string][]
13
+ export type Signals = Record<string, string | ArrayObjectMapping>
14
+
15
+ export type SignalToKeyOrIndexMap = [SignalLike, number | string][]
16
+ export type PropNameToSignalMap = Map<string, SignalLike | SignalToKeyOrIndexMap>
17
+
18
+ export type IslandAttrs = {
19
+ ['data-set']?: string,
20
+ ['id']?: string,
21
+ }
22
+
23
+ export type VirtualModuleOptions = {
24
+ include?: FilterPattern,
25
+ exclude?: FilterPattern,
26
+ }