duto 0.261001.0 → 0.261012.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/package.json +22 -26
- package/src/commands/templates/pages/components/html.tsx +55 -0
- package/src/commands/templates/pages/components/theme-select.tsx +60 -0
- package/src/commands/templates/pages/i18n/en.ts +6 -0
- package/src/commands/templates/pages/i18n/pt-br.ts +6 -0
- package/src/commands/templates/pages/pages/404.tsx +1 -1
- package/src/commands/templates/pages/pages/blog/[slug].tsx +88 -0
- package/src/commands/templates/pages/pages/blog/index.tsx +74 -0
- package/src/commands/templates/pages/pages/index.tsx +17 -7
- package/src/commands/templates/pages/pages/layout.tsx +5 -3
- package/src/commands/templates/pages/public/favicon.ico +0 -0
- package/src/commands/templates/pages/public/favicon.svg +1 -0
- package/src/commands/templates/pages/style.css +110 -7
- package/src/commands/templates/pages/tsconfig.json +2 -0
- package/src/pages/cli/commands/build.ts +1 -2
- package/src/pages/cli/utils.ts +8 -6
- package/src/pages/client/app.tsx +177 -28
- package/src/pages/client/link.ts +15 -41
- package/src/pages/client/storage.ts +1 -1
- package/src/pages/client/theme.ts +49 -0
- package/src/pages/compiler/ast.ts +13 -0
- package/src/pages/compiler/bun-plugin.ts +245 -0
- package/src/pages/compiler/constants.ts +2 -0
- package/src/pages/compiler/index.ts +44 -23
- package/src/pages/compiler/meta.ts +7 -31
- package/src/pages/compiler/types.ts +15 -25
- package/src/pages/components/link.tsx +11 -19
- package/src/pages/i18n/client.ts +3 -4
- package/src/pages/index.ts +4 -21
- package/src/server/app.ts +2 -1
- package/src/pages/client/router.ts +0 -63
- package/src/pages/compiler/vite-plugin.ts +0 -345
package/src/pages/client/app.tsx
CHANGED
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
// @ts-ignore
|
|
2
2
|
import ':duto/hmr'
|
|
3
3
|
|
|
4
|
-
import { render,
|
|
4
|
+
import { render, h, type ComponentChild } from 'preact'
|
|
5
|
+
import { flushSync, useLayoutEffect, useState } from 'preact/compat'
|
|
5
6
|
import { Switch, Route, Router } from 'wouter-preact'
|
|
6
7
|
import { useLocation, useParams, useSearchParams, } from 'wouter-preact'
|
|
8
|
+
import type { AroundNavHandler } from 'wouter-preact'
|
|
9
|
+
import type { Params, StaticProps, View } from '#/compiler/types'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import { setThemeAttr, getTheme } from './theme'
|
|
13
|
+
|
|
14
|
+
setThemeAttr(getTheme())
|
|
7
15
|
|
|
8
16
|
// @ts-ignore
|
|
9
17
|
import { fallbackLocale, locales, routeMap } from ':duto/i18n'
|
|
10
18
|
// import i18n from '../i18n/client'
|
|
11
|
-
import i18n, {
|
|
19
|
+
import i18n, {
|
|
20
|
+
detect, isPrefixed,
|
|
21
|
+
getLocale, setLocale,
|
|
22
|
+
cache, cached,
|
|
23
|
+
r, redirect,
|
|
24
|
+
route,
|
|
25
|
+
d, t, tl,
|
|
26
|
+
updatePickers,
|
|
27
|
+
} from '../i18n/client'
|
|
12
28
|
|
|
13
29
|
await i18n(fallbackLocale, locales, {}, {}, routeMap)
|
|
14
30
|
|
|
@@ -24,31 +40,172 @@ for (let [tag, el, opts] of [
|
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
import {
|
|
27
|
-
views,
|
|
43
|
+
views,
|
|
28
44
|
// @ts-ignore
|
|
29
45
|
} from ':duto'
|
|
30
46
|
// @ts-ignore
|
|
31
47
|
import ':root/styles'
|
|
32
48
|
|
|
33
49
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
50
|
+
// var props = {}
|
|
51
|
+
// const propKey = "props"
|
|
52
|
+
// const script = document.currentScript || ('function' == typeof document.querySelector ? document.querySelector(`script[${propKey}]`) : void 0)
|
|
53
|
+
// if (script) {
|
|
54
|
+
// const opts = script.getAttribute(propKey)
|
|
55
|
+
// if (opts) {
|
|
56
|
+
// try {
|
|
57
|
+
// props = JSON.parse(opts)
|
|
58
|
+
// } catch (e) { }
|
|
59
|
+
// }
|
|
41
60
|
// }
|
|
42
|
-
import { renderRoute } from './router'
|
|
43
61
|
|
|
44
|
-
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
self.$duto = self.$duto || {}
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
Object.assign(self.$duto, {
|
|
66
|
+
// props,
|
|
67
|
+
// location: '/',
|
|
68
|
+
// navigate: {},
|
|
69
|
+
// params: {},
|
|
70
|
+
// search: {},
|
|
71
|
+
// setSearch: {},
|
|
72
|
+
i18n: {
|
|
73
|
+
detect, isPrefixed,
|
|
74
|
+
getLocale, setLocale,
|
|
75
|
+
cache, cached,
|
|
76
|
+
r, redirect,
|
|
77
|
+
route,
|
|
78
|
+
d, t, tl,
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
function normalizeParams(
|
|
84
|
+
raw: Record<string, string>,
|
|
85
|
+
paramDefs: Params
|
|
86
|
+
): Record<string, string | string[] | undefined> {
|
|
87
|
+
const result: Record<string, string | string[] | undefined> = {}
|
|
88
|
+
for (const def of paramDefs) {
|
|
89
|
+
const val = raw[def.name]
|
|
90
|
+
if (def.catchAll) {
|
|
91
|
+
result[def.name] = val
|
|
92
|
+
? val.split('/').filter(Boolean)
|
|
93
|
+
: def.optional ? undefined : []
|
|
94
|
+
} else {
|
|
95
|
+
result[def.name] = val === undefined && def.optional ? undefined : val
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveRoute(
|
|
102
|
+
[, Component, defs, getProps, layouts]: View,
|
|
103
|
+
rawParams: Record<string, string>,
|
|
104
|
+
) {
|
|
105
|
+
let props: StaticProps = defs ? normalizeParams(rawParams, defs) : { ...rawParams },
|
|
106
|
+
sProps = null
|
|
107
|
+
|
|
108
|
+
if (typeof Component === 'string') {
|
|
109
|
+
const mod = await import((Component[0] == '/' ? '' : '/')+ Component)
|
|
110
|
+
Component = mod.default
|
|
111
|
+
getProps = getProps === 1 ? mod.getProps : 0
|
|
112
|
+
sProps = mod.props ?? null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// @ts-ignore
|
|
116
|
+
// const sProps = self.$duto?.props
|
|
117
|
+
// if (sProps && typeof sProps === 'object') { // @ts-ignore
|
|
118
|
+
// self.$duto.props = null
|
|
119
|
+
// Object.assign(props, sProps)
|
|
120
|
+
// } else
|
|
121
|
+
|
|
122
|
+
if (typeof getProps === 'function') {
|
|
123
|
+
try {
|
|
124
|
+
props = {
|
|
125
|
+
...(await getProps(props as Record<string, string | string[] | undefined>, sProps)),
|
|
126
|
+
...props
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error('[ssg] getProps failed:', e)
|
|
130
|
+
}
|
|
131
|
+
} else if (sProps && typeof sProps === 'object' && !Array.isArray(sProps)) {
|
|
132
|
+
Object.assign(props, sProps)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let el = h(Component, props as any)
|
|
136
|
+
for (const layout of [layouts ?? []].flat())
|
|
137
|
+
el = h(layout, null, el)
|
|
138
|
+
|
|
139
|
+
return el
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const promises = new Map<string, Promise<ComponentChild>>()
|
|
143
|
+
const resolved = new Map<string, ComponentChild>()
|
|
144
|
+
let last: ComponentChild = null
|
|
145
|
+
|
|
146
|
+
const keyOf = (view: View, params: Record<string, string>) => view[0] + '|' + JSON.stringify(params)
|
|
147
|
+
function load(view: View, params: Record<string, string>) {
|
|
148
|
+
const key = keyOf(view, params)
|
|
149
|
+
if (!promises.has(key)) {
|
|
150
|
+
promises.set(key, resolveRoute(view, params).then((el) => {
|
|
151
|
+
resolved.set(key, el)
|
|
152
|
+
return el
|
|
153
|
+
}))
|
|
154
|
+
}
|
|
155
|
+
return promises.get(key)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function View({ view, params }: { view: View, params: Record<string, string>}) {
|
|
159
|
+
const { 0: _wild, ...clean } = params
|
|
160
|
+
const key = keyOf(view, clean)
|
|
161
|
+
|
|
162
|
+
const [loaded, setLoaded] = useState(() => resolved.get(key) ?? last)
|
|
163
|
+
const el = resolved.get(key) ?? loaded
|
|
164
|
+
if (resolved.has(key)) last = el
|
|
165
|
+
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
let cancelled = false
|
|
168
|
+
self.$duto.route = { pattern: view[0], params: clean, path: location.pathname }
|
|
169
|
+
|
|
170
|
+
load(view, clean).then(el => {
|
|
171
|
+
if (cancelled) return
|
|
172
|
+
last = el
|
|
173
|
+
setLoaded(el)
|
|
174
|
+
// self.dispatchEvent(new CustomEvent('d:load'))
|
|
175
|
+
})
|
|
176
|
+
return () => { cancelled = true }
|
|
177
|
+
}, [key])
|
|
178
|
+
|
|
179
|
+
return el
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
// enable view transitions for navigation
|
|
184
|
+
const aroundNav: AroundNavHandler = (navigate, to, options) => {
|
|
185
|
+
// feature detection for browsers that dont support View Transitions
|
|
186
|
+
if (!document.startViewTransition)
|
|
187
|
+
return navigate(to, options)
|
|
188
|
+
|
|
189
|
+
// only use view transitions if explicitly requested
|
|
190
|
+
options?.transition
|
|
191
|
+
? document.startViewTransition(() => flushSync(() => navigate(to, options)))
|
|
192
|
+
: navigate(to, options)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// clean prerendered content
|
|
196
|
+
while (document.body.firstChild)
|
|
197
|
+
document.body.removeChild(document.body.firstChild)
|
|
198
|
+
|
|
199
|
+
render(<Router aroundNav={aroundNav}>
|
|
45
200
|
{h(() => {
|
|
46
201
|
document.body.removeAttribute('hidden')
|
|
47
|
-
|
|
48
|
-
self.$duto.sroutes = SRoutes
|
|
202
|
+
|
|
49
203
|
const [location, navigate] = useLocation()
|
|
50
204
|
self.$duto.location = location
|
|
51
|
-
self.$duto.navigate =
|
|
205
|
+
self.$duto.navigate = (to,options) => {
|
|
206
|
+
to = new URL(to).pathname
|
|
207
|
+
navigate(to, options)
|
|
208
|
+
}
|
|
52
209
|
self.$duto.params = useParams()
|
|
53
210
|
|
|
54
211
|
const [search, setSearch] = useSearchParams()
|
|
@@ -56,21 +213,13 @@ render(<Router>
|
|
|
56
213
|
self.$duto.setSearch = setSearch
|
|
57
214
|
|
|
58
215
|
updatePickers()
|
|
216
|
+
self.dispatchEvent(new CustomEvent('d:load'))
|
|
59
217
|
}, null)}
|
|
60
218
|
<Switch>
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// React/Preact hook rules issues with async components)
|
|
66
|
-
|
|
67
|
-
delete params[0]
|
|
68
|
-
window.$duto.route = { pattern: path, params, path: location.pathname }
|
|
69
|
-
|
|
70
|
-
renderRoute(document.body, Component, params, defs, getProps, layouts)
|
|
71
|
-
// Return null — renderRoute will call render() directly on the container
|
|
72
|
-
return null
|
|
73
|
-
}} />
|
|
219
|
+
{views.map(v => (
|
|
220
|
+
<Route key={v[0]} path={v[0]}>
|
|
221
|
+
{(params) => <View view={v} params={params} />}
|
|
222
|
+
</Route>
|
|
74
223
|
))}
|
|
75
224
|
</Switch>
|
|
76
225
|
</Router>, document.body)
|
package/src/pages/client/link.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
self.addEventListener('d:navigate', e => {
|
|
2
|
+
const el = (e as CustomEvent<HTMLAnchorElement>).detail
|
|
3
|
+
|
|
4
|
+
console.warn('d:navigate', el)
|
|
5
|
+
if (el.href) self.$duto.navigate?.(el.href)
|
|
6
|
+
})
|
|
7
|
+
|
|
1
8
|
export default class Link extends HTMLAnchorElement {
|
|
2
9
|
static observedAttributes = [
|
|
3
10
|
'href',
|
|
@@ -12,7 +19,6 @@ export default class Link extends HTMLAnchorElement {
|
|
|
12
19
|
|
|
13
20
|
constructor() {
|
|
14
21
|
super()
|
|
15
|
-
|
|
16
22
|
this.addEventListener('click', this.#onClick)
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -21,50 +27,18 @@ export default class Link extends HTMLAnchorElement {
|
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
#onClick = (e: MouseEvent) => {
|
|
24
|
-
// Respeita comportamento padrão
|
|
25
30
|
if (
|
|
26
|
-
e.defaultPrevented
|
|
31
|
+
e.defaultPrevented // respects default behavior
|
|
27
32
|
|| this.target === '_blank'
|
|
28
|
-
|
|
29
|
-
|| e.ctrlKey
|
|
30
|
-
|| e.shiftKey
|
|
31
|
-
|| e.altKey
|
|
33
|
+
// allow ctrl/cmd+click to open in new tab normally
|
|
34
|
+
|| e.metaKey || e.ctrlKey || e.shiftKey || e.altKey
|
|
32
35
|
|| e.button !== 0
|
|
33
|
-
)
|
|
34
|
-
return
|
|
35
|
-
}
|
|
36
|
-
// Allow ctrl/cmd+click to open in new tab normally
|
|
37
|
-
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
|
38
|
-
// e.preventDefault()
|
|
39
|
-
if ((e.target as HTMLAnchorElement).hasAttribute('href'))
|
|
40
|
-
window.dispatchEvent(new CustomEvent('d:navigate', { detail: e.target }))
|
|
36
|
+
) return
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
if (this.hasAttribute('href'))
|
|
40
|
+
self.dispatchEvent(new CustomEvent('d:navigate', { detail: this }))
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
get url() {
|
|
49
|
-
return new URL(this.href)
|
|
42
|
+
// TODO: scroll from #hash to elements
|
|
50
43
|
}
|
|
51
44
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
window.addEventListener('d:navigate', e => {
|
|
55
|
-
const el = (e as CustomEvent<HTMLAnchorElement>).detail
|
|
56
|
-
|
|
57
|
-
console.warn('d:navigate', el)
|
|
58
|
-
// if (href) _navigate?.(href)
|
|
59
|
-
// _navigate?.('/haha-2')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// const links = document.querySelectorAll('a[d]')
|
|
63
|
-
// for (const link of links) { // @ts-ignore
|
|
64
|
-
// link.addEventListener('click', (e: MouseEvent) => {
|
|
65
|
-
// // Allow ctrl/cmd+click to open in new tab normally
|
|
66
|
-
// if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
|
67
|
-
// e.preventDefault()
|
|
68
|
-
// window.dispatchEvent(new CustomEvent('d:navigate', { detail: e.target }))
|
|
69
|
-
// })
|
|
70
|
-
// }
|
|
@@ -6,7 +6,7 @@ interface IStoreChange<TValue> {
|
|
|
6
6
|
old: TValue | null,
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
type IStoreListener<TValue> = (change: IStoreChange<TValue>) => void
|
|
9
|
+
export type IStoreListener<TValue> = (change: IStoreChange<TValue>) => void
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Store reativo que encapsula o localStorage.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { local, type IStoreListener } from './storage'
|
|
2
|
+
|
|
3
|
+
export type Theme = 'auto' | 'dark' | 'light'
|
|
4
|
+
|
|
5
|
+
const key = 'theme'
|
|
6
|
+
const themes = ['dark', 'light']
|
|
7
|
+
const match = () => matchMedia('(prefers-color-scheme: dark)')
|
|
8
|
+
export const preferredColorScheme = () => match().matches ? 'dark' : 'light'
|
|
9
|
+
export const setThemeAttr = (theme: Theme) => {
|
|
10
|
+
if (theme == 'auto') theme = preferredColorScheme()
|
|
11
|
+
document.documentElement.classList.toggle('dark', theme == 'dark')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isValid = (val: string) => themes.includes(val)
|
|
15
|
+
export const getTheme = () => {
|
|
16
|
+
const val = local.get<string>(key) || ''
|
|
17
|
+
return isValid(val) ? val : 'auto'
|
|
18
|
+
}
|
|
19
|
+
export const setTheme = (theme: Theme) => {
|
|
20
|
+
setThemeAttr(theme)
|
|
21
|
+
isValid(theme)
|
|
22
|
+
? local.set(key, theme)
|
|
23
|
+
: local.remove(key)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const onThemeChange = (e: IStoreListener<any>) => {
|
|
27
|
+
const mm = match()
|
|
28
|
+
mm.addEventListener('change', e)
|
|
29
|
+
return [local.subscribe(key, e), () => mm.removeEventListener('change', e)]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// export function updatePickers(theme = storedTheme || 'auto') {
|
|
33
|
+
// document.querySelectorAll('.theme-select').forEach(el => {
|
|
34
|
+
// const select = el.querySelector('select')
|
|
35
|
+
// if (select) select.value = theme
|
|
36
|
+
// const tmpl = document.querySelector(`#theme-icons`)
|
|
37
|
+
// const newIcon = tmpl && tmpl.content.querySelector('.' + theme)
|
|
38
|
+
// if (newIcon) {
|
|
39
|
+
// const oldIcon = el.querySelector('svg.label-icon')
|
|
40
|
+
// if (oldIcon) oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes)
|
|
41
|
+
// }
|
|
42
|
+
// })
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
// <template id="theme-icons">
|
|
46
|
+
// <Icon name="lucide:sun" class="light" />
|
|
47
|
+
// <Icon name="lucide:moon-star" class="dark" />
|
|
48
|
+
// <Icon name="lucide:swatch-book" class="auto" />
|
|
49
|
+
// </template>
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import { resolveAlias } from './aliases'
|
|
3
3
|
|
|
4
|
+
const IMPORT_RE = /(?:\/\/[^\n]*|\/\*[\s\S]*?\*\/)|import\s*(?:type\s*)?(?:[^'"]*?from\s*)?['"]([^'"]+)['"]/g
|
|
5
|
+
export function getImports(str: string) {
|
|
6
|
+
const found: string[] = []
|
|
7
|
+
let match: RegExpExecArray | null
|
|
8
|
+
while ((match = IMPORT_RE.exec(str))) {
|
|
9
|
+
if (match[0].includes('import type') || !match[1]) continue
|
|
10
|
+
|
|
11
|
+
found.push(match[1])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return found
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
const resolveImportCache = new Map<string, string>()
|
|
5
18
|
export const resolveImport = (lib: string, importer: string, aliases: Record<string, string>) => {
|
|
6
19
|
if (resolveImportCache.has(lib))
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
2
|
+
import { cp, mkdir } from 'node:fs/promises'
|
|
3
|
+
import { dirname, extname, join, relative } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import tailwind from 'bun-plugin-tailwind'
|
|
6
|
+
import { JSJSON } from 't0n'
|
|
7
|
+
|
|
8
|
+
import { EXT_RE } from './constants'
|
|
9
|
+
import { getChunk, setAsset, setChunk, setEntry } from './assets'
|
|
10
|
+
import { renderPage } from '.'
|
|
11
|
+
import { getImports, removeExports } from './ast'
|
|
12
|
+
import { getMetadata } from './meta'
|
|
13
|
+
import { _pages, _root } from '@/utils'
|
|
14
|
+
|
|
15
|
+
import type { BunPlugin, BuildArtifact } from 'bun'
|
|
16
|
+
import type { Metadata } from './types'
|
|
17
|
+
|
|
18
|
+
const VIRTUAL_ID = ':duto'
|
|
19
|
+
const NAMESPACE = 'duto'
|
|
20
|
+
const APP_ID = ':duto/client/app.tsx'
|
|
21
|
+
|
|
22
|
+
export type DutoOptions = {
|
|
23
|
+
root: string
|
|
24
|
+
pagesDir: string
|
|
25
|
+
outDir: string
|
|
26
|
+
plugins: BunPlugin[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const config = (opts: DutoOptions): DutoOptions => {
|
|
30
|
+
opts.root ||= _root
|
|
31
|
+
opts.pagesDir ||= 'pages'
|
|
32
|
+
opts.outDir ||= 'dist'
|
|
33
|
+
opts.plugins = [...(opts.plugins || [])]
|
|
34
|
+
return opts
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let metadata: Metadata
|
|
38
|
+
export function meta() {
|
|
39
|
+
return metadata
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const CSS_CANDIDATES = [
|
|
43
|
+
'style.css',
|
|
44
|
+
'styles.css',
|
|
45
|
+
'styles/index.css',
|
|
46
|
+
'style/index.css',
|
|
47
|
+
'tailwind.css',
|
|
48
|
+
'tailwindcss.css',
|
|
49
|
+
'styles/tailwind.css',
|
|
50
|
+
'styles/tailwindcss.css',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const REACT_ALIAS: Record<string, string> = {
|
|
54
|
+
'react': 'preact/compat',
|
|
55
|
+
'react-dom': 'preact/compat',
|
|
56
|
+
'react-dom/client': 'preact/compat/client',
|
|
57
|
+
'react-dom/test-utils': 'preact/test-utils',
|
|
58
|
+
'react/jsx-runtime': 'preact/jsx-runtime',
|
|
59
|
+
'react/jsx-dev-runtime': 'preact/jsx-dev-runtime',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const stripExt = (p: string) => p.slice(0, p.length - extname(p).length)
|
|
63
|
+
|
|
64
|
+
export async function build(opts: DutoOptions) {
|
|
65
|
+
const conf = config(opts)
|
|
66
|
+
const outPath = join(conf.root, conf.outDir)
|
|
67
|
+
|
|
68
|
+
const PAGES_RE = new RegExp(`/${conf.pagesDir}/.*\\.(tsx?|jsx?)$`, 'i')
|
|
69
|
+
const stripTarget = new Set(['getStaticProps'])
|
|
70
|
+
|
|
71
|
+
metadata = await getMetadata(conf.root, conf.pagesDir)
|
|
72
|
+
|
|
73
|
+
const appEntry = join(_pages, 'client', 'app.tsx')
|
|
74
|
+
setEntry(APP_ID, relative(conf.root, appEntry))
|
|
75
|
+
|
|
76
|
+
const routes = metadata.routes.filter(r => !r.is404)
|
|
77
|
+
const entrypoints = [...routes.map(r => r.fullFilePath), appEntry]
|
|
78
|
+
|
|
79
|
+
const sources = new Map(entrypoints.map(e => [relative(conf.root, stripExt(e)), e]))
|
|
80
|
+
|
|
81
|
+
const prefix = metadata.minify ? '' : '[name].'
|
|
82
|
+
|
|
83
|
+
rmSync(outPath, { recursive: true, force: true })
|
|
84
|
+
await mkdir(outPath, { recursive: true })
|
|
85
|
+
|
|
86
|
+
const result = await Bun.build({
|
|
87
|
+
entrypoints,
|
|
88
|
+
root: conf.root,
|
|
89
|
+
target: 'browser',
|
|
90
|
+
format: 'esm',
|
|
91
|
+
splitting: true,
|
|
92
|
+
minify: metadata.minify,
|
|
93
|
+
sourcemap: 'none',
|
|
94
|
+
naming: {
|
|
95
|
+
entry: '[dir]/[name]-[hash6].[ext]', // used to map output to source
|
|
96
|
+
chunk: prefix + '[hash6].[ext]',
|
|
97
|
+
asset: prefix + '[hash6].[ext]',
|
|
98
|
+
},
|
|
99
|
+
define: {
|
|
100
|
+
'process.env.NODE_ENV': '"production"',
|
|
101
|
+
},
|
|
102
|
+
plugins: [
|
|
103
|
+
...(conf.plugins || []),
|
|
104
|
+
tailwind,
|
|
105
|
+
{
|
|
106
|
+
name: 'duto',
|
|
107
|
+
setup(build) {
|
|
108
|
+
build.onResolve({ filter: /^react(-dom)?(\/|$)/ }, args => {
|
|
109
|
+
const target = REACT_ALIAS[args.path]
|
|
110
|
+
if (!target) return
|
|
111
|
+
return { path: Bun.resolveSync(target, conf.root) }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
build.onResolve({ filter: /^:root/ }, args => {
|
|
115
|
+
for (const f of CSS_CANDIDATES) {
|
|
116
|
+
const path = join(conf.root, f)
|
|
117
|
+
if (existsSync(path)) return { path }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
build.onResolve({ filter: /^:duto/ }, args => {
|
|
122
|
+
const id = args.path
|
|
123
|
+
|
|
124
|
+
if (EXT_RE.test(id) && id.includes('/')) {
|
|
125
|
+
const path = join(_pages, id.replace(VIRTUAL_ID, ''))
|
|
126
|
+
return { path }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (id.endsWith('/hmr') && metadata.hmr)
|
|
130
|
+
return { path: join(_pages, 'client', id.replace(VIRTUAL_ID, '') + '.ts') }
|
|
131
|
+
|
|
132
|
+
return { path: id, namespace: NAMESPACE }
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, args => ({
|
|
136
|
+
contents: args.path.endsWith('/i18n') ? metadata.i18n.string : metadata.string,
|
|
137
|
+
loader: 'ts',
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
build.onLoad({ filter: PAGES_RE }, async args => {
|
|
141
|
+
if (args.path.includes('node_modules')) return
|
|
142
|
+
|
|
143
|
+
const code = await Bun.file(args.path).text()
|
|
144
|
+
const stripped = removeExports(args.path, code, stripTarget)
|
|
145
|
+
if (!stripped || stripped === code) return
|
|
146
|
+
|
|
147
|
+
const ext = extname(args.path).slice(1) as 'ts' | 'tsx' | 'js' | 'jsx'
|
|
148
|
+
return { contents: stripped, loader: ext }
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
if (!result.success)
|
|
156
|
+
throw new AggregateError(result.logs, 'duto: build failed')
|
|
157
|
+
|
|
158
|
+
const keyOf = (a: BuildArtifact, fileName: string) => {
|
|
159
|
+
let noExt = stripExt(fileName).replaceAll('_.._', '..')
|
|
160
|
+
if (a.hash) noExt = noExt.replace(new RegExp(`-${a.hash}$`), '')
|
|
161
|
+
return noExt
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const dirs = new Set<string>()
|
|
165
|
+
for (const out of result.outputs) {
|
|
166
|
+
const fileName = out.path.replace(/^\.\//, '').replaceAll('_.._', '..')
|
|
167
|
+
const dest = metadata.minify ? fileName.split('/').at(-1)!.split('-').at(-1)! : fileName.replace('../../packages/duto/src/pages', '')
|
|
168
|
+
const filePath = join(outPath, dest)
|
|
169
|
+
const key = keyOf(out, fileName)
|
|
170
|
+
const owner = sources.get(key)
|
|
171
|
+
|
|
172
|
+
const fileDir = dirname(filePath)
|
|
173
|
+
if (fileDir !== outPath && !dirs.has(fileDir)) {
|
|
174
|
+
dirs.add(fileDir)
|
|
175
|
+
await mkdir(fileDir, { recursive: true })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let content = await out.text()
|
|
179
|
+
if (fileName.endsWith('.css')) {
|
|
180
|
+
if (owner === appEntry) content += 'i-i{display:contents}'
|
|
181
|
+
content = content.replace(/\/\*!\s*tailwindcss[^*]*\*\/\s*/g, '')
|
|
182
|
+
|
|
183
|
+
setAsset(relative(conf.root, owner ?? fileName), '/'+ dest)
|
|
184
|
+
} else if (out.kind === 'entry-point' && owner) {
|
|
185
|
+
setChunk(relative(conf.root, owner),'/'+ dest)
|
|
186
|
+
// o lookup original usa o caminho relativo ao root
|
|
187
|
+
// if (owner !== appEntry) setChunk(owner.replace(conf.root, ''), name)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const imports = getImports(content)
|
|
191
|
+
for (const path of imports) {
|
|
192
|
+
content = content.replaceAll(path, '/'+ path.split('/').at(-1))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await Bun.write(filePath, content)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const [_filePath, page] of metadata.pages)
|
|
199
|
+
await renderPage(metadata, conf, page)
|
|
200
|
+
|
|
201
|
+
rmSync(join(conf.root, 'tmp'), { recursive: true, force: true })
|
|
202
|
+
|
|
203
|
+
if (existsSync(join(conf.root, 'public')))
|
|
204
|
+
await cp(join(conf.root, 'public'), outPath, { recursive: true, force: true })
|
|
205
|
+
|
|
206
|
+
const entryPath = join(outPath, getChunk(APP_ID))
|
|
207
|
+
const entry = Bun.file(entryPath)
|
|
208
|
+
|
|
209
|
+
if (await entry.exists()) {
|
|
210
|
+
let entryContent = await entry.text()
|
|
211
|
+
|
|
212
|
+
for (const r of routes) {
|
|
213
|
+
const rPath = getChunk(r.fullFilePath.replace(conf.root, ''))
|
|
214
|
+
|
|
215
|
+
if (r.props) {
|
|
216
|
+
const rFilePath = join(outPath, rPath)
|
|
217
|
+
const rText = await Bun.file(rFilePath).text()
|
|
218
|
+
await Bun.write(rFilePath, `export const props=${JSJSON(r.props)};${rText}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
entryContent = entryContent.replace(r.fullFilePath, rPath)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await Bun.write(entryPath, entryContent)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// i18n
|
|
228
|
+
for (const locale of metadata.i18n.locales) {
|
|
229
|
+
const intlPath = join(conf.root, 'i18n', locale + '.ts')
|
|
230
|
+
if (!existsSync(intlPath)) continue
|
|
231
|
+
|
|
232
|
+
const intlMod = await import(intlPath)
|
|
233
|
+
await Bun.write(
|
|
234
|
+
join(outPath, locale + '.js'),
|
|
235
|
+
`var a=${JSJSON({
|
|
236
|
+
routes: intlMod.routes ?? intlMod.default?.routes,
|
|
237
|
+
messages: intlMod.messages ?? intlMod.default?.messages,
|
|
238
|
+
})};export{a as default}`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return result
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default config
|
|
@@ -7,6 +7,8 @@ export const DOCTYPE_RE = /<!doctype html/i
|
|
|
7
7
|
|
|
8
8
|
// export const USE_CLIENT_RE = /^[\s\r\n]*["']use client["'](?=[\s\r\n]|$)/m
|
|
9
9
|
export const USE_CLIENT_RE = /^(?:\s|\/\/.*\n|\/\*[\s\S]*?\*\/)*["']use client["'](?=\s|;|$)/
|
|
10
|
+
export const USE_RAW_RE = /^(?:\s|\/\/.*\n|\/\*[\s\S]*?\*\/)*["']use raw["'](?=\s|;|$)/
|
|
11
|
+
export const USE_HTML_RE = /^(?:\s|\/\/.*\n|\/\*[\s\S]*?\*\/)*["']use html["'](?=\s|;|$)/
|
|
10
12
|
|
|
11
13
|
export const LAYOUT_NAMES = ['_layout', 'layout']
|
|
12
14
|
export const NF_NAMES = ['404', 'not-found']
|