duto 0.0.0 → 0.260808.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/LICENSE +21 -0
- package/README.md +21 -12
- package/package.json +56 -2
- package/src/cli/commands/build.ts +74 -0
- package/src/cli/commands/dev-old.ts +213 -0
- package/src/cli/commands/dev.ts +87 -0
- package/src/cli/commands/preview.ts +33 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/utils.ts +146 -0
- package/src/client/app.tsx +123 -0
- package/src/client/hmr.ts +13 -0
- package/src/client/island.ts +136 -0
- package/src/client/link.ts +70 -0
- package/src/client/router.ts +329 -0
- package/src/client/storage.ts +95 -0
- package/src/compiler/aliases.ts +36 -0
- package/src/compiler/assets.ts +43 -0
- package/src/compiler/ast.ts +764 -0
- package/src/compiler/constants.ts +24 -0
- package/src/compiler/file.ts +60 -0
- package/src/compiler/index.ts +307 -0
- package/src/compiler/island.ts +108 -0
- package/src/compiler/meta.ts +601 -0
- package/src/compiler/routes.ts +29 -0
- package/src/compiler/types.ts +209 -0
- package/src/compiler/utils.ts +5 -0
- package/src/compiler/vite-plugin.ts +324 -0
- package/src/components/link.tsx +38 -0
- package/src/i18n/client.ts +403 -0
- package/src/i18n/element.ts +44 -0
- package/src/i18n/server.ts +56 -0
- package/src/index.ts +28 -0
- package/src/preact/client-dev.ts +5 -0
- package/src/preact/client.ts +87 -0
- package/src/preact/context.ts +41 -0
- package/src/preact/server.ts +186 -0
- package/src/preact/signals.ts +89 -0
- package/src/preact/static-html.ts +45 -0
- package/src/preact/types.ts +26 -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
|
+
}
|