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.
- package/LICENSE +21 -0
- package/README.md +21 -12
- package/package.json +56 -2
- package/src/cli/commands/build.ts +74 -0
- package/src/cli/commands/dev-old.ts +213 -0
- package/src/cli/commands/dev.ts +87 -0
- package/src/cli/commands/preview.ts +33 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/utils.ts +146 -0
- package/src/client/app.tsx +123 -0
- package/src/client/hmr.ts +13 -0
- package/src/client/island.ts +136 -0
- package/src/client/link.ts +70 -0
- package/src/client/router.ts +329 -0
- package/src/client/storage.ts +95 -0
- package/src/compiler/aliases.ts +36 -0
- package/src/compiler/assets.ts +43 -0
- package/src/compiler/ast.ts +764 -0
- package/src/compiler/constants.ts +24 -0
- package/src/compiler/file.ts +60 -0
- package/src/compiler/index.ts +307 -0
- package/src/compiler/island.ts +108 -0
- package/src/compiler/meta.ts +601 -0
- package/src/compiler/routes.ts +29 -0
- package/src/compiler/types.ts +209 -0
- package/src/compiler/utils.ts +5 -0
- package/src/compiler/vite-plugin.ts +324 -0
- package/src/components/link.tsx +38 -0
- package/src/i18n/client.ts +403 -0
- package/src/i18n/element.ts +44 -0
- package/src/i18n/server.ts +56 -0
- package/src/index.ts +28 -0
- package/src/preact/client-dev.ts +5 -0
- package/src/preact/client.ts +87 -0
- package/src/preact/context.ts +41 -0
- package/src/preact/server.ts +186 -0
- package/src/preact/signals.ts +89 -0
- package/src/preact/static-html.ts +45 -0
- package/src/preact/types.ts +26 -0
|
@@ -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
|
+
}
|