duto 0.0.0 → 0.260906.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -12
  3. package/package.json +91 -2
  4. package/src/cli.ts +42 -0
  5. package/src/enum.ts +2 -0
  6. package/src/index.d.ts +61 -0
  7. package/src/index.ts +19 -0
  8. package/src/middleware.ts +2 -0
  9. package/src/pages/client/app.tsx +123 -0
  10. package/src/pages/client/hmr.ts +13 -0
  11. package/src/pages/client/island.ts +136 -0
  12. package/src/pages/client/link.ts +70 -0
  13. package/src/pages/client/router.ts +329 -0
  14. package/src/pages/client/storage.ts +95 -0
  15. package/src/pages/commands/build.ts +74 -0
  16. package/src/pages/commands/dev-old.ts +213 -0
  17. package/src/pages/commands/dev.ts +87 -0
  18. package/src/pages/commands/preview.ts +33 -0
  19. package/src/pages/compiler/aliases.ts +36 -0
  20. package/src/pages/compiler/assets.ts +43 -0
  21. package/src/pages/compiler/ast.ts +764 -0
  22. package/src/pages/compiler/constants.ts +24 -0
  23. package/src/pages/compiler/file.ts +60 -0
  24. package/src/pages/compiler/index.ts +307 -0
  25. package/src/pages/compiler/island.ts +108 -0
  26. package/src/pages/compiler/meta.ts +601 -0
  27. package/src/pages/compiler/routes.ts +29 -0
  28. package/src/pages/compiler/types.ts +209 -0
  29. package/src/pages/compiler/utils.ts +5 -0
  30. package/src/pages/compiler/vite-plugin.ts +324 -0
  31. package/src/pages/components/link.tsx +38 -0
  32. package/src/pages/i18n/client.ts +403 -0
  33. package/src/pages/i18n/element.ts +44 -0
  34. package/src/pages/i18n/server.ts +56 -0
  35. package/src/pages/index.ts +28 -0
  36. package/src/pages/preact/client-dev.ts +5 -0
  37. package/src/pages/preact/client.ts +87 -0
  38. package/src/pages/preact/context.ts +41 -0
  39. package/src/pages/preact/server.ts +186 -0
  40. package/src/pages/preact/signals.ts +89 -0
  41. package/src/pages/preact/static-html.ts +45 -0
  42. package/src/pages/preact/types.ts +26 -0
  43. package/src/server/adapter/aws.ts +4 -0
  44. package/src/server/adapter/workerd.ts +4 -0
  45. package/src/server/app.ts +96 -0
  46. package/src/server/auth/ability.ts +72 -0
  47. package/src/server/auth/authnz.ts +86 -0
  48. package/src/server/auth/index.ts +5 -0
  49. package/src/server/auth/token.ts +147 -0
  50. package/src/server/auth/types.ts +2 -0
  51. package/src/server/cli/commands/build.ts +41 -0
  52. package/src/server/cli/commands/deploy.ts +82 -0
  53. package/src/server/cli/commands/dev.ts +277 -0
  54. package/src/server/cli/commands/make.ts +98 -0
  55. package/src/server/cli/commands/migrate.ts +93 -0
  56. package/src/server/cli/commands/routes.ts +79 -0
  57. package/src/server/cli/index.ts +27 -0
  58. package/src/server/cli/stubs.ts +39 -0
  59. package/src/server/cli/types.ts +3 -0
  60. package/src/server/cli/utils.ts +443 -0
  61. package/src/server/config.ts +4 -0
  62. package/src/server/db/d1.d.ts +1 -0
  63. package/src/server/db/d1.ts +1 -0
  64. package/src/server/db/dynamodb.d.ts +1 -0
  65. package/src/server/db/dynamodb.ts +1 -0
  66. package/src/server/db/index.ts +1 -0
  67. package/src/server/entry.ts +19 -0
  68. package/src/server/handle.ts +30 -0
  69. package/src/server/http.ts +23 -0
  70. package/src/server/middlewares/auth.ts +40 -0
  71. package/src/server/middlewares/logger.ts +30 -0
  72. package/src/server/open-api/register.ts +29 -0
  73. package/src/server/open-api/spec.ts +107 -0
  74. package/src/server/request.ts +233 -0
  75. package/src/server/response.ts +142 -0
  76. package/src/server/routes.ts +474 -0
  77. package/src/server/types.ts +142 -0
  78. package/src/server/utils/environment.ts +17 -0
  79. package/src/server/utils.ts +46 -0
  80. package/src/server/validator.ts +70 -0
  81. package/src/utils.ts +5 -0
