duto 0.260915.0 → 0.261011.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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/commands/new.ts +55 -30
  4. package/src/commands/templates/pages/README.md +21 -0
  5. package/src/commands/templates/pages/_gitignore +24 -0
  6. package/src/commands/templates/pages/components/html.tsx +55 -0
  7. package/src/commands/templates/pages/components/locale-select.tsx +59 -0
  8. package/src/commands/templates/pages/components/select.tsx +95 -0
  9. package/src/commands/templates/pages/components/theme-select.tsx +60 -0
  10. package/src/commands/templates/pages/i18n/en.ts +23 -0
  11. package/src/commands/templates/pages/i18n/index.ts +19 -0
  12. package/src/commands/templates/pages/i18n/pt-br.ts +23 -0
  13. package/src/commands/templates/pages/package.json +22 -0
  14. package/src/commands/templates/pages/pages/404.tsx +12 -0
  15. package/src/commands/templates/pages/pages/blog/[slug].tsx +88 -0
  16. package/src/commands/templates/pages/pages/blog/index.tsx +74 -0
  17. package/src/commands/templates/pages/pages/index.tsx +26 -0
  18. package/src/commands/templates/pages/pages/layout.tsx +26 -0
  19. package/src/commands/templates/pages/public/favicon.ico +0 -0
  20. package/src/commands/templates/pages/public/favicon.svg +1 -0
  21. package/src/commands/templates/pages/style.css +127 -0
  22. package/src/commands/templates/pages/tsconfig.json +56 -0
  23. package/src/commands/templates/pages/types.ts +8 -0
  24. package/src/commands/templates/server/base/.editorconfig +21 -0
  25. package/src/commands/templates/{base → server/base}/README.md +1 -1
  26. package/src/pages/cli/commands/dev.ts +10 -4
  27. package/src/pages/cli/utils.ts +7 -23
  28. package/src/pages/client/app.tsx +193 -91
  29. package/src/pages/client/link.ts +15 -41
  30. package/src/pages/client/storage.ts +1 -1
  31. package/src/pages/client/theme.ts +49 -0
  32. package/src/pages/compiler/aliases.ts +2 -8
  33. package/src/pages/compiler/ast.ts +1 -538
  34. package/src/pages/compiler/constants.ts +97 -13
  35. package/src/pages/compiler/index.ts +55 -140
  36. package/src/pages/compiler/meta.ts +40 -253
  37. package/src/pages/compiler/routes.ts +4 -5
  38. package/src/pages/compiler/types.ts +16 -125
  39. package/src/pages/compiler/vite-plugin.ts +64 -27
  40. package/src/pages/components/link.tsx +11 -19
  41. package/src/pages/i18n/client.ts +21 -10
  42. package/src/pages/i18n/server.ts +21 -6
  43. package/src/pages/index.ts +4 -22
  44. package/src/server/app.ts +2 -1
  45. package/src/server/routes.ts +4 -26
  46. package/src/utils.ts +34 -3
  47. package/src/pages/client/router.ts +0 -329
  48. package/src/pages/compiler/island.ts +0 -108
  49. /package/src/commands/templates/{base → pages}/.editorconfig +0 -0
  50. /package/src/commands/templates/{base → server/base}/.env.dev.example +0 -0
  51. /package/src/commands/templates/{base → server/base}/.env.prod.example +0 -0
  52. /package/src/commands/templates/{base → server/base}/_gitignore +0 -0
  53. /package/src/commands/templates/{base → server/base}/actions/blog/index.get.ts +0 -0
  54. /package/src/commands/templates/{base → server/base}/actions/blog/post/[id].get.ts +0 -0
  55. /package/src/commands/templates/{base → server/base}/actions/index.get.ts +0 -0
  56. /package/src/commands/templates/{base → server/base}/configs/duto.ts +0 -0
  57. /package/src/commands/templates/{base → server/base}/configs/roles.ts +0 -0
  58. /package/src/commands/templates/{base → server/base}/package.json +0 -0
  59. /package/src/commands/templates/{base → server/base}/tsconfig.json +0 -0
  60. /package/src/commands/templates/{workerd → server/workerd}/wrangler.toml +0 -0
@@ -1,123 +1,225 @@
1
1
  // @ts-ignore
2
2
  import ':duto/hmr'
3
3
 
4
- import { render, hydrate, h } from 'preact'
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'
7
10
 
8
- import {
9
- fallbackLocale, locales, messages, routes, routeMap,
10
- // @ts-ignore
11
- } from ':duto/i18n'
12
- // import i18n from '../i18n/client'
13
- import i18n, { updatePickers } from '../i18n/client'
14
11
 
15
- i18n(fallbackLocale, locales, messages, routes, routeMap)
12
+ import { setThemeAttr, getTheme } from './theme'
16
13
 
14
+ setThemeAttr(getTheme())
15
+
16
+ // @ts-ignore
17
+ import { fallbackLocale, locales, routeMap } from ':duto/i18n'
18
+ // import i18n from '../i18n/client'
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'
28
+
29
+ await i18n(fallbackLocale, locales, {}, {}, routeMap)
30
+
31
+ // Custom elements
17
32
  import EI18n from '../i18n/element'
18
- import Island from './island'
19
33
  import Link from './link'
20
34
 
21
- // import Router from './router'
35
+ for (let [tag, el, opts] of [
36
+ ['i-i', EI18n],
37
+ ['i-l', Link, { extends: 'a' }],
38
+ ] as [string, new () => HTMLElement, ElementDefinitionOptions?][]) {
39
+ !customElements.get(tag) && customElements.define(tag, el, opts)
40
+ }
22
41
 
23
42
  import {
24
- views, SRoutes,
43
+ views,
25
44
  // @ts-ignore
26
45
  } from ':duto'
