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.
Files changed (32) hide show
  1. package/package.json +22 -26
  2. package/src/commands/templates/pages/components/html.tsx +55 -0
  3. package/src/commands/templates/pages/components/theme-select.tsx +60 -0
  4. package/src/commands/templates/pages/i18n/en.ts +6 -0
  5. package/src/commands/templates/pages/i18n/pt-br.ts +6 -0
  6. package/src/commands/templates/pages/pages/404.tsx +1 -1
  7. package/src/commands/templates/pages/pages/blog/[slug].tsx +88 -0
  8. package/src/commands/templates/pages/pages/blog/index.tsx +74 -0
  9. package/src/commands/templates/pages/pages/index.tsx +17 -7
  10. package/src/commands/templates/pages/pages/layout.tsx +5 -3
  11. package/src/commands/templates/pages/public/favicon.ico +0 -0
  12. package/src/commands/templates/pages/public/favicon.svg +1 -0
  13. package/src/commands/templates/pages/style.css +110 -7
  14. package/src/commands/templates/pages/tsconfig.json +2 -0
  15. package/src/pages/cli/commands/build.ts +1 -2
  16. package/src/pages/cli/utils.ts +8 -6
  17. package/src/pages/client/app.tsx +177 -28
  18. package/src/pages/client/link.ts +15 -41
  19. package/src/pages/client/storage.ts +1 -1
  20. package/src/pages/client/theme.ts +49 -0
  21. package/src/pages/compiler/ast.ts +13 -0
  22. package/src/pages/compiler/bun-plugin.ts +245 -0
  23. package/src/pages/compiler/constants.ts +2 -0
  24. package/src/pages/compiler/index.ts +44 -23
  25. package/src/pages/compiler/meta.ts +7 -31
  26. package/src/pages/compiler/types.ts +15 -25
  27. package/src/pages/components/link.tsx +11 -19
  28. package/src/pages/i18n/client.ts +3 -4
  29. package/src/pages/index.ts +4 -21
  30. package/src/server/app.ts +2 -1
  31. package/src/pages/client/router.ts +0 -63
  32. package/src/pages/compiler/vite-plugin.ts +0 -345
@@ -1,14 +1,30 @@
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'
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, { updatePickers } 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'
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, SRoutes,
43
+ views,
28
44
  // @ts-ignore
29
45
  } from ':duto'
30
46
  // @ts-ignore
31
47
  import ':root/styles'
32
48
 
33
49
 
34
- // window.$duto = {
35
- // sroutes: SRoutes,
36
- // location: '/',
37
- // navigate: {},
38
- // params: {},
39
- // search: {},
40
- // setSearch: {},
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
- render(<Router>
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
- self.$duto = self.$duto || {}
48
- self.$duto.sroutes = SRoutes
202
+
49
203
  const [location, navigate] = useLocation()
50
204
  self.$duto.location = location
51
- self.$duto.navigate = 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
- {/* @ts-ignore */}
62
- {...views.map(([path, Component, defs, getProps, layouts]) => (
63
- <Route path={path} component={({ params }: { params: Record<string, string> }) => {
64
- // Trigger async render (updates container imperatively to avoid
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)
@@ -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,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']