duto 0.0.0 → 0.260906.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -12
  3. package/package.json +91 -2
  4. package/src/cli.ts +42 -0
  5. package/src/enum.ts +2 -0
  6. package/src/index.d.ts +61 -0
  7. package/src/index.ts +19 -0
  8. package/src/middleware.ts +2 -0
  9. package/src/pages/client/app.tsx +123 -0
  10. package/src/pages/client/hmr.ts +13 -0
  11. package/src/pages/client/island.ts +136 -0
  12. package/src/pages/client/link.ts +70 -0
  13. package/src/pages/client/router.ts +329 -0
  14. package/src/pages/client/storage.ts +95 -0
  15. package/src/pages/commands/build.ts +74 -0
  16. package/src/pages/commands/dev-old.ts +213 -0
  17. package/src/pages/commands/dev.ts +87 -0
  18. package/src/pages/commands/preview.ts +33 -0
  19. package/src/pages/compiler/aliases.ts +36 -0
  20. package/src/pages/compiler/assets.ts +43 -0
  21. package/src/pages/compiler/ast.ts +764 -0
  22. package/src/pages/compiler/constants.ts +24 -0
  23. package/src/pages/compiler/file.ts +60 -0
  24. package/src/pages/compiler/index.ts +307 -0
  25. package/src/pages/compiler/island.ts +108 -0
  26. package/src/pages/compiler/meta.ts +601 -0
  27. package/src/pages/compiler/routes.ts +29 -0
  28. package/src/pages/compiler/types.ts +209 -0
  29. package/src/pages/compiler/utils.ts +5 -0
  30. package/src/pages/compiler/vite-plugin.ts +324 -0
  31. package/src/pages/components/link.tsx +38 -0
  32. package/src/pages/i18n/client.ts +403 -0
  33. package/src/pages/i18n/element.ts +44 -0
  34. package/src/pages/i18n/server.ts +56 -0
  35. package/src/pages/index.ts +28 -0
  36. package/src/pages/preact/client-dev.ts +5 -0
  37. package/src/pages/preact/client.ts +87 -0
  38. package/src/pages/preact/context.ts +41 -0
  39. package/src/pages/preact/server.ts +186 -0
  40. package/src/pages/preact/signals.ts +89 -0
  41. package/src/pages/preact/static-html.ts +45 -0
  42. package/src/pages/preact/types.ts +26 -0
  43. package/src/server/adapter/aws.ts +4 -0
  44. package/src/server/adapter/workerd.ts +4 -0
  45. package/src/server/app.ts +96 -0
  46. package/src/server/auth/ability.ts +72 -0
  47. package/src/server/auth/authnz.ts +86 -0
  48. package/src/server/auth/index.ts +5 -0
  49. package/src/server/auth/token.ts +147 -0
  50. package/src/server/auth/types.ts +2 -0
  51. package/src/server/cli/commands/build.ts +41 -0
  52. package/src/server/cli/commands/deploy.ts +82 -0
  53. package/src/server/cli/commands/dev.ts +277 -0
  54. package/src/server/cli/commands/make.ts +98 -0
  55. package/src/server/cli/commands/migrate.ts +93 -0
  56. package/src/server/cli/commands/routes.ts +79 -0
  57. package/src/server/cli/index.ts +27 -0
  58. package/src/server/cli/stubs.ts +39 -0
  59. package/src/server/cli/types.ts +3 -0
  60. package/src/server/cli/utils.ts +443 -0
  61. package/src/server/config.ts +4 -0
  62. package/src/server/db/d1.d.ts +1 -0
  63. package/src/server/db/d1.ts +1 -0
  64. package/src/server/db/dynamodb.d.ts +1 -0
  65. package/src/server/db/dynamodb.ts +1 -0
  66. package/src/server/db/index.ts +1 -0
  67. package/src/server/entry.ts +19 -0
  68. package/src/server/handle.ts +30 -0
  69. package/src/server/http.ts +23 -0
  70. package/src/server/middlewares/auth.ts +40 -0
  71. package/src/server/middlewares/logger.ts +30 -0
  72. package/src/server/open-api/register.ts +29 -0
  73. package/src/server/open-api/spec.ts +107 -0
  74. package/src/server/request.ts +233 -0
  75. package/src/server/response.ts +142 -0
  76. package/src/server/routes.ts +474 -0
  77. package/src/server/types.ts +142 -0
  78. package/src/server/utils/environment.ts +17 -0
  79. package/src/server/utils.ts +46 -0
  80. package/src/server/validator.ts +70 -0
  81. package/src/utils.ts +5 -0
