duto 0.260915.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 +1 -1
- package/package.json +1 -1
- package/src/commands/new.ts +55 -30
- 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/{base → server/base}/README.md +1 -1
- package/src/pages/cli/commands/dev.ts +10 -4
- package/src/pages/cli/utils.ts +3 -22
- package/src/pages/client/app.tsx +26 -73
- package/src/pages/client/router.ts +14 -280
- 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/index.ts +20 -126
- package/src/pages/compiler/meta.ts +38 -227
- package/src/pages/compiler/routes.ts +4 -5
- package/src/pages/compiler/types.ts +1 -101
- package/src/pages/compiler/vite-plugin.ts +73 -27
- package/src/pages/i18n/client.ts +18 -6
- package/src/pages/i18n/server.ts +21 -6
- package/src/pages/index.ts +1 -2
- package/src/server/routes.ts +4 -26
- package/src/utils.ts +34 -3
- package/src/pages/compiler/island.ts +0 -108
- /package/src/commands/templates/{base → pages}/.editorconfig +0 -0
- /package/src/commands/templates/{base → server/base}/.env.dev.example +0 -0
- /package/src/commands/templates/{base → server/base}/.env.prod.example +0 -0
- /package/src/commands/templates/{base → server/base}/_gitignore +0 -0
- /package/src/commands/templates/{base → server/base}/actions/blog/index.get.ts +0 -0
- /package/src/commands/templates/{base → server/base}/actions/blog/post/[id].get.ts +0 -0
- /package/src/commands/templates/{base → server/base}/actions/index.get.ts +0 -0
- /package/src/commands/templates/{base → server/base}/configs/duto.ts +0 -0
- /package/src/commands/templates/{base → server/base}/configs/roles.ts +0 -0
- /package/src/commands/templates/{base → server/base}/package.json +0 -0
- /package/src/commands/templates/{base → server/base}/tsconfig.json +0 -0
- /package/src/commands/templates/{workerd → server/workerd}/wrangler.toml +0 -0
|
@@ -8,17 +8,6 @@ export type PluginOptions = {
|
|
|
8
8
|
pagesDir?: string,
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export type DepMetadata = {
|
|
12
|
-
path: string,
|
|
13
|
-
ref: string,
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type FileMetadata = {
|
|
17
|
-
path: string,
|
|
18
|
-
content: string,
|
|
19
|
-
deps: Map<string, DepMetadata>,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
11
|
export type I18nMetadata = {
|
|
23
12
|
fallbackLocale: string,
|
|
24
13
|
locales: string[],
|
|
@@ -52,15 +41,12 @@ export type Metadata = {
|
|
|
52
41
|
minify: boolean,
|
|
53
42
|
|
|
54
43
|
aliases: Record<string, string>,
|
|
55
|
-
map: Map<string, FileMetadata>,
|
|
56
|
-
violations: Map<string, DepMetadata[]>,
|
|
57
44
|
layouts: Map<string, IRoute>, // all layouts
|
|
58
45
|
nf: Map<string, IRoute>, // all 404
|
|
59
46
|
pages: Map<string, IRoute>, // all
|
|
60
47
|
spages: Set<string>, // ssg pages
|
|
61
48
|
routes: Routes, // client-side
|
|
62
49
|
// routes: Map<string, IRoute>, // client-side
|
|
63
|
-
assets: string[],
|
|
64
50
|
i18n: I18nMetadata,
|
|
65
51
|
string: string,
|
|
66
52
|
summary: Map<string, SummaryMetadata>,
|
|
@@ -69,8 +55,7 @@ export type Metadata = {
|
|
|
69
55
|
export type Page<P = {}> = FunctionComponent<VNode<P>>
|
|
70
56
|
// A page module as imported by Bun/Vite
|
|
71
57
|
export type PageModule<P = {}> = {
|
|
72
|
-
default: Page<P>,
|
|
73
|
-
// default: ComponentType<VNode<any>>,
|
|
58
|
+
default: Page<P>, // ComponentType<VNode<any>>,
|
|
74
59
|
getStaticProps?: GetStaticProps,
|
|
75
60
|
getProps?: GetProps,
|
|
76
61
|
}
|
|
@@ -86,12 +71,6 @@ export type GetStaticProps =
|
|
|
86
71
|
| (() => StaticProps[] | Promise<StaticProps[]>)
|
|
87
72
|
export type StaticProps = Record<string, unknown>
|
|
88
73
|
|
|
89
|
-
/** browser-only */
|
|
90
|
-
// export type $duto = {
|
|
91
|
-
// sroutes: Set<string>,
|
|
92
|
-
// navigate: {},
|
|
93
|
-
// route: { pattern: string, params: RouteParams, path: string },
|
|
94
|
-
// }
|
|
95
74
|
export type GetProps = (params: RouteParams) => StaticProps | Promise<StaticProps>
|
|
96
75
|
export type RouteParams = Record<string, string | string[] | undefined>
|
|
97
76
|
|
|
@@ -99,14 +78,9 @@ export type Routes = IRoute[]
|
|
|
99
78
|
export type IRoute = {
|
|
100
79
|
content: string,
|
|
101
80
|
mod: PageModule,
|
|
102
|
-
/** True if the page exports getStaticProps */
|
|
103
81
|
hasGetStaticProps: boolean,
|
|
104
|
-
/** True if the page exports getProps */
|
|
105
82
|
hasGetProps: boolean,
|
|
106
83
|
isClientOnly: boolean,
|
|
107
|
-
islands: ImportUsage[], // TODO mover para cá
|
|
108
|
-
// islands: IslandMeta[],
|
|
109
|
-
// safeFilePath: string,
|
|
110
84
|
layouts: IRoute[],
|
|
111
85
|
importName: string,
|
|
112
86
|
|
|
@@ -143,77 +117,3 @@ export type RouteParam = {
|
|
|
143
117
|
catchAll: boolean,
|
|
144
118
|
value?: string | string[],
|
|
145
119
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
export interface Usage {
|
|
150
|
-
type: 'jsx' | 'call' | 'reference'
|
|
151
|
-
source: string,
|
|
152
|
-
imported: string,
|
|
153
|
-
local: string,
|
|
154
|
-
start: number,
|
|
155
|
-
end: number,
|
|
156
|
-
text: string,
|
|
157
|
-
node: unknown,
|
|
158
|
-
parent: unknown,
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface ImportUsage {
|
|
162
|
-
source: string,
|
|
163
|
-
imported: string,
|
|
164
|
-
local: string,
|
|
165
|
-
importStart: number,
|
|
166
|
-
importEnd: number,
|
|
167
|
-
importText: string,
|
|
168
|
-
usages: Usage[],
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface IslandMeta {
|
|
172
|
-
/** Identificador curto e único (base64url de 7 chars) */
|
|
173
|
-
id: string
|
|
174
|
-
/** URL pública do asset JS do componente */
|
|
175
|
-
url: string
|
|
176
|
-
/** Nome do export usado */
|
|
177
|
-
export: string
|
|
178
|
-
/** Props serializadas como JSON (HTML-encoded) */
|
|
179
|
-
props: string
|
|
180
|
-
/** HTML do SSR do componente */
|
|
181
|
-
ssrHtml: string
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// export interface ReplaceIslandsOptions {
|
|
185
|
-
// /**
|
|
186
|
-
// * Função que resolve o nome do componente para a URL pública do asset JS.
|
|
187
|
-
// * Ex: 'Counter' → '/assets/Counter.BQxoyJbC.js'
|
|
188
|
-
// */
|
|
189
|
-
// resolveUrl: (exportName: string) => string
|
|
190
|
-
// /**
|
|
191
|
-
// * Função que carrega o módulo do componente para fazer SSR.
|
|
192
|
-
// * Recebe o source da importação (ex: '@/components/counter2') e o nome do export.
|
|
193
|
-
// */
|
|
194
|
-
// loadComponent: (source: string, exportName: string) => Promise<unknown>
|
|
195
|
-
// }
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// export type Island = {
|
|
199
|
-
// /** Unique id — written as data-island on the placeholder div */
|
|
200
|
-
// id: string,
|
|
201
|
-
// /** Component display name — used by the client to look up the component */
|
|
202
|
-
// name: string,
|
|
203
|
-
// /** Props serialised into the placeholder for client hydration */
|
|
204
|
-
// props: Record<string, unknown>,
|
|
205
|
-
// }
|
|
206
|
-
|
|
207
|
-
// export type RenderResult = {
|
|
208
|
-
// html: string,
|
|
209
|
-
// islands: Island[],
|
|
210
|
-
// /** true if any islands were found (= client JS needed) */
|
|
211
|
-
// hasIslands: boolean,
|
|
212
|
-
// }
|
|
213
|
-
|
|
214
|
-
// export type IslandMeta = {
|
|
215
|
-
// id: string,
|
|
216
|
-
// name: string,
|
|
217
|
-
// props: Record<string, unknown>,
|
|
218
|
-
// }
|
|
219
|
-
|
|
@@ -6,17 +6,17 @@ import { defineConfig } from 'vite'
|
|
|
6
6
|
|
|
7
7
|
import preact from '@preact/preset-vite'
|
|
8
8
|
import tailwind from '@tailwindcss/vite'
|
|
9
|
+
import { JSJSON } from 't0n'
|
|
9
10
|
|
|
10
11
|
import { EXT_RE } from './constants'
|
|
11
|
-
import { setAsset, setChunk, setEntry } from './assets'
|
|
12
|
+
import { getChunk, setAsset, setChunk, setEntry } from './assets'
|
|
12
13
|
import { renderPage } from '.'
|
|
13
14
|
import { removeExports, resolveImport } from './ast'
|
|
14
15
|
import { getMetadata } from './meta'
|
|
15
16
|
|
|
16
17
|
import type {
|
|
17
|
-
// Plugin,
|
|
18
18
|
UserConfig, ConfigEnv, ResolvedConfig,
|
|
19
|
-
PreviewServer,
|
|
19
|
+
// PreviewServer,
|
|
20
20
|
// ViteDevServer,
|
|
21
21
|
EnvironmentOptions,
|
|
22
22
|
PluginOption,
|
|
@@ -42,6 +42,7 @@ export const config = (opts: UserConfig & PluginOptions) => { // @ts-ignore
|
|
|
42
42
|
return defineConfig(opts)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// TODO: remover..
|
|
45
46
|
export default config
|
|
46
47
|
|
|
47
48
|
let metadata: Metadata
|
|
@@ -50,13 +51,11 @@ export function meta() {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
53
|
-
// let server: ViteDevServer
|
|
54
54
|
let config: ResolvedConfig
|
|
55
55
|
const {
|
|
56
|
-
root = _root,
|
|
56
|
+
root = _root,
|
|
57
57
|
pagesDir = 'pages',
|
|
58
58
|
} = options
|
|
59
|
-
// let metadata: Metadata
|
|
60
59
|
|
|
61
60
|
const PAGES_RE = new RegExp(`\/${pagesDir}\/.*\.(tsx?|jsx?)$`, 'i')
|
|
62
61
|
const stripTarget = new Set(['getStaticProps'])
|
|
@@ -105,18 +104,9 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
105
104
|
name: 'duto:scan',
|
|
106
105
|
enforce: 'pre',
|
|
107
106
|
|
|
108
|
-
// configureServer(_server) {
|
|
109
|
-
// server = _server
|
|
110
|
-
// // servers.push(server)
|
|
111
|
-
// },
|
|
112
|
-
|
|
113
107
|
async config(conf: UserConfig, env: ConfigEnv) {
|
|
114
108
|
metadata = await getMetadata(root as string, pagesDir)
|
|
115
109
|
|
|
116
|
-
// console.log(metadata.map)
|
|
117
|
-
// console.log(metadata.violations)
|
|
118
|
-
// console.log(Array.from(metadata.violations).filter(file => EXT_RE.test(file)))
|
|
119
|
-
|
|
120
110
|
if (conf.build) {
|
|
121
111
|
// conf.build.emptyOutDir = false
|
|
122
112
|
conf.build.copyPublicDir = false
|
|
@@ -129,10 +119,11 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
129
119
|
conf.build.rolldownOptions = conf.build.rolldownOptions || {}
|
|
130
120
|
conf.build.rolldownOptions.preserveEntrySignatures = 'exports-only' ///
|
|
131
121
|
conf.build.rolldownOptions.input = [
|
|
122
|
+
...metadata.routes.map(r => r.is404 ? '' : r.fullFilePath).filter(Boolean),
|
|
132
123
|
':duto/client/app.tsx',
|
|
133
124
|
// ':duto/not-found.tsx',
|
|
134
|
-
...Object.keys(metadata.pages),
|
|
135
|
-
...metadata.assets,
|
|
125
|
+
// ...Object.keys(metadata.pages),
|
|
126
|
+
// ...metadata.assets,
|
|
136
127
|
]
|
|
137
128
|
|
|
138
129
|
const filePrefix = metadata.minify ? '' : '[name].'
|
|
@@ -141,11 +132,6 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
141
132
|
hoistTransitiveImports: false,
|
|
142
133
|
format: 'esm',
|
|
143
134
|
minifyInternalExports: metadata.minify,
|
|
144
|
-
|
|
145
|
-
// manualChunks: {
|
|
146
|
-
// vendor: ['preact', '@preact/signals', 'wouter-preact'],
|
|
147
|
-
// },
|
|
148
|
-
// preserveModules: true,
|
|
149
135
|
preserveModulesRoot: '/',
|
|
150
136
|
assetFileNames: filePrefix +'[hash][extname]',
|
|
151
137
|
entryFileNames: filePrefix +'[hash].js',
|
|
@@ -155,14 +141,19 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
155
141
|
// entryFileNames: 'assets/[name].js',
|
|
156
142
|
// chunkFileNames: 'assets/chunks/[name]-[hash].js',
|
|
157
143
|
// assetFileNames: 'assets/[name][extname]',
|
|
144
|
+
|
|
145
|
+
// manualChunks: {
|
|
146
|
+
// vendor: ['preact', '@preact/signals', 'wouter-preact'],
|
|
147
|
+
// },
|
|
148
|
+
// preserveModules: true,
|
|
158
149
|
}
|
|
159
150
|
}
|
|
160
151
|
|
|
161
152
|
return conf
|
|
162
153
|
},
|
|
163
154
|
|
|
164
|
-
async configResolved(
|
|
165
|
-
config =
|
|
155
|
+
async configResolved(c) {
|
|
156
|
+
config = c
|
|
166
157
|
},
|
|
167
158
|
|
|
168
159
|
resolveId(id) {
|
|
@@ -217,15 +208,18 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
217
208
|
// for (const id of moduleIds)
|
|
218
209
|
// console.error(id)
|
|
219
210
|
|
|
211
|
+
// TODO: rever isso...
|
|
220
212
|
for (const key in bundle) {
|
|
221
213
|
const file = bundle[key]
|
|
214
|
+
|
|
222
215
|
if (file.type === 'chunk' && file.isEntry) {
|
|
223
216
|
setChunk(file.facadeModuleId, '/'+ file.fileName)
|
|
224
217
|
continue
|
|
225
218
|
}
|
|
226
219
|
|
|
227
220
|
if (file.type === 'asset' && file?.originalFileName?.endsWith('duto/pages/client/app.tsx'))
|
|
228
|
-
file.source += '
|
|
221
|
+
file.source += 'i-i{display:contents}'
|
|
222
|
+
// file.source += 'd-b,d-p,i-d,i-s,i-i{display:contents}'
|
|
229
223
|
|
|
230
224
|
if (file.type === 'asset' && file.fileName.endsWith('.css')) {
|
|
231
225
|
setAsset(resolveImport(file.originalFileName as string, metadata.root, metadata.aliases), '/'+ file.fileName)
|
|
@@ -241,6 +235,35 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
241
235
|
|
|
242
236
|
},
|
|
243
237
|
|
|
238
|
+
// {
|
|
239
|
+
// name: 'duto:routes-inject',
|
|
240
|
+
// enforce: 'post',
|
|
241
|
+
// generateBundle(_, bundle) {
|
|
242
|
+
// // monta o mapa a partir dos inputs de metadata.routes
|
|
243
|
+
// const routeMap: Record<string, string> = {}
|
|
244
|
+
// for (const r of metadata.routes) {
|
|
245
|
+
// if (r.is404) continue
|
|
246
|
+
// // encontra o chunk cujo facadeModuleId bate com r.fullFilePath
|
|
247
|
+
// for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
248
|
+
// if (chunk.type === 'chunk' && chunk.facadeModuleId === r.fullFilePath) {
|
|
249
|
+
// routeMap[r.path] = fileName
|
|
250
|
+
// break
|
|
251
|
+
// }
|
|
252
|
+
// }
|
|
253
|
+
// }
|
|
254
|
+
|
|
255
|
+
// // injeta no chunk do app
|
|
256
|
+
// for (const chunk of Object.values(bundle)) {
|
|
257
|
+
// if (chunk.type !== 'chunk') continue
|
|
258
|
+
// if (chunk.facadeModuleId?.includes('app.tsx') || chunk.code.includes('__DUTO_ROUTES__')) {
|
|
259
|
+
// chunk.code = chunk.code.replace(
|
|
260
|
+
// '__DUTO_ROUTES__',
|
|
261
|
+
// JSON.stringify(routeMap)
|
|
262
|
+
// )
|
|
263
|
+
// }
|
|
264
|
+
// }
|
|
265
|
+
// }
|
|
266
|
+
// },
|
|
244
267
|
{
|
|
245
268
|
name: 'duto:build',
|
|
246
269
|
enforce: 'pre',
|
|
@@ -275,13 +298,36 @@ export function plugin(options: PluginOptions = {}): PluginOption[] {
|
|
|
275
298
|
},
|
|
276
299
|
|
|
277
300
|
async closeBundle() {
|
|
278
|
-
for (const [_filePath, page] of metadata.pages)
|
|
301
|
+
for (const [_filePath, page] of metadata.pages)
|
|
279
302
|
await renderPage(metadata, config, page)
|
|
280
303
|
// const mod = await server.ssrLoadModule(_filePath)
|
|
281
|
-
// }
|
|
282
304
|
|
|
283
305
|
rmSync(join(metadata.root, 'tmp'), {recursive: true, force: true})
|
|
284
306
|
// await server.close()
|
|
307
|
+
|
|
308
|
+
const outPath = join(config.root, config.build.outDir)
|
|
309
|
+
const entry = Bun.file(join(outPath, getChunk(':duto/client/app.tsx')))
|
|
310
|
+
let entryContent = await entry.text()
|
|
311
|
+
|
|
312
|
+
for (const r of metadata.routes.filter(r => !r.is404))
|
|
313
|
+
entryContent = entryContent.replace(r.fullFilePath, getChunk(r.fullFilePath.replace(config.root, '')))
|
|
314
|
+
|
|
315
|
+
await entry.write(entryContent)
|
|
316
|
+
|
|
317
|
+
// i18n
|
|
318
|
+
for (const locale of metadata.i18n.locales) {
|
|
319
|
+
const intlPath = join(metadata.root, 'i18n', locale + '.ts')
|
|
320
|
+
if (existsSync(intlPath)) {
|
|
321
|
+
const intlMod = await import(intlPath)
|
|
322
|
+
await Bun.write(
|
|
323
|
+
join(outPath, locale +'.js'),
|
|
324
|
+
`var a=${JSJSON({
|
|
325
|
+
routes: intlMod.routes ?? intlMod.default?.routes,
|
|
326
|
+
messages: intlMod.messages ?? intlMod.default?.messages,
|
|
327
|
+
})};export { a as default }`
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
285
331
|
},
|
|
286
332
|
|
|
287
333
|
// TODO: precisa ser aqui mesmo pois se a pagina for importada no entry, pode incluir codigo nao usado no chunck/bundle
|
package/src/pages/i18n/client.ts
CHANGED
|
@@ -84,12 +84,22 @@ export const getLocale = () => {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export const setLocale = (lang: string) => {
|
|
87
|
+
export const setLocale = async (lang: string) => {
|
|
88
|
+
await loadLang(lang)
|
|
88
89
|
cache(lang)
|
|
89
90
|
_locale = accepted(lang, _locales, _detected)
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
async function loadLang(lang: string) {
|
|
94
|
+
if (!_routes[lang] || !_messages[lang]) {
|
|
95
|
+
const intlMod = await import('/'+ lang +'.js')
|
|
96
|
+
if (!intlMod) return
|
|
97
|
+
_routes[lang] = intlMod.routes ?? intlMod.default?.routes
|
|
98
|
+
_messages[lang] = intlMod.messages ?? intlMod.default?.messages
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default async function i18n(
|
|
93
103
|
fallbackLocale: string,
|
|
94
104
|
locales: string[],
|
|
95
105
|
messages: Record<string, Record<string, string>>,
|
|
@@ -103,6 +113,8 @@ export default function i18n(
|
|
|
103
113
|
_detected = detect(locales, fallbackLocale)
|
|
104
114
|
_locale = typeof window === 'undefined' ? fallbackLocale : getLocale()
|
|
105
115
|
|
|
116
|
+
await loadLang(_locale)
|
|
117
|
+
|
|
106
118
|
// console.error(routes)
|
|
107
119
|
// console.error(routeMap)
|
|
108
120
|
}
|
|
@@ -173,10 +185,10 @@ type TProps = {
|
|
|
173
185
|
}
|
|
174
186
|
|
|
175
187
|
const lookup = (key: string, lang: string): string => {
|
|
176
|
-
if (!key) return ''
|
|
177
|
-
if (key.startsWith('_')) return key.replace(/^_+/, '')
|
|
178
|
-
const msg = _messages[lang] ?? _messages[_locale]
|
|
179
|
-
return (key in msg ? msg[key] : key) as string
|
|
188
|
+
if (!key) return ''
|
|
189
|
+
if (key.startsWith('_')) return key.replace(/^_+/, '')
|
|
190
|
+
const msg = _messages[lang] ?? _messages[_locale]
|
|
191
|
+
return (msg && key in msg ? msg[key] : key) as string
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
const interpolate = (template: string, params: Record<string, Primitives> | Primitives[]): string => {
|
package/src/pages/i18n/server.ts
CHANGED
|
@@ -11,7 +11,7 @@ export default async function getI18n(meta: Metadata) {
|
|
|
11
11
|
'i18n.ts',
|
|
12
12
|
'i18n/index.ts',
|
|
13
13
|
]) {
|
|
14
|
-
|
|
14
|
+
const path = join(meta.root, variant)
|
|
15
15
|
if (existsSync(path)) {
|
|
16
16
|
file = path
|
|
17
17
|
break
|
|
@@ -22,13 +22,28 @@ export default async function getI18n(meta: Metadata) {
|
|
|
22
22
|
const mod = await IMPORT<I18nMetadata>(file)
|
|
23
23
|
|
|
24
24
|
// @ts-ignore
|
|
25
|
-
if (['locales', 'fallbackLocale', '
|
|
25
|
+
if (['locales', 'fallbackLocale', 'options'].some(i => !mod[i]))
|
|
26
26
|
return {} as I18nMetadata
|
|
27
27
|
|
|
28
|
+
// const routes = Object.fromEntries(mod.locales.map(locale => [locale, ]))
|
|
29
|
+
|
|
30
|
+
const routes: Record<string, Record<string, string>> = {}
|
|
31
|
+
const messages: Record<string, Record<string, string>> = {}
|
|
32
|
+
for (const locale of mod.locales) {
|
|
33
|
+
const intlPath = join(meta.root, 'i18n', locale + '.ts')
|
|
34
|
+
if (existsSync(intlPath)) {
|
|
35
|
+
const intlMod = await IMPORT<{default:{routes: Record<string, string>, messages: Record<string, string>}}>(intlPath)
|
|
36
|
+
routes[locale] = intlMod.default.routes
|
|
37
|
+
messages[locale] = intlMod.default.messages
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
const i18n: I18nMetadata = {
|
|
29
42
|
...mod,
|
|
43
|
+
routes,
|
|
44
|
+
messages,
|
|
30
45
|
routeMap: Object.fromEntries(
|
|
31
|
-
Object.values(
|
|
46
|
+
Object.values(routes)
|
|
32
47
|
.map(paths => Object.entries(paths).map(([key, path]) => path == key ? 0 : [path, key]).filter(Boolean))
|
|
33
48
|
.flat() as Array<[string, string]>
|
|
34
49
|
),
|
|
@@ -36,7 +51,7 @@ export default async function getI18n(meta: Metadata) {
|
|
|
36
51
|
string: '',
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
Object.entries(
|
|
54
|
+
Object.entries(routes)
|
|
40
55
|
.forEach(([locale, paths]) => {
|
|
41
56
|
Object.entries(paths)
|
|
42
57
|
.forEach(([route, path]) => i18n.pageRoutes.push({ locale, path }))
|
|
@@ -44,8 +59,8 @@ export default async function getI18n(meta: Metadata) {
|
|
|
44
59
|
|
|
45
60
|
i18n.string = `export const fallbackLocale = ${JSJSON(mod.fallbackLocale)}
|
|
46
61
|
export const locales = ${JSJSON(mod.locales)}
|
|
47
|
-
export const messages = ${JSJSON(
|
|
48
|
-
export const routes = ${JSJSON(
|
|
62
|
+
export const messages = ${JSJSON(messages)}
|
|
63
|
+
export const routes = ${JSJSON(routes)}
|
|
49
64
|
export const routeMap = ${JSJSON(i18n.routeMap)}
|
|
50
65
|
export const pageRoutes = ${JSJSON(i18n.pageRoutes)}
|
|
51
66
|
export const options = ${JSJSON(mod.options)}
|
package/src/pages/index.ts
CHANGED
package/src/server/routes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { file, write } from 'bun'
|
|
2
2
|
|
|
3
3
|
import { copyFileSync } from 'node:fs'
|
|
4
4
|
import { join, relative } from 'pathe'
|
|
@@ -12,7 +12,7 @@ import { STATUS_CODES } from 'node:http'
|
|
|
12
12
|
import $ from './app'
|
|
13
13
|
import _response from './response'
|
|
14
14
|
import _validator from './validator'
|
|
15
|
-
import { _root, _server } from '@/utils'
|
|
15
|
+
import { _root, _server, scan } from '@/utils'
|
|
16
16
|
import { getLastCommitHash, ensureDir, nextId, verbAlias } from './utils'
|
|
17
17
|
import { generateOpenAPI, generateOpenAPIClient, openAPIConfig } from './open-api/spec'
|
|
18
18
|
import { highlightedMethod, highlightedURI } from './cli/utils'
|
|
@@ -21,28 +21,6 @@ import type { Routes, Rule, StandardSchemaV1 } from './types'
|
|
|
21
21
|
import { autorizedPath } from './middlewares/auth'
|
|
22
22
|
import { isDev } from './utils/environment'
|
|
23
23
|
|
|
24
|
-
export async function* scan(
|
|
25
|
-
dir: string | string[],
|
|
26
|
-
ext: string | string[],
|
|
27
|
-
ignore: string[] = ['*.d.*'],
|
|
28
|
-
cwd?: string,
|
|
29
|
-
): AsyncGenerator<[string, string, any]> {
|
|
30
|
-
if (typeof dir !== 'string')
|
|
31
|
-
dir = ext.length === 1 ? dir[0] : `{${dir.join(',')}}`
|
|
32
|
-
|
|
33
|
-
if (typeof ext !== 'string')
|
|
34
|
-
ext = ext.length === 1 ? ext[0] : `{${ext.join(',')}}`
|
|
35
|
-
|
|
36
|
-
cwd ??= _root
|
|
37
|
-
for await (const file of new Glob(dir +'/**/*.'+ ext).scan({
|
|
38
|
-
cwd,
|
|
39
|
-
absolute: true,
|
|
40
|
-
onlyFiles: true, // @ts-ignore
|
|
41
|
-
ignore,
|
|
42
|
-
})) {
|
|
43
|
-
yield [file, file.split(cwd as string)[1], await IMPORT(file)]
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
24
|
|
|
47
25
|
const importName = (name: string | undefined, dirs: string[]) => (name || nextId()).replace(/\.ts$/, '')
|
|
48
26
|
|
|
@@ -407,10 +385,10 @@ export async function cacheRoutes() {
|
|
|
407
385
|
if (isDev()) {
|
|
408
386
|
const _file = join(_server, 'middlewares', 'logger.ts')
|
|
409
387
|
const _mod = await IMPORT(_file) // @ts-ignore
|
|
410
|
-
middlewares.unshift({
|
|
388
|
+
middlewares.unshift({ // @ts-ignore
|
|
411
389
|
path: _mod?.path || '*',
|
|
412
390
|
name: 'dev_logger',
|
|
413
|
-
file: _file,
|
|
391
|
+
file: _file, // @ts-ignore
|
|
414
392
|
handle: _mod.handle,
|
|
415
393
|
})
|
|
416
394
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IMPORT } from 't0n'
|
|
2
2
|
import { dirname, join } from 'pathe'
|
|
3
3
|
|
|
4
4
|
export const _duto = dirname(new URL(import.meta.url).pathname)
|
|
5
5
|
export const _pages = join(_duto, 'pages')
|
|
6
|
-
export const _server = join(_duto, 'server')
|
|
7
|
-
export const _root =
|
|
6
|
+
export const _server = join(_duto, 'server') //@ts-ignore
|
|
7
|
+
export const _root = process.env?.npm_config_local_prefix || process.env?.PWD || process.stdin?.path || process.cwd() // || join(_duto, '../../')s
|
|
8
8
|
|
|
9
9
|
const UNITS = ['B', 'kB', 'MB', 'GB']
|
|
10
10
|
export function filesize(bytes: number, decimals = 2) {
|
|
@@ -13,3 +13,34 @@ export function filesize(bytes: number, decimals = 2) {
|
|
|
13
13
|
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), UNITS.length - 1)
|
|
14
14
|
return `${parseFloat((bytes / 1024 ** i).toFixed(decimals))} ${UNITS[i]}`
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
export const formatTime = (ms: number) => {
|
|
18
|
+
if (ms < 0.001) return `${(ms * 1_000_000).toFixed(2)}ns`
|
|
19
|
+
if (ms < 1) return `${(ms * 1000).toFixed(2)}µs`
|
|
20
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`
|
|
21
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`
|
|
22
|
+
return `${Math.floor(ms / 60000)}m ${((ms % 60000) / 1000).toFixed(1)}s`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function* scan(
|
|
26
|
+
dir: string | string[],
|
|
27
|
+
ext: string | string[],
|
|
28
|
+
ignore: string[] = ['*.d.*'],
|
|
29
|
+
cwd?: string,
|
|
30
|
+
): AsyncGenerator<[string, string, any]> {
|
|
31
|
+
if (typeof dir !== 'string')
|
|
32
|
+
dir = ext.length === 1 ? dir[0] : `{${dir.join(',')}}`
|
|
33
|
+
|
|
34
|
+
if (typeof ext !== 'string')
|
|
35
|
+
ext = ext.length === 1 ? ext[0] : `{${ext.join(',')}}`
|
|
36
|
+
|
|
37
|
+
cwd ??= _root
|
|
38
|
+
for await (const file of new Bun.Glob(dir +'/**/*.'+ ext).scan({
|
|
39
|
+
cwd,
|
|
40
|
+
absolute: true,
|
|
41
|
+
onlyFiles: true, // @ts-ignore
|
|
42
|
+
ignore,
|
|
43
|
+
})) {
|
|
44
|
+
yield [file, file.split(cwd as string)[1], await IMPORT(file)]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,108 +0,0 @@
|
|
|
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, '&')
|
|
17
|
-
.replace(/"/g, '"')
|
|
18
|
-
.replace(/</g, '<')
|
|
19
|
-
.replace(/>/g, '>')
|
|
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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|