duto 0.260906.0 → 0.261001.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/README.md +34 -12
- package/package.json +9 -3
- package/src/cli.ts +10 -7
- package/src/commands/new.ts +172 -0
- package/src/commands/templates/pages/.editorconfig +21 -0
- package/src/commands/templates/pages/README.md +21 -0
- package/src/commands/templates/pages/_gitignore +24 -0
- package/src/commands/templates/pages/components/locale-select.tsx +59 -0
- package/src/commands/templates/pages/components/select.tsx +95 -0
- package/src/commands/templates/pages/i18n/en.ts +17 -0
- package/src/commands/templates/pages/i18n/index.ts +19 -0
- package/src/commands/templates/pages/i18n/pt-br.ts +17 -0
- package/src/commands/templates/pages/package.json +22 -0
- package/src/commands/templates/pages/pages/404.tsx +12 -0
- package/src/commands/templates/pages/pages/index.tsx +16 -0
- package/src/commands/templates/pages/pages/layout.tsx +24 -0
- package/src/commands/templates/pages/style.css +24 -0
- package/src/commands/templates/pages/tsconfig.json +54 -0
- package/src/commands/templates/pages/types.ts +8 -0
- package/src/commands/templates/server/base/.editorconfig +21 -0
- package/src/commands/templates/server/base/.env.dev.example +2 -0
- package/src/commands/templates/server/base/.env.prod.example +2 -0
- package/src/commands/templates/server/base/README.md +29 -0
- package/src/commands/templates/server/base/_gitignore +100 -0
- package/src/commands/templates/server/base/actions/blog/index.get.ts +9 -0
- package/src/commands/templates/server/base/actions/blog/post/[id].get.ts +9 -0
- package/src/commands/templates/server/base/actions/index.get.ts +5 -0
- package/src/commands/templates/server/base/configs/duto.ts +11 -0
- package/src/commands/templates/server/base/configs/roles.ts +5 -0
- package/src/commands/templates/server/base/package.json +24 -0
- package/src/commands/templates/server/base/tsconfig.json +42 -0
- package/src/commands/templates/server/workerd/wrangler.toml +47 -0
- package/src/commands/upgrade.ts +30 -0
- package/src/index.ts +7 -2
- package/src/pages/{commands → cli/commands}/build.ts +13 -7
- package/src/pages/{commands → cli/commands}/dev.ts +19 -6
- package/src/pages/{commands → cli/commands}/preview.ts +6 -4
- package/src/pages/cli/index.ts +10 -0
- package/src/pages/cli/utils.ts +126 -0
- package/src/pages/client/app.tsx +27 -74
- package/src/pages/client/router.ts +15 -281
- package/src/pages/compiler/aliases.ts +2 -8
- package/src/pages/compiler/ast.ts +1 -538
- package/src/pages/compiler/constants.ts +95 -13
- package/src/pages/compiler/file.ts +1 -1
- package/src/pages/compiler/index.ts +20 -128
- package/src/pages/compiler/meta.ts +39 -228
- package/src/pages/compiler/routes.ts +4 -5
- package/src/pages/compiler/types.ts +11 -101
- package/src/pages/compiler/vite-plugin.ts +90 -69
- package/src/pages/i18n/client.ts +19 -7
- package/src/pages/i18n/element.ts +1 -1
- package/src/pages/i18n/server.ts +22 -7
- package/src/pages/index.ts +1 -2
- package/src/pages/preact/server.ts +1 -1
- package/src/server/app.ts +5 -10
- package/src/server/auth/ability.ts +1 -1
- package/src/server/auth/token.ts +5 -5
- package/src/server/cli/commands/build.ts +2 -2
- package/src/server/cli/commands/deploy.ts +1 -2
- package/src/server/cli/commands/dev.ts +6 -4
- package/src/server/cli/commands/routes.ts +0 -1
- package/src/server/cli/utils.ts +2 -8
- package/src/server/config.ts +18 -3
- package/src/server/entry.ts +3 -3
- package/src/server/open-api/register.ts +3 -3
- package/src/server/open-api/spec.ts +11 -4
- package/src/server/request.ts +46 -14
- package/src/server/routes.ts +8 -31
- package/src/server/utils.ts +25 -0
- package/src/utils.ts +43 -2
- package/src/pages/commands/dev-old.ts +0 -213
- package/src/pages/compiler/island.ts +0 -108
- package/src/pages/compiler/utils.ts +0 -5
- package/src/server/http.ts +0 -23
package/README.md
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
<h1 align="left">
|
|
1
|
+
<h1 align="left">
|
|
2
|
+
⛳️ Duto
|
|
3
|
+
<a href="https://npmjs.com/package/duto"><img src="https://img.shields.io/npm/v/duto.svg" alt="npm package"></a>
|
|
4
|
+
<br/>
|
|
5
|
+
<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>
|
|
6
|
+
</h1>
|
|
2
7
|
|
|
3
8
|
> ⚠️ Duto is under ALFA development — expect updates, rough edges, and occasional breack changes.
|
|
4
9
|
<br/>
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Duto is minimalist framework, focused on client-side pages and serverless APIs.
|
|
12
|
+
|
|
13
|
+
Principles:
|
|
14
|
+
- No vendor lock-in! Deploy anywhere with: `duto deploy`
|
|
15
|
+
- Developer happiness as a design constraint
|
|
16
|
+
- Optimized by default!
|
|
17
|
+
|
|
18
|
+
Pages features:
|
|
19
|
+
- Static pages `/blog.tsx` `getStaticProps()`
|
|
20
|
+
- Prerendered dynamic pages `/blog/[slug].tsx` `getStaticProps()`
|
|
21
|
+
- Dynamic pages on client-side `/blog/[slug].tsx` `getProps()`
|
|
22
|
+
- Not found page on client-side `/blog/not-found.tsx`
|
|
23
|
+
- Efficient route hierarchy `/not-found.tsx`
|
|
24
|
+
- Catch-all page whitout SSR! `/blog/[...slug].tsx`
|
|
25
|
+
- Optional catch-all routes whitout SSR! `/blog/[[...slug]].tsx`
|
|
26
|
+
- Simple internationalization (i18n) integration `tˋfeaturesˋ`
|
|
27
|
+
|
|
28
|
+
Server features:
|
|
29
|
+
- Instant server and cold starts
|
|
30
|
+
- Optimized build, greater efficiency in less bundle size
|
|
31
|
+
- Built-in core shims for compatibility with custom runtimes
|
|
32
|
+
- Fully typed OpenAPI documentation
|
|
33
|
+
- Powerful request validations
|
|
34
|
+
- Fine-grained authentication
|
|
35
|
+
|
|
36
|
+
[Read the Docs to Learn More](https://hub.bi/docs/duto)
|
|
13
37
|
|
|
14
38
|
## Ecosystem Packages
|
|
15
39
|
|
|
16
40
|
| Package | Version | Description |
|
|
17
41
|
| -: | :- | :- |
|
|
18
|
-
| [
|
|
19
|
-
| [create-rajt](https://github.com/attla/create-rajt) | [](https://npm.im/create-rajt) | 📁 CLI tool for create new Rajt projects, from starter templates. |
|
|
20
|
-
| [forj](https://github.com/attla/forj) | [](https://npm.im/forj) | 🗂️ Just another query builder in typescript |
|
|
21
|
-
| [t0n](https://github.com/attla/t0n) | [](https://npm.im/t0n) | 🧰 Collection of elegant typescript resources for web artisans. |
|
|
42
|
+
| [forj](https://github.com/attla/forj) | [](https://npm.im/forj) | 🗂️ Fast and lightweight query builder in typescript |
|
|
22
43
|
| [cripta](https://github.com/attla/cripta) | [](https://npm.im/cripta) | 🧛 A layer of encryption a little too judicious. |
|
|
44
|
+
| [t0n](https://github.com/attla/t0n) | [](https://npm.im/t0n) | 🧰 Collection of elegant typescript resources for web artisans. |
|
|
23
45
|
|
|
24
46
|
## License
|
|
25
47
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "duto",
|
|
3
3
|
"description": "All-in-one serverless framework for web artisans",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.261001.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"./length": "./src/server/utils/length.ts"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@inquirer/confirm": "^5.1.8",
|
|
29
|
+
"@inquirer/input": "^4.1.8",
|
|
30
|
+
"@inquirer/select": "^4.1.0",
|
|
31
|
+
"nanospinner": "^1.2.2",
|
|
28
32
|
"@hono/node-server": "^1.19.11",
|
|
29
33
|
"@hono/standard-validator": "^0.2.2",
|
|
30
34
|
"@hono/zod-validator": "^0.7.6",
|
|
@@ -38,7 +42,6 @@
|
|
|
38
42
|
"cripta": "0.x",
|
|
39
43
|
"dotenv": "^16.5.0",
|
|
40
44
|
"esbuild": "^0.25.2",
|
|
41
|
-
"filesize": "^11.0",
|
|
42
45
|
"forj": "0.x",
|
|
43
46
|
"hono": "^4.13.7",
|
|
44
47
|
"hono-openapi": "^1.3.0",
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
"preact-render-to-string": "^6.6.6",
|
|
51
54
|
"quansync": "^0.2.11",
|
|
52
55
|
"t0n": "0.x",
|
|
56
|
+
"vite": "^8.0.0",
|
|
53
57
|
"wouter-preact": "^3.10.0",
|
|
54
58
|
"wrangler": "^4.128.0",
|
|
55
59
|
"zod": "^4.3.6",
|
|
@@ -57,7 +61,9 @@
|
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
59
63
|
"@cloudflare/workers-types": "^4.20260113.0",
|
|
60
|
-
"@
|
|
64
|
+
"@minify-html/node": "^0.18.1",
|
|
65
|
+
"@preact/preset-vite": "^2.10.5",
|
|
66
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
61
67
|
"bun-types": "^1.4.0",
|
|
62
68
|
"typescript": "^5.9.3"
|
|
63
69
|
},
|
package/src/cli.ts
CHANGED
|
@@ -16,21 +16,20 @@ global.__st = performance.now()
|
|
|
16
16
|
|
|
17
17
|
import { join } from 'pathe'
|
|
18
18
|
import { cli } from 't0n/cli'
|
|
19
|
-
import {
|
|
19
|
+
import { setEnv } from 't0n'
|
|
20
20
|
import { version } from '!/package.json'
|
|
21
21
|
import { _root, _duto } from './utils'
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// upgrade:
|
|
26
|
-
}
|
|
23
|
+
const cmd = (c: string) => join(import.meta.dirname, 'commands', c)
|
|
24
|
+
|
|
27
25
|
let pkg
|
|
26
|
+
let commands = {}
|
|
28
27
|
try {
|
|
29
28
|
pkg = await import(join(_root, 'package.json'))
|
|
30
29
|
if (!pkg || !pkg.duto || !pkg?.duto?.type || !['pages', 'server'].includes(pkg.duto.type))
|
|
31
30
|
exit('Error: invalid app type.')
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
setEnv('duto', pkg.duto)
|
|
34
33
|
commands = (await import(join(_duto, pkg.duto.type, 'cli')))?.default
|
|
35
34
|
} catch {}
|
|
36
35
|
|
|
@@ -38,5 +37,9 @@ cli({
|
|
|
38
37
|
name: 'duto',
|
|
39
38
|
description: 'Duto',
|
|
40
39
|
version,
|
|
41
|
-
commands
|
|
40
|
+
commands: {
|
|
41
|
+
new: cmd('new'),
|
|
42
|
+
upgrade: cmd('upgrade'),
|
|
43
|
+
...commands,
|
|
44
|
+
},
|
|
42
45
|
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { basename, join, resolve } from 'pathe'
|
|
2
|
+
import { command } from 't0n/cli'
|
|
3
|
+
import { bold, dim, red } from 't0n/color'
|
|
4
|
+
import { event, step, rn } from 't0n/log'
|
|
5
|
+
import { _root } from '@/utils'
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import confirm from '@inquirer/confirm'
|
|
9
|
+
import input from '@inquirer/input'
|
|
10
|
+
import select from '@inquirer/select'
|
|
11
|
+
import { createSpinner } from 'nanospinner'
|
|
12
|
+
import { exit } from 'node:process'
|
|
13
|
+
|
|
14
|
+
const types = ['pages', 'server']
|
|
15
|
+
const templates = [
|
|
16
|
+
'aws',
|
|
17
|
+
'workerd',
|
|
18
|
+
'vercel',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const renameFiles: Record<string, string | undefined> = {
|
|
22
|
+
_gitignore: '.gitignore',
|
|
23
|
+
_gitkeep: '.gitkeep',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
function mkdirp(dir: string) {
|
|
28
|
+
try {
|
|
29
|
+
mkdirSync(dir, { recursive: true })
|
|
30
|
+
} catch (e) {
|
|
31
|
+
if (e instanceof Error) {
|
|
32
|
+
if ('code' in e && e.code === 'EEXIST') {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw e
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function copy(src: string, dest: string) {
|
|
41
|
+
const stat = statSync(src)
|
|
42
|
+
stat.isDirectory()
|
|
43
|
+
? copyDir(src, dest)
|
|
44
|
+
: copyFileSync(src, dest)
|
|
45
|
+
}
|
|
46
|
+
function copyDir(srcDir: string, destDir: string) {
|
|
47
|
+
mkdirSync(destDir, { recursive: true })
|
|
48
|
+
for (const file of readdirSync(srcDir)) {
|
|
49
|
+
const srcFile = resolve(srcDir, file)
|
|
50
|
+
const destFile = resolve(destDir, file)
|
|
51
|
+
copy(srcFile, destFile)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default command({
|
|
56
|
+
meta: {
|
|
57
|
+
name: 'new',
|
|
58
|
+
description: '📄 Create new project with duto\n',
|
|
59
|
+
},
|
|
60
|
+
args: {
|
|
61
|
+
install: {
|
|
62
|
+
alias: 'i',
|
|
63
|
+
description: 'Install dependencies',
|
|
64
|
+
type: 'boolean',
|
|
65
|
+
default: false,
|
|
66
|
+
},
|
|
67
|
+
type: {
|
|
68
|
+
description: 'Project type',
|
|
69
|
+
type: 'enum',
|
|
70
|
+
options: types,
|
|
71
|
+
},
|
|
72
|
+
template: {
|
|
73
|
+
alias: 't',
|
|
74
|
+
description: 'Template to use',
|
|
75
|
+
type: 'enum',
|
|
76
|
+
options: templates,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
async run({ args }) { // @ts-ignore
|
|
80
|
+
let dir = args._[0]
|
|
81
|
+
if (dir) {
|
|
82
|
+
step(bold('Using target directory: '+ dim('/'+ dir.replace(/^\//, ''))))
|
|
83
|
+
} else {
|
|
84
|
+
dir = await input({ message: 'Target directory', default: _root })
|
|
85
|
+
if (!dir.startsWith(_root))
|
|
86
|
+
dir = join(_root, dir)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const type = args.type || (await select({
|
|
90
|
+
loop: true,
|
|
91
|
+
message: 'Which type of project is it?',
|
|
92
|
+
choices: types.map(v => ({ title: v, value: v})),
|
|
93
|
+
}))
|
|
94
|
+
|
|
95
|
+
if (!type)
|
|
96
|
+
throw new Error('No type selected')
|
|
97
|
+
if (!types.includes(type))
|
|
98
|
+
throw new Error(`Invalid type selected: ${type}`)
|
|
99
|
+
|
|
100
|
+
const dirs = [type === 'server' ? 'base' : '']
|
|
101
|
+
const name = basename(/^(\.\/|\.\\|\.)$/.test(dir) ? _root : dir)
|
|
102
|
+
|
|
103
|
+
if (type === 'server') {
|
|
104
|
+
const template = args.template || (await select({
|
|
105
|
+
loop: true,
|
|
106
|
+
message: 'Which template do you want to use?',
|
|
107
|
+
choices: templates.map(v => ({ title: v, value: v})),
|
|
108
|
+
default: 'workerd',
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
if (!template)
|
|
112
|
+
throw new Error('No template selected')
|
|
113
|
+
if (!templates.includes(template))
|
|
114
|
+
throw new Error(`Invalid template selected: ${template}`)
|
|
115
|
+
|
|
116
|
+
dirs.push(template)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
existsSync(dir) && readdirSync(dir).length > 0
|
|
121
|
+
&& !(await confirm({ message: 'Directory not empty. Continue?', default: false }))
|
|
122
|
+
) {
|
|
123
|
+
process.exit(1)
|
|
124
|
+
} else {
|
|
125
|
+
mkdirp(dir)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const spinner = createSpinner('Cloning project..').start()
|
|
129
|
+
for (const d of dirs) {
|
|
130
|
+
const templateDir = resolve(import.meta.dir, `./templates/${type}/`+ d)
|
|
131
|
+
const files = readdirSync(templateDir)
|
|
132
|
+
for (const file of files)
|
|
133
|
+
copy(join(templateDir, file), join(dir, renameFiles[file] ?? file))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
spinner.success(bold('Cloned project'))
|
|
137
|
+
|
|
138
|
+
const pkgPath = join(dir, 'package.json')
|
|
139
|
+
if (existsSync(pkgPath)) {
|
|
140
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
141
|
+
pkg.name = name
|
|
142
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
const install = args.install || (await confirm({ message: 'Install dependencies?', default: false }))
|
|
147
|
+
if (install) {
|
|
148
|
+
const spinner2 = createSpinner('Installing project dependencies').start()
|
|
149
|
+
const proc = Bun.spawn(['bun', 'i'], { stdout: 'ignore', stderr: 'inherit' })
|
|
150
|
+
const procExit = await proc.exited
|
|
151
|
+
|
|
152
|
+
if (procExit === 0) {
|
|
153
|
+
spinner2.success(bold('Installed dependencies'))
|
|
154
|
+
} else {
|
|
155
|
+
spinner2.stop({
|
|
156
|
+
mark: red('×'),
|
|
157
|
+
text: 'Failed to install project dependencies',
|
|
158
|
+
})
|
|
159
|
+
exit(procExit)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
event(bold('Project successfully created'))
|
|
164
|
+
if (dir != _root) {
|
|
165
|
+
rn()
|
|
166
|
+
console.log(
|
|
167
|
+
dim('Get started with:'),
|
|
168
|
+
bold(`cd ${name} && duto dev`),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
charset = utf-8
|
|
5
|
+
end_of_line = lf
|
|
6
|
+
insert_final_newline = true
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
indent_style = space
|
|
9
|
+
indent_size = 2
|
|
10
|
+
|
|
11
|
+
# [*.{js,jsx,ts,tsx,json,yaml,yml}]
|
|
12
|
+
# Inherits all settings from [*] and applies to these extensions
|
|
13
|
+
|
|
14
|
+
[*.{json,jsonc}]
|
|
15
|
+
insert_final_newline = false
|
|
16
|
+
|
|
17
|
+
[*.md]
|
|
18
|
+
trim_trailing_whitespace = false
|
|
19
|
+
|
|
20
|
+
[Dockerfile]
|
|
21
|
+
indent_size = 4
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Duto Pages - Starter Template
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing.
|
|
4
|
+
|
|
5
|
+
## Install dependencies
|
|
6
|
+
|
|
7
|
+
To install, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun i
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Development environment
|
|
14
|
+
|
|
15
|
+
Test the application locally in an environment identical to production using the command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
duto dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
[Read the Full Docs to Learn More](https://hub.bi/docs/duto)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Editor directories and files
|
|
16
|
+
.vscode/*
|
|
17
|
+
!.vscode/extensions.json
|
|
18
|
+
.idea
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.suo
|
|
21
|
+
*.ntvs*
|
|
22
|
+
*.njsproj
|
|
23
|
+
*.sln
|
|
24
|
+
*.sw?
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useSignal } from '@preact/signals'
|
|
2
|
+
import { options } from '@/i18n'
|
|
3
|
+
import { local, getLocale, setLocale, isPrefixed, r } from 'duto'
|
|
4
|
+
import { useLayoutEffect } from 'preact/hooks'
|
|
5
|
+
|
|
6
|
+
import Select from './select'
|
|
7
|
+
|
|
8
|
+
function Languages(props: any) {
|
|
9
|
+
return (
|
|
10
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={[props.class || '', props.className || ''].filter(Boolean).join(' ')}><path d="m5 8 6 6" /><path d="m4 14 6-6 2-3" /><path d="M2 5h12" /><path d="M7 2h1" /><path d="m22 22-5-10-5 10" /><path d="M14 18h6" /></svg>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
export default function LocaleSelect({ class: className = '' }) {
|
|
14
|
+
const localeOptions = Object.entries(options).map(([value, label]) => ({ label, value }))
|
|
15
|
+
const selectedValue = useSignal(getLocale())
|
|
16
|
+
let unsubscribe: (() => void) | null = null
|
|
17
|
+
const handle = (e: any, lang?: string) => {
|
|
18
|
+
lang ??= getLocale()
|
|
19
|
+
selectedValue.value = lang
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
unsubscribe = local.subscribe('lang', e => !isPrefixed() && handle(null, e.val as string))
|
|
24
|
+
const lang = getLocale()
|
|
25
|
+
selectedValue.value = lang
|
|
26
|
+
|
|
27
|
+
window.addEventListener('popstate', handle)
|
|
28
|
+
return () => {
|
|
29
|
+
window.removeEventListener('popstate', handle)
|
|
30
|
+
unsubscribe?.()
|
|
31
|
+
}
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
const handleChange = async (e: Event) => {
|
|
35
|
+
const target = e.target as HTMLSelectElement
|
|
36
|
+
if (target) {
|
|
37
|
+
await setLocale(target.value)
|
|
38
|
+
selectedValue.value = target.value
|
|
39
|
+
isPrefixed() && r((window?.location?.pathname || '').split('/').filter(Boolean).slice(1).join('/'), target.value)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const containerClasses = [
|
|
44
|
+
'locale-select overflow-hidden text-ellipsis p-1 rounded-lg hover:bg-neutral-200 hover:text-zinc-900',
|
|
45
|
+
className
|
|
46
|
+
].filter(Boolean).join(' ')
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Select
|
|
50
|
+
Icon={Languages}
|
|
51
|
+
options={localeOptions}
|
|
52
|
+
value={selectedValue.value}
|
|
53
|
+
class={'outline-none text-ellipsis absolute inset-0 opacity-0 cursor-pointer' + className}
|
|
54
|
+
containerClass={containerClasses}
|
|
55
|
+
id="locale-select"
|
|
56
|
+
onchange={handleChange}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { forwardRef } from 'preact/compat'
|
|
2
|
+
|
|
3
|
+
type OptionObject = {
|
|
4
|
+
label: string
|
|
5
|
+
value: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type Option =
|
|
9
|
+
| OptionObject
|
|
10
|
+
| [value: string, label: string]
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
placeholder?: string,
|
|
14
|
+
value?: string,
|
|
15
|
+
Icon?: any,
|
|
16
|
+
// icon?: string,
|
|
17
|
+
id?: string,
|
|
18
|
+
class?: string,
|
|
19
|
+
containerClass?: string,
|
|
20
|
+
options: Record<string, Option[]> | Option[],
|
|
21
|
+
onchange?: (e: Event) => void,
|
|
22
|
+
onclick?: (e: Event) => void,
|
|
23
|
+
required?: boolean,
|
|
24
|
+
ref?: (el: HTMLSelectElement | null) => void,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderOption(op: Option, value: string) {
|
|
28
|
+
if (Array.isArray(op)) {
|
|
29
|
+
const [val, label] = op
|
|
30
|
+
return <option key={val} value={val}>{label}</option>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return <option key={op.value} value={op.value}>{op.label}</option>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default forwardRef<HTMLSelectElement, Props>(function Select({
|
|
37
|
+
placeholder,
|
|
38
|
+
value = '',
|
|
39
|
+
Icon,
|
|
40
|
+
id,
|
|
41
|
+
class: className,
|
|
42
|
+
containerClass,
|
|
43
|
+
options,
|
|
44
|
+
...props
|
|
45
|
+
}, ref) {
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<label
|
|
49
|
+
htmlFor={id}
|
|
50
|
+
class={[
|
|
51
|
+
'relative inline-flex items-center cursor-pointer select',
|
|
52
|
+
containerClass,
|
|
53
|
+
].filter(Boolean).join(' ')}
|
|
54
|
+
>
|
|
55
|
+
{Icon && <Icon class="size-6 shrink-0" />}
|
|
56
|
+
{/* {icon && (
|
|
57
|
+
<Icon
|
|
58
|
+
name={icon}
|
|
59
|
+
class="icon label-icon pointer-events-none"
|
|
60
|
+
/>
|
|
61
|
+
)} */}
|
|
62
|
+
|
|
63
|
+
<select
|
|
64
|
+
ref={ref}
|
|
65
|
+
id={id}
|
|
66
|
+
value={value}
|
|
67
|
+
autocomplete="off"
|
|
68
|
+
class={className}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{placeholder && (
|
|
72
|
+
<option disabled value="">{placeholder}</option>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{Array.isArray(options)
|
|
76
|
+
? options.map(op => renderOption(op, value))
|
|
77
|
+
: Object.entries(options).map(([label, opts]) => (
|
|
78
|
+
<optgroup
|
|
79
|
+
key={label}
|
|
80
|
+
label={label}
|
|
81
|
+
>
|
|
82
|
+
{opts.map(op => renderOption(op, value))}
|
|
83
|
+
</optgroup>
|
|
84
|
+
))}
|
|
85
|
+
</select>
|
|
86
|
+
|
|
87
|
+
{/* <Icon
|
|
88
|
+
name="lucide:chevron-down"
|
|
89
|
+
class="icon caret"
|
|
90
|
+
style="scale:115% 115%"
|
|
91
|
+
/> */}
|
|
92
|
+
</label>
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
routes: {
|
|
3
|
+
about: 'about',
|
|
4
|
+
shop: 'shop',
|
|
5
|
+
notfound: 'not-found',
|
|
6
|
+
},
|
|
7
|
+
messages: {
|
|
8
|
+
title: 'Welcome to Duto',
|
|
9
|
+
description: 'A fast, lightweight framework that keeps things simple!',
|
|
10
|
+
gotodocs: 'Go to docs',
|
|
11
|
+
notfound: 'Page not found',
|
|
12
|
+
sorry404: 'Sorry, the page you are looking for does not exist.',
|
|
13
|
+
backhome: 'Back to homepage',
|
|
14
|
+
about: 'About',
|
|
15
|
+
shop: 'Shop',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const fallbackLocale = 'pt-br'
|
|
2
|
+
export const locales = ['en', 'pt-br']
|
|
3
|
+
|
|
4
|
+
export const options = {
|
|
5
|
+
en: 'English',
|
|
6
|
+
'pt-br': 'Português (Brasil)',
|
|
7
|
+
// de: 'Deutsch',
|
|
8
|
+
// es: 'Español',
|
|
9
|
+
// 'zh-cn': '简体中文',
|
|
10
|
+
// 'zh-tw': '正體中文',
|
|
11
|
+
// fr: 'Français',
|
|
12
|
+
// hi: 'हिन्दी',
|
|
13
|
+
// ar: 'العربية',
|
|
14
|
+
// ja: '日本語',
|
|
15
|
+
// ko: '한국어',
|
|
16
|
+
// pl: 'Polski',
|
|
17
|
+
// ru: 'Русский',
|
|
18
|
+
// it: 'Italiano',
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
routes: {
|
|
3
|
+
about: 'sobre',
|
|
4
|
+
shop: 'loja',
|
|
5
|
+
notfound: 'pagina-desconhecida',
|
|
6
|
+
},
|
|
7
|
+
messages: {
|
|
8
|
+
title: 'Bem-vindo ao Duto',
|
|
9
|
+
description: 'Um framework rápido e leve que mantém as coisas simples!',
|
|
10
|
+
gotodocs: 'Documentação',
|
|
11
|
+
notfound: 'Página não encontrada',
|
|
12
|
+
sorry404: 'Esta página pode ter sido excluída ou movida...',
|
|
13
|
+
backhome: 'Ir para página inicial',
|
|
14
|
+
about: 'Sobre',
|
|
15
|
+
shop: 'Loja',
|
|
16
|
+
},
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "duto-docs",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"duto": "workspace:*",
|
|
7
|
+
"@preact/signals": "catalog:",
|
|
8
|
+
"preact": "catalog:"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"bun-types": "catalog:",
|
|
12
|
+
"tailwindcss": "catalog:",
|
|
13
|
+
"typescript": "catalog:"
|
|
14
|
+
},
|
|
15
|
+
"duto": {
|
|
16
|
+
"type": "pages",
|
|
17
|
+
"platform": "workerd",
|
|
18
|
+
"dev": {
|
|
19
|
+
"port": 3000
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { t, Link } from 'duto'
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div class="flex flex-col items-center justify-center text-center w-full text-lg py-16">
|
|
6
|
+
<h1 class="text-sky-gratient text-mono text-7xl lg:text-8xl">404</h1>
|
|
7
|
+
<p class="body-large mb-2 mt-4 text-3xl lg:text-4xl">{t`notfound`}</p>
|
|
8
|
+
<p class="body lg:body-large">{t`sorry404`}</p>
|
|
9
|
+
<Link to="/" class="border-b border-current mt-8">{t`backhome`}</Link>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { t, Link } from 'duto'
|
|
2
|
+
|
|
3
|
+
export default function Index() {
|
|
4
|
+
return (
|
|
5
|
+
<div class="relative">
|
|
6
|
+
<h1 class="title text-sky-gratient mt-10 lg:mt-2">{t`title`}</h1>
|
|
7
|
+
<p class="sub-title">{t`description`}</p>
|
|
8
|
+
<div class="flex flex-row gap-8 mt-10">
|
|
9
|
+
|
|
10
|
+
<Link href="https://hub.bi/docs/duto" class="group inline-flex shrink-0 items-center justify-center leading-6 font-medium select-none focus:outline-0 gap-2 px-4 py-2.5 text-base rounded-lg bg-white hover:bg-neutral-200 border border-gray-700 text-zinc-900">
|
|
11
|
+
{t`gotodocs`}<span class="ml-0.5 inline-block transition-transform group-hover:translate-x-1">→</span>
|
|
12
|
+
</Link>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact'
|
|
2
|
+
|
|
3
|
+
import LocaleSelect from '@/components/locale-select'
|
|
4
|
+
|
|
5
|
+
export default function Layout({ children }: { children: ComponentChildren }) {
|
|
6
|
+
return (
|
|
7
|
+
<div class="relative w-full max-w-full min-h-svh overflow-hidden flex flex-col flex-1 justify-center items-center bg-gradient-to-br from-zinc-800 to-zinc-950 text-gray-300 select-none">
|
|
8
|
+
<div class='absolute top-10 right-10'>
|
|
9
|
+
<LocaleSelect />
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<main class="flex justify-center items-center px-6 py-16 min-h-[calc(100vh-3.75rem)]" children={children} />
|
|
13
|
+
|
|
14
|
+
<footer class="py-[1.1rem] relative text-base lex flex-row items-center mx-auto">
|
|
15
|
+
<a href="https://hub.bi?ref=duto" class="no-underline group flex items-center justify-center" target="_blank" rel="noopener noreferrer">
|
|
16
|
+
Powered by
|
|
17
|
+
<span class="group-hover:underline flex items-center">
|
|
18
|
+
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 20 20" aria-hidden="true" class="mx-1 text-xl" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16.555 5.412a8.028 8.028 0 0 0-3.503-2.81 14.899 14.899 0 0 1 1.663 4.472 8.547 8.547 0 0 0 1.84-1.662ZM13.326 7.825a13.43 13.43 0 0 0-2.413-5.773 8.087 8.087 0 0 0-1.826 0 13.43 13.43 0 0 0-2.413 5.773A8.473 8.473 0 0 0 10 8.5c1.18 0 2.304-.24 3.326-.675ZM6.514 9.376A9.98 9.98 0 0 0 10 10c1.226 0 2.4-.22 3.486-.624a13.54 13.54 0 0 1-.351 3.759A13.54 13.54 0 0 1 10 13.5c-1.079 0-2.128-.127-3.134-.366a13.538 13.538 0 0 1-.352-3.758ZM5.285 7.074a14.9 14.9 0 0 1 1.663-4.471 8.028 8.028 0 0 0-3.503 2.81c.529.638 1.149 1.199 1.84 1.66ZM17.334 6.798a7.973 7.973 0 0 1 .614 4.115 13.47 13.47 0 0 1-3.178 1.72 15.093 15.093 0 0 0 .174-3.939 10.043 10.043 0 0 0 2.39-1.896ZM2.666 6.798a10.042 10.042 0 0 0 2.39 1.896 15.196 15.196 0 0 0 .174 3.94 13.472 13.472 0 0 1-3.178-1.72 7.973 7.973 0 0 1 .615-4.115ZM10 15c.898 0 1.778-.079 2.633-.23a13.473 13.473 0 0 1-1.72 3.178 8.099 8.099 0 0 1-1.826 0 13.47 13.47 0 0 1-1.72-3.178c.855.151 1.735.23 2.633.23ZM14.357 14.357a14.912 14.912 0 0 1-1.305 3.04 8.027 8.027 0 0 0 4.345-4.345c-.953.542-1.971.981-3.04 1.305ZM6.948 17.397a8.027 8.027 0 0 1-4.345-4.345c.953.542 1.971.981 3.04 1.305a14.912 14.912 0 0 0 1.305 3.04Z"></path></svg>
|
|
19
|
+
HUB</span>
|
|
20
|
+
</a>
|
|
21
|
+
</footer>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|