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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/commands/new.ts +55 -30
  4. package/src/commands/templates/pages/README.md +21 -0
  5. package/src/commands/templates/pages/_gitignore +24 -0
  6. package/src/commands/templates/pages/components/locale-select.tsx +59 -0
  7. package/src/commands/templates/pages/components/select.tsx +95 -0
  8. package/src/commands/templates/pages/i18n/en.ts +17 -0
  9. package/src/commands/templates/pages/i18n/index.ts +19 -0
  10. package/src/commands/templates/pages/i18n/pt-br.ts +17 -0
  11. package/src/commands/templates/pages/package.json +22 -0
  12. package/src/commands/templates/pages/pages/404.tsx +12 -0
  13. package/src/commands/templates/pages/pages/index.tsx +16 -0
  14. package/src/commands/templates/pages/pages/layout.tsx +24 -0
  15. package/src/commands/templates/pages/style.css +24 -0
  16. package/src/commands/templates/pages/tsconfig.json +54 -0
  17. package/src/commands/templates/pages/types.ts +8 -0
  18. package/src/commands/templates/server/base/.editorconfig +21 -0
  19. package/src/commands/templates/{base → server/base}/README.md +1 -1
  20. package/src/pages/cli/commands/dev.ts +10 -4
  21. package/src/pages/cli/utils.ts +3 -22
  22. package/src/pages/client/app.tsx +26 -73
  23. package/src/pages/client/router.ts +14 -280
  24. package/src/pages/compiler/aliases.ts +2 -8
  25. package/src/pages/compiler/ast.ts +1 -538
  26. package/src/pages/compiler/constants.ts +95 -13
  27. package/src/pages/compiler/index.ts +20 -126
  28. package/src/pages/compiler/meta.ts +38 -227
  29. package/src/pages/compiler/routes.ts +4 -5
  30. package/src/pages/compiler/types.ts +1 -101
  31. package/src/pages/compiler/vite-plugin.ts +73 -27
  32. package/src/pages/i18n/client.ts +18 -6
  33. package/src/pages/i18n/server.ts +21 -6
  34. package/src/pages/index.ts +1 -2
  35. package/src/server/routes.ts +4 -26
  36. package/src/utils.ts +34 -3
  37. package/src/pages/compiler/island.ts +0 -108
  38. /package/src/commands/templates/{base → pages}/.editorconfig +0 -0
  39. /package/src/commands/templates/{base → server/base}/.env.dev.example +0 -0
  40. /package/src/commands/templates/{base → server/base}/.env.prod.example +0 -0
  41. /package/src/commands/templates/{base → server/base}/_gitignore +0 -0
  42. /package/src/commands/templates/{base → server/base}/actions/blog/index.get.ts +0 -0
  43. /package/src/commands/templates/{base → server/base}/actions/blog/post/[id].get.ts +0 -0
  44. /package/src/commands/templates/{base → server/base}/actions/index.get.ts +0 -0
  45. /package/src/commands/templates/{base → server/base}/configs/duto.ts +0 -0
  46. /package/src/commands/templates/{base → server/base}/configs/roles.ts +0 -0
  47. /package/src/commands/templates/{base → server/base}/package.json +0 -0
  48. /package/src/commands/templates/{base → server/base}/tsconfig.json +0 -0
  49. /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, // process.env.npm_config_local_prefix || process.env.PWD || process.cwd() || __dirname,
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(conf) {
165
- config = conf
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 += 'd-b,d-p,i-d,i-s,i-i{display:contents}'
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
@@ -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
- export default function i18n(
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 => {
@@ -11,7 +11,7 @@ export default async function getI18n(meta: Metadata) {
11
11
  'i18n.ts',
12
12
  'i18n/index.ts',
13
13
  ]) {
14
- const path = join(meta.root, variant)
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', 'messages', 'routes', 'options'].some(i => !mod[i]))
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(mod.routes)
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(mod.routes)
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(mod.messages)}
48
- export const routes = ${JSJSON(mod.routes)}
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)}
@@ -21,8 +21,7 @@ export const navigate = <S = any>(
21
21
  return
22
22
  }
23
23
 
24
- const n = window?.$duto?.navigate || useLocation()[1]
25
- n(to, options)
24
+ window?.$duto?.navigate || useLocation()[1](to, options)
26
25
  }
27
26
 
28
27
  export { Link } from './components/link'
@@ -1,4 +1,4 @@
1
- import { Glob, file, write } from 'bun'
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 { getEnv } from 't0n'
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 = getEnv('npm_config_local_prefix') || getEnv('PWD') || join(_duto, '../../')
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, '&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
- }