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,403 @@
1
+ import { h, type JSX } from 'preact'
2
+ import { local } from '#/storage'
3
+ import { navigate } from '../'
4
+
5
+ const accepted = (lang: string | undefined, locales: string[] = [], fallback: string) => lang && locales.includes(lang) ? lang : fallback
6
+
7
+ const splitLang = (lang: string) => {
8
+ let arr = []
9
+ if (lang.indexOf('-') > -1) {
10
+ arr = lang.split('-')
11
+ } else if (lang.indexOf('_') > -1) {
12
+ arr = lang.split('_')
13
+ } else {
14
+ arr = [lang]
15
+ }
16
+
17
+ return arr.map(v => v.toLowerCase())
18
+ }
19
+
20
+ const isAccepted = (lang: string, locales: string[] = []) => {
21
+ if (locales.includes(lang)) return true
22
+
23
+ for (var locale in locales) {
24
+ if (locale == lang || lang.startsWith(locale) || lang.endsWith(locale)) return true
25
+ }
26
+
27
+ return false
28
+ }
29
+
30
+ const getAccepted = (lang: string, locales: string[] = [], defaultLocale = '') => {
31
+ if (!lang || !locales?.length) return defaultLocale
32
+
33
+ lang = lang.toLowerCase()
34
+
35
+ const spl = splitLang(lang)
36
+
37
+ for (var key in spl) {
38
+ if (isAccepted(spl[key], locales)) return spl[key]
39
+ }
40
+
41
+ return defaultLocale
42
+ }
43
+
44
+ const detect = (locales: string[], defaultLocale: string) => {
45
+ if (!defaultLocale) defaultLocale = 'en'
46
+ if (typeof window === 'undefined') return defaultLocale
47
+
48
+ locales = locales.map(l => l.toLowerCase())
49
+
50
+ if (window?.navigator?.languages)
51
+ return window.navigator.languages.map(lang => getAccepted(lang, locales)).filter(Boolean)[0] || defaultLocale
52
+
53
+ // @ts-ignore
54
+ return getAccepted(window?.navigator?.language || window.navigator?.userLanguage || '', locales, defaultLocale)
55
+ }
56
+
57
+
58
+ const fixDate = (str: string) => str.replace(/de/g, (match, offset, val) => val.lastIndexOf('de', offset - 1) === -1 ? match : '').replace(/\s+/g, ' ').trim()
59
+
60
+ let _locales: string[] = []
61
+ let _messages: Record<string, Record<string, string>> = {}
62
+ let _routes: Record<string, Record<string, string>> = {}
63
+ let _routeMap: Record<string, string> = {}
64
+
65
+ let _locale: string = ''
66
+ let _detected: string = ''
67
+ export let _prefixed: boolean = false
68
+
69
+ export const isPrefixed = () => _prefixed
70
+
71
+ export const getLocale = () => {
72
+ if (typeof window === 'undefined') {
73
+ return cached() || _detected
74
+ } else {
75
+ const matched = window?.location?.pathname.match(new RegExp(`^/(${_locales.join('|')})(/|$)`))
76
+ if (matched && matched[1]) {
77
+ _prefixed = true
78
+ return matched[1]
79
+ }
80
+
81
+ _prefixed = false
82
+ return cached() || _detected
83
+ // return matched && matched[1] ? matched[1] : cached() || _detected
84
+ }
85
+ }
86
+
87
+ export const setLocale = (lang: string) => {
88
+ cache(lang)
89
+ _locale = accepted(lang, _locales, _detected)
90
+ }
91
+
92
+ export default function i18n(
93
+ fallbackLocale: string,
94
+ locales: string[],
95
+ messages: Record<string, Record<string, string>>,
96
+ routes: Record<string, Record<string, string>>,
97
+ routeMap: Record<string, string>,
98
+ ) {
99
+ _locales = locales
100
+ _messages = messages
101
+ _routes = routes
102
+ _routeMap = routeMap
103
+ _detected = detect(locales, fallbackLocale)
104
+ _locale = typeof window === 'undefined' ? fallbackLocale : getLocale()
105
+
106
+ // console.error(routes)
107
+ // console.error(routeMap)
108
+ }
109
+
110
+ export const cache = (val: string) => local.set('lang', accepted(val, _locales, ''))
111
+ export const cached = () => local.get('lang') as string || ''
112
+
113
+ export const updateWidth = (el: HTMLSelectElement) => {
114
+ if (el) {
115
+ const option = el.querySelector(`option[value="${el.value}"]`) as HTMLOptionElement
116
+ const length = option.textContent.split(' ')[0]?.length || 0
117
+ el.style.width = (length * 15.7)+ 'px'
118
+ }
119
+ }
120
+ export const updatePickers = (lang?: string) => {
121
+ lang ??= accepted(lang, _locales, _locale)
122
+
123
+ document.querySelectorAll('.locale-select').forEach(el => {
124
+ const select = el.querySelector('select')
125
+ if (select) {
126
+ select.value = lang
127
+ updateWidth(select)
128
+ }
129
+ })
130
+ }
131
+
132
+ export const route = (route = '', lang?: string) => {
133
+ lang ??= accepted(lang, _locales, _locale)
134
+ const path = _routeMap[route]
135
+ return _routes[lang][path ? path : route] || _routes[lang]?.notfound || undefined
136
+ }
137
+
138
+ export const redirect = (key = '', lang?: string) => r(key, lang)
139
+ export const r = (key: string, lang?: string) => {
140
+ lang ??= accepted(lang, _locales, _locale)
141
+ const path = '/'+ lang + (key ? '/'+ route(key, lang) : '')
142
+
143
+ if (!location.pathname.endsWith(path))
144
+ navigate(path)
145
+
146
+ // return '/' + lang + '/' + (key in _routes[lang] ? _routes[lang][key] : key)
147
+ }
148
+ // export const redirect = (r = '', lang?: string) => {
149
+ // lang = accepted(lang, _locales, _locale)
150
+ // const path = '/'+ lang + (r ? '/'+ route(r, lang) : '')
151
+
152
+ // if (!location.pathname.endsWith(path))
153
+ // location.pathname = path
154
+ // }
155
+
156
+ ////////////////////////////////////////////////////////////////////////////////
157
+
158
+ type Primitives = string | number | boolean
159
+
160
+ type TFunc = {
161
+ (key: string, lang?: string): string
162
+ (key: string, params: Record<string, Primitives>, lang?: string): string
163
+ (key: string, params: Primitives[], lang?: string): string
164
+ (strings: TemplateStringsArray, ...values: Primitives[]): string & JSX.Element
165
+ (props: TProps): JSX.Element
166
+ }
167
+
168
+ type TProps = {
169
+ s?: string
170
+ children?: string
171
+ lang?: string
172
+ params?: Record<string, Primitives> | Primitives[]
173
+ }
174
+
175
+ const lookup = (key: string, lang: string): string => {
176
+ if (!key) return '';
177
+ if (key.startsWith('_')) return key.replace(/^_+/, '');
178
+ const msg = _messages[lang] ?? _messages[_locale];
179
+ return (key in msg ? msg[key] : key) as string;
180
+ }
181
+
182
+ const interpolate = (template: string, params: Record<string, Primitives> | Primitives[]): string => {
183
+ if (Array.isArray(params))
184
+ return template.replace(/\{(\d+)\}/g, (_, i) => String(params[Number(i)] ?? `{${i}}`))
185
+
186
+ return template.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`))
187
+ }
188
+
189
+ const taggedKey = (strings: TemplateStringsArray, values: Primitives[]): string =>
190
+ strings.reduce((acc, s, i) => acc + s + (values[i] !== undefined ? `%${i}%` : ''), '');
191
+
192
+ const resolveTagged = (strings: TemplateStringsArray, values: Primitives[], lang: string): string => {
193
+ const template = lookup(taggedKey(strings, values), lang)
194
+ return values.reduce((acc, val, i) => acc.replace(`%${i}%`, String(val)), template)
195
+ }
196
+
197
+ // --- VNode que se comporta como string em qualquer coerção ---
198
+ const hybridNode = (key: string, resolved: string, lang?: string, params?: TProps['params']) => {
199
+ const attrs: Record<string, string> = { s: key, children: resolved }
200
+ if (lang) attrs.lang = lang
201
+ if (params) attrs.params = JSON.stringify(params)
202
+
203
+ const vnode = h('i-i', attrs) as unknown as Record<string | symbol, unknown>
204
+
205
+ const _attrs = { value: () => resolved, enumerable: false }
206
+ Object.defineProperty(vnode, 'toString', _attrs)
207
+ Object.defineProperty(vnode, 'valueOf', _attrs)
208
+ vnode[Symbol.toPrimitive] = () => resolved
209
+
210
+ return vnode as unknown as string & JSX.Element
211
+ }
212
+
213
+ const isJSXProps = (val: unknown): val is TProps =>
214
+ typeof val === 'object' && val !== null && !Array.isArray(val) && !('raw' in val)
215
+
216
+ export const t = ((first: string | TemplateStringsArray | TProps, ...rest: unknown[]): unknown => {
217
+
218
+ // --- JSX: <t s='home' /> ou <t>home</t> ---
219
+ if (isJSXProps(first)) {
220
+ const { s, children, lang, params } = first;
221
+ const key = s ?? children ?? '';
222
+ const resolved = params
223
+ ? interpolate(lookup(key, lang ?? _locale), params)
224
+ : lookup(key, lang ?? _locale);
225
+
226
+ return hybridNode(key, resolved, lang, params)
227
+ }
228
+
229
+ // --- Tagged template: t`home` ou t`Bem-vindo ${name}` ---
230
+ if (Array.isArray(first) && 'raw' in first) {
231
+ const strings = first as TemplateStringsArray;
232
+ const values = rest as Primitives[];
233
+ const key = taggedKey(strings, values);
234
+ const resolved = resolveTagged(strings, values, _locale);
235
+
236
+ return hybridNode(key, resolved);
237
+ }
238
+
239
+ // --- Chamada normal: t('key', ...) ---
240
+ const key = first as string;
241
+ const lang = () => typeof rest[rest.length - 1] === 'string' ? rest[rest.length - 1] as string : _locale;
242
+
243
+ if (typeof rest[0] === 'object' && rest[0] !== null)
244
+ return interpolate(lookup(key, lang()), rest[0] as Record<string, Primitives> | Primitives[]);
245
+
246
+ return lookup(key, rest[0] && typeof rest[0] === 'string' ? rest[0] : _locale)
247
+ }) as TFunc
248
+
249
+ export const tl = (lang: string) =>
250
+ (strings: TemplateStringsArray, ...values: Primitives[]): string =>
251
+ resolveTagged(strings, values, lang);
252
+ ////////////////////////////////////////////////////////////////////////////////
253
+
254
+ // export const t = (key: string, lang?: string) => {
255
+ // lang ??= accepted(lang, _locales, _locale)
256
+ // const msg = _messages[lang]
257
+
258
+ // if (key && !key.startsWith('_') && key in msg)
259
+ // return msg[key]
260
+
261
+ // return key?.replace(/^_+/, '')
262
+ // }
263
+ export const d = (str: string, format?: string, lang?: string) => {
264
+ lang ??= accepted(lang, _locales, _locale)
265
+
266
+ const date = new Date(str+ 'T00:00:00')
267
+ if (isNaN(date.getTime())) return str
268
+
269
+ if (format && !['short', 'long'].includes(format)) {
270
+ const year = date.getFullYear(),
271
+ month = date.getMonth(),
272
+ day = date.getDate(),
273
+ formatter = new Intl.DateTimeFormat(lang, {month: 'long'}),
274
+ monthName = formatter.format(date),
275
+ shortFormatter = new Intl.DateTimeFormat(lang, {month: 'short'}),
276
+ shortMonthName = shortFormatter.format(date)
277
+
278
+ return fixDate(format
279
+ .replace(/YYYY/g, year.toString())
280
+ .replace(/YY/g, year.toString().slice(-2))
281
+ .replace(/MMMM/g, monthName)
282
+ .replace(/MMM/g, shortMonthName)
283
+ .replace(/MM/g, (month + 1).toString().padStart(2, '0'))
284
+ .replace(/M/g, (month + 1).toString())
285
+ .replace(/DD/g, day.toString().padStart(2, '0'))
286
+ .replace(/D/g, day.toString()))
287
+ // .replace(/de/g, (match, offset, val) => val.lastIndexOf('de', offset - 1) === -1 ? match : '')
288
+ }
289
+
290
+ return fixDate(new Intl.DateTimeFormat(lang, {
291
+ // dateStyle: 'full',
292
+ year: 'numeric',
293
+ month: format == 'short' ? 'short' : 'long',
294
+ day: 'numeric',
295
+ }).format(date))
296
+ }
297
+
298
+ // export default function i18n(
299
+ // fallbackLocale: string,
300
+ // locales: string[],
301
+ // messages: Record<string, Record<string, string>>,
302
+ // routes: Record<string, Record<string, string>>,
303
+ // routeMap: Record<string, string>,
304
+ // ) {
305
+ // // const fallbackLocale = 'pt-br'
306
+ // // const locales = ['pt-br', 'en']
307
+
308
+ // const localeRegex = new RegExp(`^/(${locales.join('|')})(/|$)`)
309
+ // const matched = window.location.pathname.match(localeRegex)
310
+
311
+ // const detected = detect(locales, fallbackLocale)
312
+ // const locale = matched ? matched[1] : detected
313
+
314
+ // return {
315
+ // detected,
316
+ // locale,
317
+ // locales,
318
+ // cache(val: string) {
319
+ // localStorage.setItem('lang', accepted(val, locales, ''))
320
+ // },
321
+ // cached() {
322
+ // return localStorage.lang || ''
323
+ // },
324
+ // updateWidth (el: HTMLSelectElement) {
325
+ // if (el) {
326
+ // const option = el.querySelector(`option[value="${el.value}"]`) as HTMLOptionElement
327
+ // const length = option.textContent.split(' ')[0]?.length || 0
328
+ // el.style.width = (length * 15.7)+ 'px'
329
+ // }
330
+ // },
331
+ // updatePickers(lang = '') {
332
+ // lang = accepted(lang, locales, locale)
333
+ // document.querySelectorAll('.locale-select').forEach(el => {
334
+ // const select = el.querySelector('select')
335
+ // if (select) {
336
+ // select.value = lang
337
+ // this.updateWidth(select)
338
+ // }
339
+ // })
340
+ // },
341
+ // route(route = '', lang?: string) {
342
+ // lang = accepted(lang, locales, locale)
343
+ // const path = routeMap[route]
344
+ // return routes[lang][path ? path : route] || routes[lang]?.notfound || undefined
345
+ // },
346
+ // redirect(route = '', lang?: string) {
347
+ // lang = accepted(lang, locales, locale)
348
+ // const path = '/'+ lang + (route ? '/'+ this.route(route, lang) : '')
349
+
350
+ // if (!window.location.pathname.endsWith(path))
351
+ // window.location.pathname = path
352
+ // },
353
+
354
+ // t(key: string, lang?: string) {
355
+ // lang ??= accepted(lang, locales, locale)
356
+ // const msg = messages[lang]
357
+
358
+ // if (key && !key.startsWith('_') && key in msg)
359
+ // return msg[key]
360
+
361
+ // return key?.replace(/^_+/, '')
362
+ // },
363
+ // r(key: string, lang?: string) {
364
+ // lang ??= accepted(lang, locales, locale)
365
+
366
+ // return '/' + lang + '/' + (key in routes[lang] ? routes[lang][key] : key)
367
+ // },
368
+ // d(str: string, format?: string, lang?: string) {
369
+ // lang ??= accepted(lang, locales, locale)
370
+
371
+ // const date = new Date(str+ 'T00:00:00')
372
+ // if (isNaN(date.getTime())) return str
373
+
374
+ // if (format && !['short', 'long'].includes(format)) {
375
+ // const year = date.getFullYear(),
376
+ // month = date.getMonth(),
377
+ // day = date.getDate(),
378
+ // formatter = new Intl.DateTimeFormat(lang, {month: 'long'}),
379
+ // monthName = formatter.format(date),
380
+ // shortFormatter = new Intl.DateTimeFormat(lang, {month: 'short'}),
381
+ // shortMonthName = shortFormatter.format(date)
382
+
383
+ // return fixDate(format
384
+ // .replace(/YYYY/g, year.toString())
385
+ // .replace(/YY/g, year.toString().slice(-2))
386
+ // .replace(/MMMM/g, monthName)
387
+ // .replace(/MMM/g, shortMonthName)
388
+ // .replace(/MM/g, (month + 1).toString().padStart(2, '0'))
389
+ // .replace(/M/g, (month + 1).toString())
390
+ // .replace(/DD/g, day.toString().padStart(2, '0'))
391
+ // .replace(/D/g, day.toString()))
392
+ // // .replace(/de/g, (match, offset, val) => val.lastIndexOf('de', offset - 1) === -1 ? match : '')
393
+ // }
394
+
395
+ // return fixDate(new Intl.DateTimeFormat(lang, {
396
+ // // dateStyle: 'full',
397
+ // year: 'numeric',
398
+ // month: format == 'short' ? 'short' : 'long',
399
+ // day: 'numeric',
400
+ // }).format(date))
401
+ // },
402
+ // }
403
+ // }
@@ -0,0 +1,44 @@
1
+ import { local } from '#/storage'
2
+ import { t, _prefixed } from './client'
3
+
4
+ export default class EI18n extends HTMLElement {
5
+ static observedAttributes = ['s']
6
+ #builded = false
7
+ #unsubscribe: (() => void) | null = null
8
+
9
+ connectedCallback() {
10
+ if (!_prefixed) // @ts-ignore // TODO: talvez usar função para obter o valor atual..
11
+ this.#unsubscribe = local.subscribe('lang', e => this.build(e.val))
12
+
13
+ if (document.readyState === 'interactive' || document.readyState === 'complete') {
14
+ this.build()
15
+ return
16
+ }
17
+
18
+ setTimeout(() => {
19
+ this.build()
20
+ }, 0)
21
+ }
22
+
23
+ attributeChangedCallback() {
24
+ if (this.#builded)
25
+ this.build()
26
+ }
27
+
28
+ disconnectedCallback() {
29
+ if (this.#unsubscribe) {
30
+ this.#unsubscribe?.()
31
+ this.#unsubscribe = null
32
+ }
33
+ }
34
+
35
+ build(val?: string) {
36
+ // if (!this.isConnected) return
37
+ const key = this.getAttribute('s')
38
+ if (!key) return
39
+
40
+ this.innerHTML = t(key, val)
41
+
42
+ this.#builded = true
43
+ }
44
+ }
@@ -0,0 +1,56 @@
1
+ import { join } from 'node:path'
2
+ import { IMPORT, JSJSON } from 't0n'
3
+
4
+ import type { Metadata, I18nMetadata } from '@/types'
5
+ import { existsSync } from 'node:fs'
6
+
7
+ export default async function getI18n(meta: Metadata) {
8
+ let file = ''
9
+
10
+ for (const variant of [
11
+ 'i18n.ts',
12
+ 'i18n/index.ts',
13
+ ]) {
14
+ const path = join(meta.root, variant)
15
+ if (existsSync(path)) {
16
+ file = path
17
+ break
18
+ }
19
+ }
20
+
21
+ if (!file) return {} as I18nMetadata
22
+ const mod = await IMPORT<I18nMetadata>(file)
23
+
24
+ // @ts-ignore
25
+ if (['locales', 'fallbackLocale', 'messages', 'routes', 'options'].some(i => !mod[i]))
26
+ return {} as I18nMetadata
27
+
28
+ const i18n: I18nMetadata = {
29
+ ...mod,
30
+ routeMap: Object.fromEntries(
31
+ Object.values(mod.routes)
32
+ .map(paths => Object.entries(paths).map(([key, path]) => path == key ? 0 : [path, key]).filter(Boolean))
33
+ .flat() as Array<[string, string]>
34
+ ),
35
+ pageRoutes: [],
36
+ string: '',
37
+ }
38
+
39
+ Object.entries(mod.routes)
40
+ .forEach(([locale, paths]) => {
41
+ Object.entries(paths)
42
+ .forEach(([route, path]) => i18n.pageRoutes.push({ locale, path }))
43
+ })
44
+
45
+ i18n.string = `export const fallbackLocale = ${JSJSON(mod.fallbackLocale)}
46
+ export const locales = ${JSJSON(mod.locales)}
47
+ export const messages = ${JSJSON(mod.messages)}
48
+ export const routes = ${JSJSON(mod.routes)}
49
+ export const routeMap = ${JSJSON(i18n.routeMap)}
50
+ export const pageRoutes = ${JSJSON(i18n.pageRoutes)}
51
+ export const options = ${JSJSON(mod.options)}
52
+ `
53
+
54
+ // console.error(i18n.string)
55
+ return i18n
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ export {
2
+ Redirect,
3
+ useLocation, useParams, useSearch, useSearchParams,
4
+ matchRoute,
5
+ } from 'wouter-preact'
6
+
7
+ export {
8
+ t, r, d,
9
+ getLocale, setLocale,
10
+ isPrefixed,
11
+ } from './i18n/client'
12
+
13
+ export { local } from './client/storage'
14
+
15
+ export const navigate = <S = any>(
16
+ to: string | URL,
17
+ options?: { replace?: boolean; state?: S; transition?: boolean }
18
+ ) => {
19
+ if (window?.$duto?.sroutes?.has(to)) {
20
+ location.pathname = to
21
+ return
22
+ }
23
+
24
+ const n = window?.$duto?.navigate || useLocation()[1]
25
+ n(to, options)
26
+ }
27
+
28
+ export { Link } from './components/link'
@@ -0,0 +1,5 @@
1
+ // TODO: not used yet
2
+ import 'preact/debug'
3
+ import clientFn from './client'
4
+
5
+ export default clientFn
@@ -0,0 +1,87 @@
1
+ import { h, hydrate, render } from 'preact'
2
+ import StaticHtml from './static-html'
3
+ import type { SignalLike } from './types'
4
+
5
+ const sharedSignalMap = new Map<string, SignalLike>()
6
+
7
+ type MaskId = number | string
8
+
9
+ function getIslandMaskId(el: HTMLElement): MaskId | null {
10
+ return el.id || null
11
+ }
12
+
13
+ function setVNodeMask(child: any, maskId: MaskId) {
14
+ const mask: [MaskId, number] = [maskId, 0]
15
+ child._mask = mask
16
+ child.__m = mask
17
+ }
18
+
19
+ export default async (
20
+ el: HTMLElement,
21
+ Component: unknown,
22
+ props: Record<string, unknown>,
23
+ { default: children, ...slotted }: Record<string, unknown>,
24
+ ): Promise<void> => {
25
+ for (const [key, value] of Object.entries(slotted)) // @ts-ignore
26
+ props[key] = h(StaticHtml, { value, name: key })
27
+
28
+ const signalsRaw = el.dataset.set
29
+ if (signalsRaw) {
30
+ const { signal } = await import('@preact/signals')
31
+ const signals: Record<string, string | [string, number][]> = JSON.parse(signalsRaw)
32
+
33
+ for (const [propName, signalId] of Object.entries(signals)) {
34
+ if (Array.isArray(signalId)) {
35
+ signalId.forEach(([id, indexOrKeyInProps]) => {
36
+ const mapValue = (props[propName] as any)[indexOrKeyInProps]
37
+ let valueOfSignal = mapValue
38
+ let resolvedKey: string | number = indexOrKeyInProps
39
+
40
+ if (typeof indexOrKeyInProps !== 'string') {
41
+ valueOfSignal = mapValue[0]
42
+ resolvedKey = mapValue[1]
43
+ }
44
+
45
+ if (!sharedSignalMap.has(id))
46
+ sharedSignalMap.set(id, signal(valueOfSignal))
47
+
48
+ // @ts-ignore
49
+ props[propName][resolvedKey] = sharedSignalMap.get(id)
50
+ })
51
+ } else {
52
+ if (!sharedSignalMap.has(signalId))
53
+ sharedSignalMap.set(signalId, signal(props[propName]))
54
+
55
+ props[propName] = sharedSignalMap.get(signalId)
56
+ }
57
+ }
58
+ }
59
+
60
+ const rawChildren = children ?? props.children
61
+ delete props.children
62
+
63
+ const child = h(
64
+ Component as any,
65
+ props,
66
+ rawChildren == null
67
+ ? undefined
68
+ : typeof rawChildren === 'string'
69
+ ? h(StaticHtml, { value: rawChildren })
70
+ : rawChildren
71
+ )
72
+
73
+ const islandMaskId = getIslandMaskId(el)
74
+ if (islandMaskId !== null)
75
+ setVNodeMask(child, islandMaskId)
76
+
77
+ hydrate(child, el)
78
+
79
+ el.addEventListener('d:unmount', () => {
80
+ render(null, el)
81
+
82
+ // 2. Remove o elemento do DOM apenas após o desmonte,
83
+ // usando microtask para garantir que o Preact finalizou
84
+ // o ciclo de reconciliação antes da remoção física
85
+ Promise.resolve().then(() => el.remove())
86
+ }, { once: true })
87
+ }
@@ -0,0 +1,41 @@
1
+ import type { PropNameToSignalMap, SignalLike } from './types'
2
+
3
+ export type Context = {
4
+ id: string;
5
+ c: number;
6
+ islandCount: number;
7
+ signals: Map<SignalLike, string>;
8
+ propsToSignals: Map<Record<string, any>, PropNameToSignalMap>;
9
+ }
10
+
11
+ const contexts = new Map<string, Context>()
12
+
13
+ export function getContext(id: string): Context {
14
+ if (contexts.has(id))
15
+ return contexts.get(id)!
16
+
17
+ let ctx = {
18
+ c: 0,
19
+ islandCount: 0,
20
+ get id() {
21
+ return 'p' + this.c.toString()
22
+ },
23
+ signals: new Map(),
24
+ propsToSignals: new Map(),
25
+ }
26
+
27
+ contexts.set(id, ctx)
28
+ return ctx
29
+ }
30
+
31
+ export function incrementId(ctx: Context): string {
32
+ let id = ctx.id
33
+ ctx.c++
34
+ return id
35
+ }
36
+
37
+ export function incrementIslandId(ctx: Context): number {
38
+ const islandId = ctx.islandCount
39
+ ctx.islandCount++
40
+ return islandId
41
+ }