@@ -0,0 +1,601 @@
1
+ // https://github.com/cloudflare/vinext/blob/main/packages/vinext/src/routing/utils.ts
2
+
3
+ // https://github.com/cloudflare/vinext/blob/adfc2236cba5f7cde63f32f6fb35c0d892428376/packages/vinext/src/build/prerender.ts#L203
4
+ // https://github.com/cloudflare/vinext/blob/main/packages/vinext/src/entries/pages-client-entry.ts
5
+ // https://github.com/cloudflare/vinext/blob/main/packages/vinext/src/shims/ALS-ARCHITECTURE.md
6
+
7
+ import { dirname, extname, join } from 'node:path'
8
+ import { readdirSync } from 'node:fs'
9
+
10
+ import { IMPORT, JSJSON, newID } from 't0n'
11
+
12
+ import { getAliases } from './aliases'
13
+ import { EXT_RE, DYNAMIC_RE, INTERNAL_RE, LAYOUT_RE, NF_NAMES } from './constants'
14
+
15
+ import { USE_CLIENT_RE } from './constants'
16
+ import { parseSync } from '@swc/core'
17
+ import { findUsage, getFileMap } from './ast'
18
+ import { replaceIslands } from './island'
19
+ import { mkdir } from 'node:fs/promises'
20
+
21
+
22
+ import getI18n from '../i18n/server'
23
+ import i18n from '../i18n/client'
24
+
25
+ import type {
26
+ PageModule,
27
+ IRoute, RouteParam, RouteParams,
28
+ Metadata,
29
+ ImportUsage,
30
+ SummaryMetadata,
31
+ } from './types'
32
+
33
+ export function getAssets(meta: Metadata): Set<string> {
34
+ const deps = new Set<string>()
35
+ const queue: string[] = []
36
+ const dir = join(meta.root, meta.dir)
37
+
38
+ for (const [path] of meta.map) {
39
+ if (path.startsWith(dir))
40
+ queue.push(path)
41
+ }
42
+
43
+ while (queue.length > 0) {
44
+ const path = queue.pop()!
45
+ const imports = meta.map.get(path)!.deps
46
+ if (!imports?.size) continue
47
+
48
+ for (const [dep] of imports) {
49
+ if (!dep.startsWith(meta.root) || dep.startsWith(dir) || deps.has(dep)) continue
50
+ deps.add(dep)
51
+ if (dep in meta.map) queue.push(dep)
52
+ }
53
+ }
54
+
55
+ return deps
56
+ }
57
+
58
+
59
+ export async function parsePage(page: IRoute, meta: Metadata) {
60
+ const fileMeta = meta.map.get(page.fullFilePath)!
61
+ if (!meta.violations.has(page.fullFilePath)) {
62
+ return page
63
+ }
64
+
65
+ const islands: ImportUsage[] = []
66
+ const ast = parseSync(fileMeta.content, { syntax: 'typescript', tsx: true })
67
+ for (const [depPath, dep] of fileMeta.deps) {
68
+ // console.error(depPath, page.fullFilePath)
69
+ if (!meta.violations.has(depPath)) continue
70
+
71
+ const usages = findUsage(fileMeta.content, dep.ref, ast)
72
+ // console.log(usages, dep.ref)
73
+ islands.push(...usages)
74
+ // for (const usage of usages)
75
+ // islands.push(...usage.usages)
76
+
77
+
78
+ // console.log(id)
79
+ // console.log(usage)
80
+ // console.log(usage[0].usages)
81
+ }
82
+
83
+ // // Limpar linhas vazias extras
84
+ // result = result.replace(/^\s*\n/gm, '')
85
+
86
+ const content = await replaceIslands(fileMeta.content, islands)
87
+ // @ts-ignore
88
+ let mod: PageModule = {}
89
+ const tmpPath = join(meta.root, 'tmp', page.filePath)
90
+ try {
91
+ await mkdir(dirname(tmpPath), {recursive: true})
92
+ await Bun.write(tmpPath, content)
93
+ mod = await IMPORT(tmpPath)
94
+ } catch {}
95
+
96
+ return {
97
+ safeFilePath: tmpPath,
98
+ mod, content, islands,
99
+ hasGetStaticProps: typeof mod.getStaticProps === 'function',
100
+ hasGetProps: typeof mod.getProps === 'function',
101
+ isClientOnly: USE_CLIENT_RE.test(content),
102
+ }
103
+ }
104
+
105
+
106
+ // TODO: https://github.com/cloudflare/vinext/blob/adfc2236cba5f7cde63f32f6fb35c0d892428376/packages/vinext/src/routing/file-matcher.ts#L90
107
+ export async function getPages(meta: Metadata) {
108
+ const files = readdirSync(join(meta.root, meta.dir), {recursive: true})
109
+ const nf = new Map<string, IRoute>()
110
+ const layouts = new Map<string, IRoute>()
111
+ const pages = new Map<string, IRoute>()
112
+ const spages = new Set<string>()
113
+ const routes: IRoute[] = []
114
+
115
+ for (const file of files) {
116
+ if (typeof file !== 'string' || !EXT_RE.test(file)) continue
117
+
118
+ const ext = extname(file)
119
+ const fullFilePath = join(meta.root, meta.dir, file)
120
+ const { pattern, params } = filePathToWouterPattern(file)
121
+
122
+ const name = file.replace(EXT_RE, '').split('/').pop() as string
123
+
124
+ const fileMeta = meta.map.get(fullFilePath)!
125
+ const mod = (await IMPORT(fullFilePath)) as PageModule
126
+
127
+ // console.error(file, INTERNAL_RE.test(file))
128
+ const page = {
129
+ fileExt: ext.substring(1),
130
+ fileName: file.replace(ext, ''),
131
+ filePath: file,
132
+ fullFilePath,
133
+ pattern,
134
+ params,
135
+ is404: name ? NF_NAMES.includes(name) : false,
136
+ isDynamic: DYNAMIC_RE.test(file),
137
+ isInternal: INTERNAL_RE.test(file),
138
+
139
+ importName: newID(),
140
+
141
+ mod,
142
+ content: fileMeta.content,
143
+ islands: [],
144
+ layouts: [],
145
+ hasGetStaticProps: typeof mod.getStaticProps === 'function',
146
+ hasGetProps: typeof mod.getProps === 'function',
147
+ isClientOnly: USE_CLIENT_RE.test(fileMeta.content),
148
+ }
149
+
150
+ if (LAYOUT_RE.test(file)) {
151
+ layouts.set(page.fileName, page)
152
+ continue
153
+ }
154
+
155
+ if (page.is404) {
156
+ nf.set(page.fileName, page)
157
+ routes.push(page)
158
+ pages.set(fullFilePath, page)
159
+ continue
160
+ }
161
+
162
+ if (!page.isInternal)
163
+ pages.set(fullFilePath, page)
164
+
165
+ if (!page.isInternal && page.isDynamic && page.hasGetProps)
166
+ routes.push(page)
167
+
168
+ if (!page.isInternal) {
169
+ if (page.isDynamic && page.hasGetStaticProps) {
170
+ try {
171
+ const _props = await page.mod.getStaticProps?.()
172
+ const props = Array.isArray(_props) ? _props : [_props]
173
+ for (const prop of props) {
174
+ // console.log(prop, file)
175
+ spages.add('/'+resolveStaticOutputPath(page, prop as any))
176
+ }
177
+ // spages.add('/'+resolveStaticOutputPath(page, prop as any))
178
+ } catch(e) {
179
+ console.log('errro', e)
180
+ }
181
+ } else if (!page.isDynamic) {
182
+ spages.add(page.pattern)
183
+ }
184
+ }
185
+
186
+ }
187
+
188
+ meta.nf = nf
189
+ meta.layouts = layouts
190
+ meta.pages = pages
191
+ meta.spages = spages
192
+ meta.routes = await sortRoutes(meta, routes)
193
+ }
194
+
195
+ /**
196
+ * A partir do path de um arquivo relativo à raiz de rotas do Next.js
197
+ * (app/ ou pages/), retorna:
198
+ *
199
+ * - layouts: arquivos "layout.{tsx,ts,jsx,js}" aplicáveis à rota, ordenados
200
+ * do mais próximo do arquivo (segmento irmão) ao mais distante (raiz).
201
+ * Ex.: ["users/layout.tsx", "layout.tsx"]
202
+ * - document: o "_document.{tsx,ts,jsx,js}" do Pages Router, se existir na
203
+ * raiz das rotas. Só é checado uma vez no topo, nunca por segmento — é
204
+ * um arquivo único, diferente do layout, que pode ser aninhado.
205
+ *
206
+ * Compatibilidade com a especificação oficial do Next.js:
207
+ * - App Router: nenhum arquivo especial usa prefixo "_" — é "layout", não
208
+ * "_layout". layout.js é o componente mais externo de um route segment e
209
+ * envolve template.js, error.js, loading.js, not-found.js e page.js.
210
+ * - Pages Router: pages/_document.js (com "_") customiza as tags <html> e
211
+ * <body> e existe só uma vez, na raiz de pages/. No App Router esse papel
212
+ * foi assumido pelo root layout.js — não há "_document" equivalente lá.
213
+ */
214
+ // export async function getLayouts(meta: Metadata, path: string) {
215
+ // const isDir = existsSync(path) && statSync(path).isDirectory()
216
+ // const fileDir = isDir ? path : dirname(path)
217
+
218
+ // const layouts: Page[] = []
219
+ // let currentDir = fileDir
220
+
221
+ // while (true) {
222
+ // const layout = findFileByName(meta, currentDir, 'layout')
223
+ // if (layout && meta.pages.has(layout)) {
224
+ // // const mod = meta.pages.get(layout)?.mod
225
+ // const { mod, content } = await parsePage(meta.pages.get(layout)!, meta)
226
+ // if (mod?.default) layouts.push(mod.default)
227
+ // }
228
+
229
+ // const parentDir = dirname(currentDir)
230
+ // if (parentDir === currentDir) break
231
+
232
+ // currentDir = parentDir
233
+ // }
234
+
235
+ // return layouts
236
+ // }
237
+
238
+ export async function getLayouts(meta: Metadata, path: string) {
239
+ const layouts: IRoute[] = []
240
+ const segments = dirname(path).split(/[\\/]/).filter(Boolean)
241
+
242
+ for (let i = segments.length; i >= 0; i--) {
243
+ const dir = segments.slice(0, i).join('/')
244
+
245
+ const page = meta.layouts.get(dir ? dir +'/_layout' : '_layout') || meta.layouts.get(dir ? dir +'/layout' : 'layout')
246
+ if (!page) continue
247
+
248
+ const { mod } = await parsePage(page, meta) // @ts-ignore
249
+ if (mod?.default) {
250
+ page.mod = mod
251
+ layouts.push(page)
252
+ }
253
+
254
+ }
255
+
256
+ return layouts
257
+ }
258
+
259
+ export async function getMetadata(root: string, dir: string) {
260
+ const meta = { root, dir } as Metadata
261
+ meta.summary = new Map<string, SummaryMetadata>()
262
+
263
+ meta.i18n = await getI18n(meta)
264
+ i18n(meta.i18n.fallbackLocale, meta.i18n.locales, meta.i18n.messages, meta.i18n.routes, meta.i18n.routeMap)
265
+
266
+ meta.aliases = getAliases(root)
267
+ const fileMap = await getFileMap(meta)
268
+ meta.map = fileMap.map
269
+ meta.violations = fileMap.violations
270
+ meta.assets = Array.from(getAssets(meta))
271
+
272
+ await getPages(meta)
273
+
274
+ meta.string = `// AUTO-GENERATED FILE - DO NOT EDIT
275
+ ${meta.routes.map((page, index) => `\nimport Page${index + (page.hasGetProps ? `, { getProps as getProps${index} }` : '')} from '${page.fullFilePath}'`).join('\n')}
276
+ ${Array.from(meta.layouts.values()).map(layout => `\nimport Layout${layout.importName} from '${layout.fullFilePath}'`).join('\n')}
277
+
278
+ export const views = [${meta.routes.map((page, index) => `['${page.pattern}',Page${index},${JSJSON(Object.entries(page.params).map(([_,v]) => v))}${page.hasGetProps ? `,getProps${index}` : (page.layouts.length ? ',0': '')}${page.layouts.length ? ',[' + Object.entries(page.layouts).map(([_,v]) => 'Layout'+v.importName) +']' : ''}]`).join(',')}]
279
+
280
+ export const SRoutes = new Set(${JSJSON([...meta.spages])})
281
+
282
+
283
+ ` /// + meta.i18n.string
284
+
285
+ // console.error(meta.string)
286
+ // console.log(meta.map.get(`/Users/nicolau/Projects/preact-skeleton/pages/index.tsx`))
287
+
288
+ return meta
289
+ }
290
+
291
+
292
+
293
+
294
+
295
+
296
+
297
+ // //////////////////////////////////////////////////////////////////////////////////////////
298
+ type RouteMeta = {
299
+ staticSegments: number,
300
+ dynamicSegments: number,
301
+ optionalSegments: number,
302
+ catchAll: boolean,
303
+ globalCatchAll: boolean,
304
+ segmentsCount: number,
305
+ }
306
+ export async function sortRoutes(meta: Metadata, routes: IRoute[]) {
307
+ const metas = new Map<IRoute, RouteMeta>()
308
+
309
+ for (const route of routes) {
310
+ route.layouts = await getLayouts(meta, route.fileName)
311
+ metas.set(route, getRouteMeta(route.pattern))
312
+ }
313
+
314
+ // for (const route of routes) // TODO: talvez inserir o layout nesse loop
315
+ // metas.set(route, getRouteMeta(route.pattern))
316
+
317
+ return routes.sort((a, b) => {
318
+ const ma = metas.get(a)!
319
+ const mb = metas.get(b)!
320
+
321
+ // 404 global always last
322
+ if (ma.globalCatchAll !== mb.globalCatchAll)
323
+ return ma.globalCatchAll ? 1 : -1
324
+
325
+ // catch-all at the end
326
+ if (ma.catchAll !== mb.catchAll)
327
+ return ma.catchAll ? 1 : -1
328
+
329
+ // more static segments first
330
+ if (ma.staticSegments !== mb.staticSegments)
331
+ return mb.staticSegments - ma.staticSegments
332
+
333
+ // fewer parameters first
334
+ if (ma.dynamicSegments !== mb.dynamicSegments)
335
+ return ma.dynamicSegments - mb.dynamicSegments
336
+
337
+ // mandatory before optional ones
338
+ if (ma.optionalSegments !== mb.optionalSegments)
339
+ return ma.optionalSegments - mb.optionalSegments
340
+
341
+ // deepest first
342
+ if (ma.segmentsCount !== mb.segmentsCount)
343
+ return mb.segmentsCount - ma.segmentsCount
344
+
345
+ return a.pattern.localeCompare(b.pattern)
346
+ })
347
+ }
348
+
349
+ function getRouteMeta(pattern: string): RouteMeta {
350
+ const segments = pattern.split('/').filter(Boolean)
351
+
352
+ let staticSegments = 0
353
+ let dynamicSegments = 0
354
+ let optionalSegments = 0
355
+ let catchAll = false
356
+
357
+ for (const segment of segments) {
358
+ if (segment === '*' || segment === '*?') {
359
+ catchAll = true
360
+ continue
361
+ }
362
+
363
+ if (segment.startsWith(':')) {
364
+ if (segment.endsWith('?'))
365
+ optionalSegments++
366
+ else
367
+ dynamicSegments++
368
+
369
+ continue
370
+ }
371
+
372
+ staticSegments++
373
+ }
374
+
375
+ return {
376
+ staticSegments,
377
+ dynamicSegments,
378
+ optionalSegments,
379
+ catchAll,
380
+ globalCatchAll: pattern === '/*',
381
+ segmentsCount: segments.length,
382
+ }
383
+ }
384
+ // //////////////////////////////////////////////////////////////////////////////////////////
385
+
386
+ // export function sortRoutes(pages: Map<string, IRoute>) {
387
+ // const metas = new Map<string, { score: number, segmentsCount: number }>()
388
+
389
+ // for (const [route, page] of pages)
390
+ // metas.set(route, computeRouteMeta(page.pattern))
391
+
392
+ // const list = Array.from(pages.values()).sort((a, b) => {
393
+ // const metaA = metas.get(a.fullFilePath)!
394
+ // const metaB = metas.get(b.fullFilePath)!
395
+
396
+ // if (metaA.score === metaB.score)
397
+ // return metaB.segmentsCount - metaA.segmentsCount
398
+
399
+ // return metaB.score - metaA.score
400
+ // })
401
+
402
+ // while (list.length && list.at(-1)?.pattern === '/') {
403
+ // const last = list.pop()
404
+ // last && list.unshift(last)
405
+ // }
406
+
407
+ // return list
408
+ // }
409
+
410
+ // function computeRouteMeta(path: string) {
411
+ // const segments = path.split('/').filter(Boolean)
412
+
413
+ // let score = 0
414
+ // for (const segment of segments) {
415
+ // if (segment === '*') {
416
+ // continue
417
+ // } else if (segment.startsWith(':')) {
418
+ // score += 1
419
+ // } else {
420
+ // score += 10
421
+ // }
422
+ // }
423
+
424
+ // return { score, segmentsCount: segments.length }
425
+ // }
426
+
427
+ // //////////////////////////////////////////////////////////////////////////////////////////
428
+ const CATCHALL_NAMES = new Set(['404', 'not-found'])
429
+
430
+ export function filePathToWouterPattern(filePath: string): {
431
+ pattern: string
432
+ params: Record<string, RouteParam>
433
+ } {
434
+ const withoutExt = filePath.replace(EXT_RE, '')
435
+
436
+ const normalized =
437
+ withoutExt === 'index'
438
+ ? ''
439
+ : withoutExt.endsWith('/index')
440
+ ? withoutExt.slice(0, -6)
441
+ : withoutExt
442
+
443
+ const segments = normalized ? normalized.split('/') : []
444
+
445
+ const params: Record<string, RouteParam> = {}
446
+ const patternParts: string[] = []
447
+
448
+ for (let i = 0; i < segments.length; i++) {
449
+ const segment = segments[i]
450
+ const isLast = i === segments.length - 1
451
+
452
+ // 404.tsx / not-found.tsx
453
+ if (isLast && CATCHALL_NAMES.has(segment)) {
454
+ params['*'] = {
455
+ name: '*',
456
+ optional: false,
457
+ catchAll: true,
458
+ }
459
+
460
+ patternParts.push('*')
461
+ continue
462
+ }
463
+
464
+ // [[...slug]]
465
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(\w+)\]\]$/)
466
+
467
+ if (optionalCatchAll) {
468
+ const name = optionalCatchAll[1]
469
+
470
+ params[name] = {
471
+ name,
472
+ optional: true,
473
+ catchAll: true,
474
+ }
475
+
476
+ patternParts.push('*?')
477
+ continue
478
+ }
479
+
480
+ // [...slug]
481
+ const catchAll = segment.match(/^\[\.\.\.(\w+)\]$/)
482
+
483
+ if (catchAll) {
484
+ const name = catchAll[1]
485
+
486
+ params[name] = {
487
+ name,
488
+ optional: false,
489
+ catchAll: true,
490
+ }
491
+
492
+ patternParts.push('*')
493
+ continue
494
+ }
495
+
496
+ // [[slug]]
497
+ const optionalParam = segment.match(/^\[\[(\w+)\]\]$/)
498
+
499
+ if (optionalParam) {
500
+ const name = optionalParam[1]
501
+
502
+ params[name] = {
503
+ name,
504
+ optional: true,
505
+ catchAll: false,
506
+ }
507
+
508
+ patternParts.push(`:${name}?`)
509
+ continue
510
+ }
511
+
512
+ // [slug]
513
+ const param = segment.match(/^\[(\w+)\]$/)
514
+
515
+ if (param) {
516
+ const name = param[1]
517
+
518
+ params[name] = {
519
+ name,
520
+ optional: false,
521
+ catchAll: false,
522
+ }
523
+
524
+ patternParts.push(`:${name}`)
525
+ continue
526
+ }
527
+
528
+ patternParts.push(segment)
529
+ }
530
+
531
+ const pattern =
532
+ patternParts.length === 0
533
+ ? '/'
534
+ : `/${patternParts.join('/')}`
535
+
536
+ return { pattern, params }
537
+ }
538
+ // //////////////////////////////////////////////////////////////////////////////////////////
539
+
540
+
541
+ /**
542
+ * Given a static props entry (returned by getStaticProps),
543
+ * compute the output HTML path for that route + params.
544
+ *
545
+ * e.g. filePath=`blog/[slug].tsx`, params={slug:'hello'} → `blog/hello.html`
546
+ * e.g. filePath=`shop/[...slug].tsx`, params={slug:['a','b']} → `shop/a/b.html`
547
+ * e.g. filePath=`index.tsx`, params={} → `index.html`
548
+ */
549
+ export function resolveStaticOutputPath(
550
+ page: IRoute,
551
+ params: RouteParams
552
+ ) {
553
+ let path = page.fileName
554
+
555
+ if (page.isDynamic) {
556
+ const resolved: string[] = []
557
+ const segments = path.split('/')
558
+ for (const segment of segments) {
559
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(\w+)\]\]$/)
560
+ if (optionalCatchAll) {
561
+ const val = params[optionalCatchAll[1]]
562
+ if (val === undefined) continue
563
+ const parts = Array.isArray(val) ? val : [val]
564
+ resolved.push(...parts)
565
+ continue
566
+ }
567
+
568
+ const catchAll = segment.match(/^\[\.\.\.(\w+)\]$/)
569
+ if (catchAll) {
570
+ const val = params[catchAll[1]]
571
+ const parts = Array.isArray(val) ? val : val ? [val] : []
572
+ resolved.push(...parts)
573
+ continue
574
+ }
575
+
576
+ const optionalParam = segment.match(/^\[\[(\w+)\]\]$/)
577
+ if (optionalParam) {
578
+ const val = params[optionalParam[1]]
579
+ if (val !== undefined) resolved.push(String(val))
580
+ continue
581
+ }
582
+
583
+ const param = segment.match(/^\[(\w+)\]$/)
584
+ if (param) {
585
+ const val = params[param[1]]
586
+ if (val !== undefined) resolved.push(String(val))
587
+ continue
588
+ }
589
+
590
+ resolved.push(segment)
591
+ }
592
+
593
+ path = resolved.join('/')
594
+ }
595
+
596
+ if (path === 'index' || path === '') return 'index'
597
+ if (path === '404') return '404'
598
+
599
+ if (path.endsWith('/index')) path = path.replace('/index', '')
600
+ return path
601
+ }
@@ -0,0 +1,29 @@
1
+ import { join } from 'pathe'
2
+ import { _root } from './utils'
3
+ import type { IRoute, Metadata } from './types'
4
+ import { dirname } from 'node:path'
5
+ import { NF_NAMES } from './constants'
6
+
7
+ export const router = new Bun.FileSystemRouter({
8
+ style: 'nextjs',
9
+ dir: join(_root, 'pages'),
10
+ // origin: 'https://mydomain.com',
11
+ // assetPrefix: 'assets/',
12
+ })
13
+ export const reload = () => router.reload()
14
+ export const match = (input: string | Request | Response) => router.match(input)
15
+
16
+ export const match404 = (meta: Metadata, path: string) => {
17
+ const segments = dirname(path).split(/[\\/]/).filter(Boolean)
18
+
19
+ for (let i = segments.length; i >= 0; i--) {
20
+ const dir = segments.slice(0, i).join('/')
21
+
22
+ for (const item of NF_NAMES) {
23
+ const page = meta.nf.get((dir ? dir+'/' : '') + item)
24
+ if (page) return '/'+ page.fileName
25
+ }
26
+ }
27
+
28
+ return ''
29
+ }