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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present, HUB.BI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,17 +1,26 @@
1
- ### Verify if name exists:
1
+ <h1 align="left">Duto<br/><a href="https://pr.new/attla/duto"><img align="right" src="https://developer.stackblitz.com/img/start_pr_dark_small.svg" alt="Start new PR in StackBlitz Codeflow"></a><a href="https://npmjs.com/package/duto"><img align="right" src="https://img.shields.io/npm/v/duto.svg" alt="npm package"></a></h1>
2
2
 
3
- ```bash
4
- npm view PACKAGE_NAME
5
- ```
3
+ > ⚠️ Duto is under ALFA development — expect updates, rough edges, and occasional breack changes.
4
+ <br/>
6
5
 
7
- ou
6
+ This framework is fully geared towards the serverless world, specifically for static pages.
8
7
 
9
- ```bash
10
- bun info PACKAGE_NAME
11
- ```
8
+ - 💡 SSG
9
+ - ⚡️ Dynamic static routes
10
+ - 📦 Optimized Build
12
11
 
13
- ### Publish package
12
+ [Read the Docs to Learn More](https://github.com/attla/duto/blob/main/DOCS.md)
14
13
 
15
- ```bash
16
- bun publish
17
- ```
14
+ ## Ecosystem Packages
15
+
16
+ | Package | Version | Description |
17
+ | -: | :- | :- |
18
+ | [rajt](https://github.com/attla/rajt) | [![rajt version](https://img.shields.io/npm/v/rajt.svg?label=%20)](https://npm.im/rajt) | 🧱 The core of Rajt serverless framework. |
19
+ | [create-rajt](https://github.com/attla/create-rajt) | [![create-rajt version](https://img.shields.io/npm/v/create-rajt.svg?label=%20)](https://npm.im/create-rajt) | 📁 CLI tool for create new Rajt projects, from starter templates. |
20
+ | [forj](https://github.com/attla/forj) | [![forj version](https://img.shields.io/npm/v/forj.svg?label=%20)](https://npm.im/forj) | 🗂️ Just another query builder in typescript |
21
+ | [t0n](https://github.com/attla/t0n) | [![t0n version](https://img.shields.io/npm/v/t0n.svg?label=%20)](https://npm.im/t0n) | 🧰 Collection of elegant typescript resources for web artisans. |
22
+ | [cripta](https://github.com/attla/cripta) | [![cripta version](https://img.shields.io/npm/v/cripta.svg?label=%20)](https://npm.im/cripta) | 🧛 A layer of encryption a little too judicious. |
23
+
24
+ ## License
25
+
26
+ This package is licensed under the [MIT license](https://github.com/attla/duto/blob/main/LICENSE) © [HUB](https://hub.bi)
package/package.json CHANGED
@@ -1,4 +1,93 @@
1
1
  {
2
- "name": "duto",
3
- "version": "0.0.0"
2
+ "name": "duto",
3
+ "description": "All-in-one serverless framework for web artisans",
4
+ "version": "0.260906.0",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.d.ts",
8
+ "bin": "./src/cli.ts",
9
+ "files": ["src"],
10
+ "exports": {
11
+ ".": "./src/index.ts",
12
+ "./auth": "./src/server/auth/index.ts",
13
+ "./db": "./src/db/index.ts",
14
+ "./database": "./src/db/index.ts",
15
+ "./d1": "./src/server/db/d1.ts",
16
+ "./d1/types": "./src/server/db/d1.d.ts",
17
+ "./dynamodb": "./src/server/db/dynamodb.ts",
18
+ "./dynamodb/types": "./src/server/db/dynamodb.d.ts",
19
+ "./enum": "./src/enum.ts",
20
+ "./http": "./src/server/http.ts",
21
+ "./middleware": "./src/middleware.ts",
22
+ "./types": "./src/server/types.ts",
23
+ "./types/global": "./global.d.ts",
24
+ "./env": "./src/server/utils/environment.ts",
25
+ "./length": "./src/server/utils/length.ts"
26
+ },
27
+ "dependencies": {
28
+ "@hono/node-server": "^1.19.11",
29
+ "@hono/standard-validator": "^0.2.2",
30
+ "@hono/zod-validator": "^0.7.6",
31
+ "@scalar/hono-api-reference": "^0.12.0",
32
+ "@minify-html/node": "^0.18.1",
33
+ "@preact/signals": "^2.9.2",
34
+ "@swc/core": "^1.15.21",
35
+ "chokidar": "^3.5.2",
36
+ "citty": "^0.2.2",
37
+ "consola": "^3.4.2",
38
+ "cripta": "0.x",
39
+ "dotenv": "^16.5.0",
40
+ "esbuild": "^0.25.2",
41
+ "filesize": "^11.0",
42
+ "forj": "0.x",
43
+ "hono": "^4.13.7",
44
+ "hono-openapi": "^1.3.0",
45
+ "localflare-api": "^0.5.0",
46
+ "localflare-core": "^0.5.0",
47
+ "miniflare": "^4.20260730.0",
48
+ "pathe": "^2.0",
49
+ "preact": "^10.29.7",
50
+ "preact-render-to-string": "^6.6.6",
51
+ "quansync": "^0.2.11",
52
+ "t0n": "0.x",
53
+ "wouter-preact": "^3.10.0",
54
+ "wrangler": "^4.128.0",
55
+ "zod": "^4.3.6",
56
+ "zod-openapi": "^5.4.6"
57
+ },
58
+ "devDependencies": {
59
+ "@cloudflare/workers-types": "^4.20260113.0",
60
+ "@types/node": "^26.4.1",
61
+ "bun-types": "^1.4.0",
62
+ "typescript": "^5.9.3"
63
+ },
64
+ "resolutions": {
65
+ "@smithy/types": "^4.3.0",
66
+ "zod": "^4.3.6"
67
+ },
68
+ "sideEffects": false,
69
+ "packageManager": "bun@1.4.0",
70
+ "publishConfig": {
71
+ "registry": "https://registry.npmjs.org"
72
+ },
73
+ "author": "HUB <foss@hub.bi>",
74
+ "license": "MIT",
75
+ "homepage": "https://hub.bi/docs/duto",
76
+ "repository": "git://github.com/attla/duto",
77
+ "bugs": "https://github.com/attla/duto/issues",
78
+ "keywords": [
79
+ "SSG",
80
+ "app",
81
+ "http",
82
+ "application",
83
+ "framework",
84
+ "router",
85
+ "serverless",
86
+ "typescript",
87
+ "cloudflare",
88
+ "workers",
89
+ "workerd",
90
+ "vercel",
91
+ "bun"
92
+ ]
4
93
  }
package/src/cli.ts ADDED
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bun
2
+
3
+ function exit(msg?: string, desc?: string) {
4
+ msg && console.error(msg)
5
+ desc && console.log(desc)
6
+ process.exit(0)
7
+ }
8
+
9
+ if (!import.meta.main)
10
+ exit('Error: Duto must be used directly as CLI command.')
11
+
12
+ if (!process?.isBun || typeof Bun === 'undefined')
13
+ exit('Error: "Bun" is not available.', ' Try install: https://bun.sh/get')
14
+
15
+ global.__st = performance.now()
16
+
17
+ import { join } from 'pathe'
18
+ import { cli } from 't0n/cli'
19
+ import { Envir } from 't0n'
20
+ import { version } from '!/package.json'
21
+ import { _root, _duto } from './utils'
22
+
23
+ let commands = {
24
+ // new:
25
+ // upgrade:
26
+ }
27
+ let pkg
28
+ try {
29
+ pkg = await import(join(_root, 'package.json'))
30
+ if (!pkg || !pkg.duto || !pkg?.duto?.type || !['pages', 'server'].includes(pkg.duto.type))
31
+ exit('Error: invalid app type.')
32
+
33
+ Envir.set('duto', pkg.duto)
34
+ commands = (await import(join(_duto, pkg.duto.type, 'cli')))?.default
35
+ } catch {}
36
+
37
+ cli({
38
+ name: 'duto',
39
+ description: 'Duto',
40
+ version,
41
+ commands,
42
+ })
package/src/enum.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Enum } from 't0n'
2
+ export type { EnumStatic, EnumValue, EnumType } from 't0n'
package/src/index.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /// <reference types='bun-types' />
2
+ /// <reference types='node' />
3
+
4
+ // export { }
5
+
6
+ import * as Duto from './'
7
+
8
+ declare global {
9
+ // Duto
10
+ var __st: number
11
+ var __hmr: boolean
12
+
13
+ type Req = Duto.Req
14
+ type Res = Duto.Res
15
+ type $v = Duto.$v
16
+ type IRequest = Duto.IRequest
17
+ type IResponse = Duto.IResponse
18
+ type IValidator = Duto.IValidator
19
+
20
+ type Context = Duto.Context
21
+ type Next = Duto.Next
22
+
23
+ type Abilities = Duto.Abilities
24
+ type Roles = Duto.Roles
25
+
26
+ type EnumStatic = Duto.EnumStatic
27
+ type EnumValue = Duto.EnumValue
28
+ type EnumType = Duto.EnumType
29
+
30
+
31
+ // Bun:test
32
+ var test: typeof import('bun:test').test
33
+ var describe: typeof import('bun:test').describe
34
+ var it: typeof import('bun:test').it
35
+ var expect: typeof import('bun:test').expect
36
+ // Hooks
37
+ var beforeAll: typeof import('bun:test').beforeAll
38
+ var afterAll: typeof import('bun:test').afterAll
39
+ var beforeEach: typeof import('bun:test').beforeEach
40
+ var afterEach: typeof import('bun:test').afterEach
41
+ // Mocking
42
+ var mock: typeof import('bun:test').mock
43
+ var vi: typeof import('bun:test').vi
44
+ var jest: typeof import('bun:test').jest
45
+ var spyOn: typeof import('bun:test').spyOn
46
+ // Utils
47
+ var setSystemTime: typeof import('bun:test').setSystemTime
48
+ var setDefaultTimeout: typeof import('bun:test').setDefaultTimeout
49
+ // Types
50
+ var Test: typeof import('bun:test').Test
51
+ var Describe: typeof import('bun:test').Describe
52
+ var Expect: typeof import('bun:test').Expect
53
+ var Mock: typeof import('bun:test').Mock
54
+ var TestOptions: typeof import('bun:test').TestOptions
55
+ var Matchers: typeof import('bun:test').Matchers
56
+ // Namespace
57
+ namespace jest {
58
+ var fn: typeof import('bun:test').vi.fn
59
+ var mock: typeof import('bun:test').vi.mock
60
+ }
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export { default as Response } from './server/response'
2
+ export { default as Config } from './server/config'
3
+
4
+ export { Enum, Envir } from 't0n'
5
+ export type { EnumStatic, EnumValue, EnumType } from 't0n'
6
+
7
+ export type {
8
+ IRequest, IRequest as Req, IRequest as $req,
9
+ IResponse, IResponse as Res, IResponse as $res,
10
+ IValidator, IValidator as $v,
11
+ Context, Next,
12
+ } from './server/types'
13
+
14
+ // Auth
15
+ export { Ability } from './server/auth/ability'
16
+ export { Authnz } from './server/auth/authnz'
17
+ export { Token } from './server/auth/token'
18
+
19
+ export type { Abilities, Roles } from './server/auth/types'
@@ -0,0 +1,2 @@
1
+ export { auth } from './server/middlewares/auth'
2
+ // export { cors } from './server/middlewares/cors'
@@ -0,0 +1,123 @@
1
+ // @ts-ignore
2
+ import ':duto/client/hmr'
3
+
4
+ import { render, hydrate, h } from 'preact'
5
+ import { Switch, Route, Router } from 'wouter-preact'
6
+ import { useLocation, useParams, useSearchParams, } from 'wouter-preact'
7
+
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
+
15
+ i18n(fallbackLocale, locales, messages, routes, routeMap)
16
+
17
+ import EI18n from '../i18n/element'
18
+ import Island from './island'
19
+ import Link from './link'
20
+
21
+ // import Router from './router'
22
+
23
+ import {
24
+ views, SRoutes,
25
+ // @ts-ignore
26
+ } from ':duto'
27
+ // @ts-ignore
28
+ import ':root/styles'
29
+
30
+ window.$duto = {
31
+ sroutes: SRoutes,
32
+ location: '/',
33
+ navigate: {},
34
+ params: {},
35
+ search: {},
36
+ setSearch: {},
37
+ }
38
+ import { renderRoute } from './router'
39
+ // import { navigate } from 'wouter-preact/use-hash-location'
40
+
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)
48
+ }
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>
82
+ {h(() => {
83
+ document.body.removeAttribute('hidden')
84
+ const [location, navigate] = useLocation()
85
+ window.$duto.location = location
86
+ window.$duto.navigate = navigate
87
+ window.$duto.params = useParams()
88
+ const [search, setSearch] = useSearchParams()
89
+ window.$duto.search = search
90
+ window.$duto.setSearch = setSearch
91
+
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
+ updatePickers()
95
+
96
+ // React.useEffect(() => {
97
+ // document.body.removeAttribute('hidden');
98
+ // navigateRef.current = navigate;
99
+ // updatePickers();
100
+ // }, [navigate]);
101
+ // return null;
102
+ }, null)}
103
+ <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
+ }} />
121
+ ))}
122
+ </Switch>
123
+ </Router>, document.body)
@@ -0,0 +1,13 @@
1
+ let hmr: WebSocket | null = null
2
+ const url = `ws${location.protocol === 'https:' ? 'ws' : ''}://${location.host}`
3
+
4
+ const connect = () => {
5
+ hmr = new WebSocket(url, 'duto')
6
+ hmr.onmessage = ({ data }) => data === 'reload' && location.reload()
7
+ hmr.onerror = () => hmr?.close()
8
+ }
9
+ const reconect = () => (!hmr || hmr?.readyState === WebSocket.CLOSED) && location.reload()
10
+ connect()
11
+ document.addEventListener('visibilitychange', () => document.visibilityState === 'visible' && reconect())
12
+ window.addEventListener('focus', reconect)
13
+ window.addEventListener('pageshow', reconect)
@@ -0,0 +1,136 @@
1
+ import hydrator from '../preact/client'
2
+
3
+ const propTypes: { [k: string]: (value: any) => any } = {
4
+ 0: (value) => reviveObject(value),
5
+ 1: (value) => reviveArray(value),
6
+ 2: (value) => new RegExp(value),
7
+ 3: (value) => new Date(value),
8
+ 4: (value) => new Map(reviveArray(value)),
9
+ 5: (value) => new Set(reviveArray(value)),
10
+ 6: (value) => BigInt(value),
11
+ 7: (value) => new URL(value),
12
+ 8: (value) => new Uint8Array(value),
13
+ 9: (value) => new Uint16Array(value),
14
+ 10: (value) => new Uint32Array(value),
15
+ 11: (value) => Number.POSITIVE_INFINITY * value,
16
+ }
17
+
18
+ // Not using JSON.parse reviver because it's bottom-up but we want top-down
19
+ const reviveTuple = (raw: any): any => {
20
+ const [type, value] = raw
21
+ return type in propTypes ? propTypes[type](value) : undefined
22
+ }
23
+
24
+ const reviveArray = (raw: any): any => (raw as Array<any>).map(reviveTuple)
25
+ const reviveObject = (raw: any): any => {
26
+ if (typeof raw !== 'object' || raw === null) return raw
27
+ return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]))
28
+ }
29
+
30
+ // 🌊🏝🌴
31
+ export default class Island extends HTMLElement {
32
+ public Component: any
33
+ private _hydrated = false
34
+ static observedAttributes = ['props']
35
+
36
+ connectedCallback() {
37
+ if (!this.hasAttribute('await-children') || document.readyState === 'interactive' || document.readyState === 'complete') {
38
+ this.build()
39
+ return
40
+ }
41
+
42
+ setTimeout(() => {
43
+ this.build()
44
+ }, 0)
45
+ }
46
+
47
+ disconnectedCallback() {
48
+ // document.removeEventListener('d:after-swap', this.unmount);
49
+ // document.addEventListener('d:after-swap', this.unmount, { once: true });
50
+
51
+ this.unmount()
52
+ }
53
+
54
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
55
+ if (this.Component) {
56
+ // console.log(`[AttributeChanged] O atributo "${name}" mudou de "${oldValue}" para "${newValue}".`, this)
57
+ this._hydrated = false
58
+ this.hydrate()
59
+ }
60
+ }
61
+
62
+ async build() {
63
+ const componentModule = await import(this.getAttribute('url')!)
64
+ const componentExport = this.getAttribute('export') || 'default'
65
+ if (!componentExport.includes('.')) {
66
+ this.Component = componentModule[componentExport]
67
+ } else {
68
+ this.Component = componentModule
69
+ for (const part of componentExport.split('.'))
70
+ this.Component = this.Component[part]
71
+ }
72
+
73
+ await this.hydrate()
74
+ }
75
+
76
+ // hydrate = async () => {
77
+ async hydrate() {
78
+ // Make sure the island is mounted on the DOM before hydrating. It could be unmounted
79
+ // when the parent island hydrates and re-creates this island.
80
+ if (!this.isConnected || this._hydrated) return
81
+
82
+ // Wait for parent island to hydrate first so we hydrate top-down. The `ssr` attribute
83
+ // represents that it has not completed hydration yet.
84
+ const parent = this.parentElement?.closest('i-s')
85
+ if (parent) {
86
+ parent.addEventListener('d:hydrate', this.hydrate, { once: true })
87
+ return
88
+ }
89
+
90
+ const slotted = this.querySelectorAll('d-b')
91
+ const slots: Record<string, string> = {};
92
+ // Always check to see if there are templates.
93
+ // This happens if slots were passed but the client component did not render them
94
+ const templates = this.querySelectorAll('template[d-template]')
95
+ for (const template of templates) {
96
+ const closest = template.closest(this.tagName)
97
+ if (!closest?.isSameNode(this)) continue
98
+ slots[template.getAttribute('d-template') || 'default'] = template.innerHTML
99
+ template.remove()
100
+ }
101
+ for (const slot of slotted) {
102
+ const closest = slot.closest(this.tagName)
103
+ if (!closest?.isSameNode(this)) continue
104
+ slots[slot.getAttribute('name') || 'default'] = slot.innerHTML
105
+ }
106
+
107
+ let props: Record<string, unknown>
108
+
109
+ try {
110
+ props = this.hasAttribute('props') ? reviveObject(JSON.parse(this.getAttribute('props')!)) : {}
111
+ } catch (e) {
112
+ let componentName: string = this.getAttribute('url') || '<unknown>'
113
+ const componentExport = this.getAttribute('export')
114
+
115
+ if (componentExport)
116
+ componentName += ` (export ${componentExport})`
117
+
118
+ console.error(
119
+ `[hydrate] Error parsing props for component ${componentName}`,
120
+ this.getAttribute('props'),
121
+ e,
122
+ )
123
+ throw e
124
+ }
125
+
126
+ // @ts-ignore
127
+ await hydrator(this, this.Component, props, slots)
128
+
129
+ this._hydrated = true
130
+ this.dispatchEvent(new CustomEvent('d:hydrate'))
131
+ }
132
+
133
+ unmount() {
134
+ if (!this.isConnected) this.dispatchEvent(new CustomEvent('d:unmount'))
135
+ }
136
+ }
@@ -0,0 +1,70 @@
1
+ export default class Link extends HTMLAnchorElement {
2
+ static observedAttributes = [
3
+ 'href',
4
+ 'target',
5
+ 'download',
6
+ 'rel',
7
+ 'hreflang',
8
+ 'referrerpolicy',
9
+ 'ping',
10
+ 'type',
11
+ ]
12
+
13
+ constructor() {
14
+ super()
15
+
16
+ this.addEventListener('click', this.#onClick)
17
+ }
18
+
19
+ disconnectedCallback() {
20
+ this.removeEventListener('click', this.#onClick)
21
+ }
22
+
23
+ #onClick = (e: MouseEvent) => {
24
+ // Respeita comportamento padrão
25
+ if (
26
+ e.defaultPrevented
27
+ || this.target === '_blank'
28
+ || e.metaKey
29
+ || e.ctrlKey
30
+ || e.shiftKey
31
+ || e.altKey
32
+ || 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 }))
41
+
42
+ // Exemplo: interceptar navegação SPA
43
+ // event.preventDefault()
44
+ // router.push(this.href)
45
+ }
46
+
47
+ // Proxy opcional para facilitar typings
48
+ get url() {
49
+ return new URL(this.href)
50
+ }
51
+ }
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
+ // }