@@ -0,0 +1,24 @@
1
+ export const PAGE_RE = /\/pages\/.*\.(tsx?|jsx?)$/i
2
+ export const EXT_RE = /\.(tsx?|jsx?)$/i
3
+ export const DYNAMIC_RE = /\[/
4
+ export const INTERNAL_RE = /(?:.*\/)?_?(layout|error|template|loading)\.(tsx?|jsx?)$/i
5
+ export const LAYOUT_RE = /(?:.*\/)?_?(layout)\.(tsx?|jsx?)$/i
6
+ export const DOCTYPE_RE = /<!doctype html/i
7
+
8
+ // export const USE_CLIENT_RE = /^[\s\r\n]*["']use client["'](?=[\s\r\n]|$)/m
9
+ export const USE_CLIENT_RE = /^(?:\s|\/\/.*\n|\/\*[\s\S]*?\*\/)*["']use client["'](?=\s|;|$)/
10
+ // export const IMPORT_RE = /import\s+(?:type\s+)?(?:[\w*\s{},]+from\s+)?['"]([^'"]+)['"]/g
11
+ export const IMPORT_RE = /(?:\/\/.*|\/\*[\s\S]*?\*\/)|import\s+(?:type\s+)?(?:[\w*\s{},]+from\s+)?['"]([^'"]+)['"]/g
12
+ export const BROWSER_ONLY = [
13
+ 'preact', '@preact/signals',
14
+ 'react', 'react-dom',
15
+ 'wouter', 'wouter-preact',
16
+ 'framer-motion', 'gsap', 'three',
17
+ ]
18
+ export const BROWSER_ONLY_SET = new Set(BROWSER_ONLY)
19
+
20
+ export const LAYOUT_NAMES = ['_layout', 'layout']
21
+ export const NF_NAMES = ['404', 'not-found']
22
+ export const FILE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'] as const
23
+ export const CONFIG_EXTENSIONS = ['json', 'toml', 'yaml'] as const
24
+ export const EXTENSIONS = [...FILE_EXTENSIONS, ...CONFIG_EXTENSIONS] as const
@@ -0,0 +1,60 @@
1
+ import { extname, join } from 'node:path'
2
+ import { existsSync } from 'node:fs'
3
+ import { EXTENSIONS, FILE_EXTENSIONS } from './constants'
4
+ import type { Metadata } from '@/types'
5
+
6
+ const extensionCache = new Map<string, string | null>()
7
+ export function resolveExtension(filePath: string): string | null {
8
+ const cacheKey = extname(filePath)
9
+ ? filePath.slice(0, -extname(filePath).length)
10
+ : filePath
11
+
12
+ const cached = extensionCache.get(cacheKey)
13
+ if (cached !== undefined) return cached
14
+
15
+ const result = resolveExtensionUncached(filePath)
16
+ extensionCache.set(cacheKey, result)
17
+ return result
18
+ }
19
+
20
+ export function resolveExtensionUncached(filePath: string): string | null {
21
+ if (extname(filePath))
22
+ return existsSync(filePath) ? filePath : null
23
+
24
+ for (const ext of EXTENSIONS) {
25
+ const direct = `${filePath}.${ext}`
26
+ if (existsSync(direct)) return direct
27
+
28
+ const index = join(filePath, `index.${ext}`)
29
+ if (existsSync(index)) return index
30
+ }
31
+
32
+ return null
33
+ }
34
+
35
+ export function findFileByName(meta: Metadata, dir: string, baseName: string): string | null {
36
+ for (const extension of FILE_EXTENSIONS) {
37
+ const candidate = join(meta.root, 'pages', dir, `_${baseName}.${extension}`)
38
+ if (existsSync(candidate))
39
+ return candidate
40
+ }
41
+
42
+ return null
43
+ }
44
+
45
+ // export function fileWhitoutExt(filePath: string): string {
46
+ // const parsed = parse(filePath)
47
+ // let name = parsed.name
48
+ // while (name.includes('.'))
49
+ // name = name.replace(/\.[^/.]+`$/, '')
50
+
51
+ // return join(parsed.dir, name)
52
+ // }
53
+
54
+ // export async function ensureDir(dir: string): Promise<void> {
55
+ // try {
56
+ // await access(dir)
57
+ // } catch (error) {
58
+ // await mkdir(dir, { recursive: true })
59
+ // }
60
+ // }
@@ -0,0 +1,307 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import { join, dirname } from 'node:path'
3
+ import { getLayouts, parsePage, resolveStaticOutputPath } from './meta'
4
+
5
+ import { h } from 'preact'
6
+ import { render } from 'preact-render-to-string'
7
+ import minify from '@minify-html/node'
8
+
9
+ import type { ResolvedConfig } from 'vite'
10
+ import { DOCTYPE_RE } from './constants'
11
+ import { getAsset, getChunk, normalizeId } from './assets'
12
+
13
+ import type { IRoute, Metadata, RouteParams, StaticProps, SummaryMetadata } from './types'
14
+
15
+
16
+ // import i18n from '../client/i18n'
17
+ import { setLocale } from '../i18n/client'
18
+ import { log, substep } from 't0n/log'
19
+
20
+
21
+
22
+ import { promisify } from 'node:util'
23
+ import { brotliCompress } from 'node:zlib'
24
+
25
+ const brCompress = promisify(brotliCompress);
26
+
27
+
28
+ export const selfCloseEmptyTags = (html: string, tagName: string) => html.replace(new RegExp(`<${tagName}\\b([^>]*?)>\\s*</${tagName}>`, 'g'), (_, attrs) => `<${tagName}${attrs} />`)
29
+
30
+ export async function renderPage(
31
+ meta: Metadata,
32
+ config: ResolvedConfig,
33
+ page: IRoute
34
+ ) {
35
+ const {
36
+ mod, islands,
37
+ hasGetProps, hasGetStaticProps,
38
+ isClientOnly,
39
+ } = await parsePage(page, meta)
40
+ // const {
41
+ // mod, islands,
42
+ // hasGetProps, hasGetStaticProps,
43
+ // isClientOnly,
44
+ // } = page
45
+
46
+ if (!mod?.default) {
47
+ console.warn(`No default export in ${page.filePath} — skipping`)
48
+ return
49
+ }
50
+
51
+ // console.log(page.filePath)
52
+ // console.error('hasGetProps:', hasGetProps)
53
+ // console.error('hasGetStaticProps:', hasGetStaticProps)
54
+ // console.error('isClientOnly:', isClientOnly)
55
+
56
+ if (page.isInternal || page.is404 || (page.isDynamic && !hasGetStaticProps)) {
57
+ meta.summary.set('/'+ page.fileName, {
58
+ type: 2,
59
+ path: '/'+ page.fileName,
60
+ size: 0,
61
+ sizeBr: 0,
62
+ sizeJS: 0,
63
+ })
64
+ // meta.routes.set(page.fullFilePath, page)
65
+ return
66
+ }
67
+
68
+ // console.log(meta.routes.size)
69
+ // meta.routes.set(page.filePath, page)
70
+
71
+ const { filePath, pattern, params } = page
72
+
73
+ let props = []
74
+ if (hasGetStaticProps) {
75
+ let sProps = []
76
+ try {
77
+ const _sProps = await Promise.resolve(mod.getStaticProps!())
78
+ sProps = Array.isArray(_sProps) ? _sProps : [_sProps]
79
+ } catch (e) {
80
+ return console.error(`getStaticProps failed in ${filePath}: ${e}`)
81
+ }
82
+
83
+ for (const entry of sProps) {
84
+ const _props = {
85
+ routeParams: {},
86
+ pageProps: {},
87
+ } as {
88
+ routeParams: RouteParams,
89
+ pageProps: StaticProps,
90
+ }
91
+
92
+ for (const [k, v] of Object.entries(entry as Record<string, unknown>)) {
93
+ if (k in params) {
94
+ _props.routeParams[k] = v as string | string[]
95
+ } else {
96
+ _props.pageProps[k] = v
97
+ }
98
+ }
99
+
100
+ props.push(_props)
101
+ }
102
+ } else {
103
+ props.push({ routeParams: {}, pageProps: {} })
104
+ }
105
+
106
+ const layouts = await getLayouts(meta, page.filePath)
107
+
108
+ for (const prop of props) {
109
+ const allProps = { ...prop.pageProps, ...prop.routeParams }
110
+
111
+ // @ts-ignore
112
+ // i18n(allProps?.locale || meta.i18n.fallbackLocale, meta.i18n.locales, meta.i18n.messages, meta.i18n.routes, meta.i18n.routeMap)
113
+ allProps?.locale && setLocale(allProps?.locale || meta.i18n.fallbackLocale)
114
+
115
+ const pathname = resolveStaticOutputPath(page, prop.routeParams)
116
+ // TODO: melhorar isso para impedir erros do wouter e afins
117
+ // @ts-ignore
118
+ global.location = { pathname: '/'+ (pathname === 'index' ? '' : pathname) }
119
+ // @ts-ignore
120
+ global.window = { $duto: {
121
+ sroutes: meta.spages,
122
+ navigate: {},
123
+ route: { pattern: page.pattern, params: prop.routeParams, path: global.location.pathname },
124
+ } }
125
+
126
+ let html = ''
127
+
128
+ // if (!isClientOnly) { // TODO: fazer funcionar com `use client`
129
+ try {
130
+ let el = h(mod.default, allProps as any)
131
+ for (const layout of layouts)
132
+ el = h(layout.mod.default, null, el)
133
+
134
+ html = render(el)
135
+ } catch (e) {
136
+ console.error(` SSR render failed for ${filePath}: ${e}`)
137
+ console.warn(e)
138
+ html = ''
139
+ }
140
+ // }
141
+
142
+ // TODO: refatorar isso... aqui dentro já é o SSG
143
+ const needsClient = hasGetProps || !!isClientOnly || islands.length > 0
144
+
145
+ // console.error(page.fileName)
146
+ // console.error('needsClient: ', needsClient)
147
+
148
+ const assets = []
149
+ for (const [depPath, dep] of meta.map.get(page.fullFilePath)!.deps)
150
+ assets.push(normalizeId(dep.path))
151
+
152
+
153
+ html = buildHtml(assets, html) //, needsClient, allProps, pattern, islands)
154
+ // html = selfCloseEmptyTags(String(minify.minify(
155
+ // Buffer.from(buildHtml(assets, html, needsClient, allProps, pattern, islands)),
156
+ // {}
157
+ // // options != null ? options : {},
158
+ // )), 'i-i')
159
+
160
+ const outFile = join(
161
+ config.root,
162
+ config.build.outDir,
163
+ pathname +'.html'
164
+ )
165
+ const outHTML = minify.minify(
166
+ Buffer.from(html),
167
+ {}
168
+ // options != null ? options : {},
169
+ )
170
+ await mkdir(dirname(outFile), {recursive: true})
171
+ // await writeFile(outFile, outHTML, 'utf-8')
172
+ await Bun.write(outFile, outHTML)
173
+
174
+ const key = '/'+ pathname
175
+ if (page.isDynamic) {
176
+ const _key = '/'+ page.fileName
177
+ if (!meta.summary.has(_key)) {
178
+ const ddata = {
179
+ path: _key,
180
+ size: page.content.length,
181
+ sizeBr: (await brCompress(page.content)).length,
182
+ sizeJS: 0, // TODO
183
+ }
184
+ meta.summary.set(_key, { type: 1, children: new Map<string, SummaryMetadata>(), ...ddata})
185
+ meta.summary.set(_key + 2, { type: 2, ...ddata})
186
+ }
187
+
188
+ const summary = meta.summary.get(_key)!
189
+ summary.children?.set(key, {
190
+ type: 0,
191
+ path: key,
192
+ size: outHTML.length,
193
+ sizeBr: (await brCompress(outHTML)).length,
194
+ sizeJS: 0,
195
+ })
196
+
197
+ } else {
198
+ meta.summary.set(key, {
199
+ type: 0,
200
+ path: key,
201
+ size: outHTML.length,
202
+ sizeBr: (await brCompress(outHTML)).length,
203
+ sizeJS: 0,
204
+ })
205
+ }
206
+
207
+ }
208
+ }
209
+
210
+ function buildHtml(
211
+ _assets: string[],
212
+ html: string,
213
+ // needsClient: boolean,
214
+ // props?: StaticProps,
215
+ // routePattern?: string,
216
+ // islands?: Island[],
217
+ ): string {
218
+ if (!(html.includes('<body') || html.includes('<html') || html.includes('<head')))
219
+ html = wrapHtml(html)
220
+
221
+ if (!DOCTYPE_RE.test(html))
222
+ html = '<!DOCTYPE html>'+ html
223
+
224
+ // const pageProps = needsClient
225
+ // ? `<script>
226
+ // window.__PROPS__ = ${JSON.stringify(props ?? {})};
227
+ // </script>`
228
+ // : ''
229
+
230
+ const assets = [
231
+ ':duto/client/app.tsx',
232
+ // ..._assets,
233
+ ]
234
+
235
+ const prepend = assets.map(asset => {
236
+ const href = getAsset(asset)
237
+ return href ? `<link href="${href}" rel=stylesheet>` : null
238
+ }).filter(Boolean).join('')
239
+ // + `<style>d-b,d-p,i-d,i-s{display:contents}</style>`
240
+ // + `<style>d,d-slot,d-s-slot{display:contents}</style>`
241
+
242
+ // d>*,d-slot>*,d-s-slot>*{display:revert}
243
+
244
+ // d {
245
+ // display: block;
246
+ // box-sizing: border-box;
247
+ // }
248
+
249
+ // /* Herda todos os estilos padrão de div */
250
+ // d:not([hidden]) {
251
+ // display: block;
252
+ // }
253
+
254
+ // /* Remove qualquer comportamento de display:contents */
255
+ // d, d-slot, d-s-slot {
256
+ // display: block !important;
257
+ // }
258
+
259
+ // d *,
260
+ // d-slot *,
261
+ // d-s-slot * {
262
+ // visibility: visible !important;
263
+ // opacity: 1 !important;
264
+ // }
265
+ // </style>
266
+
267
+ // <script>class DDiv extends HTMLElement {
268
+ // constructor() {
269
+ // super();
270
+ // }
271
+ // }
272
+
273
+ // if (typeof customElements !== 'undefined' && !customElements.get('d')) {
274
+ // customElements.define('d', DDiv);
275
+ // }</script>
276
+
277
+ if (html.includes('</head>')) {
278
+ html = html.replace('</head>', prepend +'</head>')
279
+ } else if (html.includes('<head>')) {
280
+ html = html.replace('<head>', '<head>'+ prepend)
281
+ } else {
282
+ html = prepend + html
283
+ }
284
+
285
+ // const append = pageProps + assets.map(file => {
286
+ const append = assets.map(file => {
287
+ const src = getChunk(file)
288
+ return src ? `<script src="${src}" type=module></script>` : null
289
+ }).filter(Boolean).join('')
290
+
291
+ if (html.includes('</body>'))
292
+ return html.replace('</body>', `${append}</body>`)
293
+
294
+ return html + append
295
+ }
296
+
297
+ export function wrapHtml(content: string) {
298
+ return `<html>
299
+ <head>
300
+ <meta charset=utf-8>
301
+ <meta content="width=device-width,initial-scale=1" name=viewport>
302
+ </head>
303
+ <body hidden>
304
+ ${content}
305
+ </body>
306
+ </html>`
307
+ }
@@ -0,0 +1,108 @@
1
+ import { h } from 'preact'
2
+ import { renderToString } from 'preact-render-to-string'
3
+ import { IMPORT, newID } from 't0n'
4
+
5
+ import { getEntry } from './assets'
6
+ import { makeByteToCharMap, extractPropsFromJSX } from './ast'
7
+
8
+ import type { ImportUsage, IslandMeta } from './types'
9
+
10
+ function serializeProps(props: Record<string, unknown>) {
11
+ return JSON.stringify(props)
12
+ }
13
+
14
+ function htmlEncodeProps(serialized: string) {
15
+ return serialized
16
+ .replace(/&/g, '&amp;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ }
21
+
22
+ async function renderIslandSSR(
23
+ component: unknown,
24
+ props: Record<string, unknown>,
25
+ children?: unknown,
26
+ ): Promise<string> {
27
+ try {
28
+ return renderToString(h(component as never, props as never, children as never))
29
+ } catch {
30
+ return ''
31
+ }
32
+ }
33
+
34
+ function buildIslandTag(meta: IslandMeta) {
35
+ return `<i-s id="${meta.id}" url="${getEntry(meta.url)}"${meta.export === 'default' ? '' : ` export="${meta.export}"`}${meta.props === '{}' ? '' : ` props="${meta.props}"`}>${meta.ssrHtml}</i-s>`
36
+ }
37
+
38
+ /**
39
+ * Replaces in "sourceCode" each occurrence of imported components
40
+ * from "importSource" by hydrator-compatible <island> tags.
41
+ *
42
+ * @param sourceCode TSX source code
43
+ * @param importSource The target module (e.g. '@/components/counter2')
44
+ * @param opts Resolution callbacks
45
+ * @returns Code with replacements applied
46
+ */
47
+ export async function replaceIslands(
48
+ code: string,
49
+ usages: ImportUsage[],
50
+ ) {
51
+ if (!usages.length) return code
52
+
53
+ const b2c = makeByteToCharMap(code)
54
+ const toChar = (byteOffset: number) => b2c[byteOffset - 1]
55
+
56
+ const replacements: Array<{ start: number; end: number; replacement: string }> = []
57
+
58
+ for (const importUsage of usages) {
59
+ const component = await IMPORT(importUsage.source, importUsage.imported)
60
+
61
+ // schedule removal of the import declaration (replacement with an empty string)
62
+ replacements.push({
63
+ start: toChar(importUsage.importStart),
64
+ end: toChar(importUsage.importEnd),
65
+ replacement: '',
66
+ })
67
+
68
+ // schedule the replacement of each JSX usage with `<island>`
69
+ for (const usage of importUsage.usages) {
70
+ if (usage.type !== 'jsx') continue
71
+
72
+ const props = extractPropsFromJSX(usage.node)
73
+ const ssrHtml = await renderIslandSSR(component, props)
74
+
75
+ const island = buildIslandTag({
76
+ id: newID(),
77
+ url: importUsage.source,
78
+ export: importUsage.imported,
79
+ props: htmlEncodeProps(serializeProps(props)),
80
+ ssrHtml,
81
+ })
82
+
83
+ replacements.push({
84
+ start: toChar(usage.start),
85
+ end: toChar(usage.end),
86
+ replacement: island,
87
+ })
88
+ }
89
+ }
90
+
91
+ if (!replacements.length) return code
92
+
93
+ replacements.sort((a, b) => a.start - b.start)
94
+
95
+ const parts: string[] = []
96
+ let cursor = 0
97
+
98
+ for (const { start, end, replacement } of replacements) {
99
+ if (start < cursor) continue // overlapped, discards
100
+ if (start > cursor) parts.push(code.slice(cursor, start))
101
+ parts.push(replacement)
102
+ cursor = end
103
+ }
104
+
105
+ if (cursor < code.length) parts.push(code.slice(cursor))
106
+
107
+ return parts.join('').replace(/\n{3,}/g, '\n\n')
108
+ }