27
46
  // @ts-ignore
28
47
  import ':root/styles'
29
48
 
30
- window.$duto = {
31
- sroutes: SRoutes,
32
- location: '/',
33
- navigate: {},
34
- params: {},
35
- search: {},
36
- setSearch: {},
49
+
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
+ // }
60
+ // }
61
+
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
37
99
  }
38
- import { renderRoute } from './router'
39
- // import { navigate } from 'wouter-preact/use-hash-location'
40
100
 
41
- for (let [tag, el, opts] of [
42
- ['i-i', EI18n],
43
- ['i-l', Link, { extends: 'a' }],
44
- ['i-s', Island],
45
- ] as [string, new () => HTMLElement, ElementDefinitionOptions?][]) {
46
- if (!customElements.get(tag))
47
- customElements.define(tag, el, opts)
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(/* @vite-ignore */ 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
48
180
  }
49
- // Router.load(SRoutes).mount(views)
50
-
51
- // render(h(Router, null,
52
- // h(() => {
53
- // // const __ = useRoute(location.pathname)
54
- // document.body.removeAttribute('hidden')
55
- // const [, navigate] = useLocation()
56
- // // console.log(navigate)
57
- // // this.navigate = navigate
58
-
59
- // // TODO: talvez registrar todos os valores pelo id do elemento em uma fila e atualizar nesse tick...
60
- // // input, select etc registra um update val e aqui popula..
61
- // updatePickers()
62
- // return null
63
- // }, null),
64
- // h(Switch, null,
65
- // h(Route, { path: SRoutes.has(location.pathname) ? location.pathname : '/' }),
66
- // ...views.map(([path, Component, defs, getProps]) =>
67
- // h(Route, {
68
- // path,
69
- // component: ({ params }: { params: Record<string, string> }) => {
70
- // // Trigger async render (updates container imperatively to avoid
71
- // // React/Preact hook rules issues with async components)
72
-
73
- // renderRoute(document.body, Component, params, defs, getProps)
74
- // // Return null — renderRoute will call render() directly on the container
75
- // return null
76
- // },
77
- // })
78
- // ),
79
-
80
- // ) ), document.body)
81
- render(<Router>
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}>
82
200
  {h(() => {
83
201
  document.body.removeAttribute('hidden')
202
+
84
203
  const [location, navigate] = useLocation()
85
- window.$duto.location = location
86
- window.$duto.navigate = navigate
87
- window.$duto.params = useParams()
204
+ self.$duto.location = location
205
+ self.$duto.navigate = (to,options) => {
206
+ to = new URL(to).pathname
207
+ navigate(to, options)
208
+ }
209
+ self.$duto.params = useParams()
210
+
88
211
  const [search, setSearch] = useSearchParams()
89
- window.$duto.search = search
90
- window.$duto.setSearch = setSearch
212
+ self.$duto.search = search
213
+ self.$duto.setSearch = setSearch
91
214
 
92
- // TODO: talvez registrar todos os valores pelo id do elemento em uma fila e atualizar nesse tick...
93
- // input, select etc registra um update val e aqui popula..
94
215
  updatePickers()
95
-
96
- // React.useEffect(() => {
97
- // document.body.removeAttribute('hidden');
98
- // navigateRef.current = navigate;
99
- // updatePickers();
100
- // }, [navigate]);
101
- // return null;
216
+ self.dispatchEvent(new CustomEvent('d:load'))
102
217
  }, null)}
103
218
  <Switch>
104
- <Route path={SRoutes.has(location.pathname) ? location.pathname : '/'} component={({ params }) => {
105
- delete params[0]
106
- window.$duto.route = { pattern: location.pathname, params, path: location.pathname }
107
- }} />
108
- {/* @ts-ignore */}
109
- {...views.map(([path, Component, defs, getProps, layouts]) => (
110
- <Route path={path} component={({ params }: { params: Record<string, string> }) => {
111
- // Trigger async render (updates container imperatively to avoid
112
- // React/Preact hook rules issues with async components)
113
-
114
- delete params[0]
115
- window.$duto.route = { pattern: path, params, path: location.pathname }
116
-
117
- renderRoute(document.body, Component, params, defs, getProps, layouts)
118
- // Return null — renderRoute will call render() directly on the container
119
- return null
120
- }} />
219
+ {views.map(v => (
220
+ <Route key={v[0]} path={v[0]}>
221
+ {(params) => <View view={v} params={params} />}
222
+ </Route>
121
223
  ))}
122
224
  </Switch>
123
225
  </Router>, document.body)
@@ -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
- || e.metaKey
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
- // Exemplo: interceptar navegação SPA
43
- // event.preventDefault()
44
- // router.push(this.href)
45
- }
38
+ e.preventDefault()
39
+ if (this.hasAttribute('href'))
40
+ self.dispatchEvent(new CustomEvent('d:navigate', { detail: this }))
46
41
 
47
- // Proxy opcional para facilitar typings
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,14 +1,8 @@
1
1
  import { join, resolve } from 'node:path'
2
- import { readFileSync } from 'node:fs'
3
2
  import { resolveExtension } from './file'
4
3
 
5
- export function getTsConfig(configPath = './tsconfig.json') {
6
- const content = readFileSync(configPath, 'utf8')
7
- return JSON.parse(content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''))
8
- }
9
-
10
- export function getAliases(root: string) {
11
- const tsconfig = getTsConfig(join(root, './tsconfig.json'))
4
+ export async function getAliases(root: string) {
5
+ const tsconfig = await import(join(root, './tsconfig.json')) // @ts-ignore
12
6
  const compilerOptions = tsconfig.compilerOptions || {}
13
7
  const paths = Object.entries(compilerOptions.paths || {})
14
8
  const baseUrl = compilerOptions.baseUrl || '.'