Skip to content

Commit 7f590ba

Browse files
authored
refactor: normalize pages to it's own adex sub-plugin (#20)
* refactor: move pages to it's own plugin * fix: null scenario
1 parent 432cce9 commit 7f590ba

File tree

5 files changed

+132
-106
lines changed

5 files changed

+132
-106
lines changed

adex/runtime/handler.js

+5-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { CONSTANTS, emitToHooked } from 'adex/hook'
22
import { prepareRequest, prepareResponse } from 'adex/http'
3-
import { normalizeRouteImports, renderToString, toStatic } from 'adex/ssr'
3+
import { renderToString, toStatic } from 'adex/ssr'
44
import { h } from 'preact'
55

6-
const apiRoutes = import.meta.glob('/src/api/**/*.{js,ts}')
7-
const pageRoutes = import.meta.glob('/src/pages/**/*.{tsx,jsx,js}')
6+
// @ts-expect-error injected by vite
7+
import { routes as apiRoutes } from '~apiRoutes'
8+
// @ts-expect-error injected by vite
9+
import { routes as pageRoutes } from '~routes'
810

911
const html = String.raw
1012

@@ -14,7 +16,6 @@ export async function handler(req, res) {
1416
prepareRequest(req)
1517
prepareResponse(res)
1618

17-
const { pageRoutes, apiRoutes } = await getRouterMaps()
1819
const baseURL = req.url
1920

2021
const { metas, links, title, lang } = toStatic()
@@ -116,21 +117,6 @@ function HTMLTemplate({
116117
`
117118
}
118119

119-
async function getRouterMaps() {
120-
const apiRouteMap = normalizeRouteImports(apiRoutes, [
121-
/^\/(src\/api)/,
122-
'/api',
123-
])
124-
const pageRouteMap = normalizeRouteImports(pageRoutes, [
125-
/^\/(src\/pages)/,
126-
'',
127-
])
128-
return {
129-
pageRoutes: pageRouteMap,
130-
apiRoutes: apiRouteMap,
131-
}
132-
}
133-
134120
function getRouteParams(baseURL, matchedRoute) {
135121
const matchedParams = baseURL.match(matchedRoute.regex.pattern)
136122
const routeParams = regexToParams(matchedRoute, matchedParams)

adex/runtime/pages.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { pathToRegex } from 'adex/ssr'
2+
3+
const pages = import.meta.glob('#{__PLUGIN_PAGES_ROOT}')
4+
5+
export const routes = normalizeRouteImports(pages, [
6+
new RegExp('#{__PLUGIN_PAGES_ROOT_REGEX}'),
7+
'#{__PLUGIN_PAGES_ROOT_REGEX_REPLACER}',
8+
])
9+
10+
// taken from
11+
// https://github.com/cyco130/smf/blob/c4b601f48cd3b3b71bea6d76b52b9a85800813e4/smf/shared.ts#L22
12+
// as it's decently tested and aligns to what we want for our routing
13+
function compareRoutePatterns(a, b) {
14+
// Non-catch-all routes first: /foo before /$$rest
15+
const catchAll = Number(a.match(/\$\$(\w+)$/)) - Number(b.match(/\$\$(\w+)$/))
16+
if (catchAll) return catchAll
17+
18+
// Split into segments
19+
const aSegments = a.split('/')
20+
const bSegments = b.split('/')
21+
22+
// Routes with fewer dynamic segments first: /foo/bar before /foo/$bar
23+
const dynamicSegments =
24+
aSegments.filter(segment => segment.includes('$')).length -
25+
bSegments.filter(segment => segment.includes('$')).length
26+
if (dynamicSegments) return dynamicSegments
27+
28+
// Routes with fewer segments first: /foo/bar before /foo/bar
29+
const segments = aSegments.length - bSegments.length
30+
if (segments) return segments
31+
32+
// Routes with earlier dynamic segments first: /foo/$bar before /$foo/bar
33+
for (let i = 0; i < aSegments.length; i++) {
34+
const aSegment = aSegments[i]
35+
const bSegment = bSegments[i]
36+
const dynamic =
37+
Number(aSegment.includes('$')) - Number(bSegment.includes('$'))
38+
if (dynamic) return dynamic
39+
40+
// Routes with more dynamic subsegments at this position first: /foo/$a-$b before /foo/$a
41+
const subsegments = aSegment.split('$').length - bSegment.split('$').length
42+
if (subsegments) return subsegments
43+
}
44+
45+
// Equal as far as we can tell
46+
return 0
47+
}
48+
49+
function normalizeRouteImports(imports, baseKeyMatcher) {
50+
return Object.keys(imports)
51+
.sort(compareRoutePatterns)
52+
.map(route => {
53+
const routePath = simplifyPath(route).replace(
54+
baseKeyMatcher[0],
55+
baseKeyMatcher[1]
56+
)
57+
58+
const regex = pathToRegex(routePath)
59+
60+
return {
61+
route,
62+
regex,
63+
routePath,
64+
module: imports[route],
65+
}
66+
})
67+
}
68+
69+
function simplifyPath(path) {
70+
return path
71+
.replace(/(\.(js|ts)x?)/, '')
72+
.replace(/index/, '/')
73+
.replace('//', '/')
74+
.replace(/\$\$/, '*')
75+
.replace(/\$/, ':')
76+
}

adex/src/ssr.d.ts

+1-15
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,3 @@
11
export { toStatic } from 'hoofd/preact'
22
export { renderToString } from 'preact-render-to-string'
3-
4-
export function normalizeRouteImports<Routes>(
5-
obj: Routes,
6-
matcher: [RegExp, string]
7-
): {
8-
route: string
9-
regex: {
10-
pattern: RegExp
11-
keys: string[]
12-
}
13-
routePath: string
14-
module: () => Promise<{
15-
default: () => any
16-
}>
17-
}[]
3+
export { parse as pathToRegex } from 'regexparam'

adex/src/ssr.js

+1-72
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,5 @@
11
export { renderToString } from 'preact-render-to-string'
22
export { default as sirv } from 'sirv'
33
export { default as mri } from 'mri'
4-
import { parse } from 'regexparam'
4+
export { parse as pathToRegex } from 'regexparam'
55
export { toStatic } from 'hoofd/preact'
6-
7-
// taken from
8-
// https://github.com/cyco130/smf/blob/c4b601f48cd3b3b71bea6d76b52b9a85800813e4/smf/shared.ts#L22
9-
// as it's decently tested and aligns to what we want for our routing
10-
export function compareRoutePatterns(a, b) {
11-
// Non-catch-all routes first: /foo before /$$rest
12-
const catchAll = Number(a.match(/\$\$(\w+)$/)) - Number(b.match(/\$\$(\w+)$/))
13-
if (catchAll) return catchAll
14-
15-
// Split into segments
16-
const aSegments = a.split('/')
17-
const bSegments = b.split('/')
18-
19-
// Routes with fewer dynamic segments first: /foo/bar before /foo/$bar
20-
const dynamicSegments =
21-
aSegments.filter(segment => segment.includes('$')).length -
22-
bSegments.filter(segment => segment.includes('$')).length
23-
if (dynamicSegments) return dynamicSegments
24-
25-
// Routes with fewer segments first: /foo/bar before /foo/bar
26-
const segments = aSegments.length - bSegments.length
27-
if (segments) return segments
28-
29-
// Routes with earlier dynamic segments first: /foo/$bar before /$foo/bar
30-
for (let i = 0; i < aSegments.length; i++) {
31-
const aSegment = aSegments[i]
32-
const bSegment = bSegments[i]
33-
const dynamic =
34-
Number(aSegment.includes('$')) - Number(bSegment.includes('$'))
35-
if (dynamic) return dynamic
36-
37-
// Routes with more dynamic subsegments at this position first: /foo/$a-$b before /foo/$a
38-
const subsegments = aSegment.split('$').length - bSegment.split('$').length
39-
if (subsegments) return subsegments
40-
}
41-
42-
// Equal as far as we can tell
43-
return 0
44-
}
45-
46-
export function normalizeRouteImports(imports, baseKeyMatcher) {
47-
return Object.keys(imports)
48-
.sort(compareRoutePatterns)
49-
.map(route => {
50-
const routePath = simplifyPath(route).replace(
51-
baseKeyMatcher[0],
52-
baseKeyMatcher[1]
53-
)
54-
const regex = pathToRegex(routePath)
55-
56-
return {
57-
route,
58-
regex,
59-
routePath,
60-
module: imports[route],
61-
}
62-
})
63-
}
64-
65-
function simplifyPath(path) {
66-
return path
67-
.replace(/(\.(js|ts)x?)/, '')
68-
.replace(/index/, '/')
69-
.replace('//', '/')
70-
.replace(/\$\$/, '*')
71-
.replace(/\$/, ':')
72-
}
73-
74-
function pathToRegex(path) {
75-
return parse(path)
76-
}

adex/src/vite.js

+49
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { addImportToAST, codeFromAST } from '@dumbjs/preland/ast'
1313
import preact from '@preact/preset-vite'
1414
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
15+
import { readFile } from 'fs/promises'
1516
import { dirname, join, resolve } from 'path'
1617
import { fileURLToPath } from 'url'
1718
import { build } from 'vite'
@@ -28,6 +29,15 @@ let runningIslandBuild = false
2829
*/
2930
export function adex({ fonts, islands = false } = {}) {
3031
return [
32+
preactPages({
33+
root: '/src/pages',
34+
id: '~routes',
35+
}),
36+
preactPages({
37+
root: '/src/api',
38+
id: '~apiRoutes',
39+
replacer: '/api',
40+
}),
3141
createUserDefaultVirtualModule(
3242
'virtual:adex:global.css',
3343
'',
@@ -332,6 +342,7 @@ function adexServerBuilder({ islands = false } = {}) {
332342
},
333343
async resolveId(id, importer, meta) {
334344
if (id.endsWith('.css')) {
345+
if (!importer) return
335346
const importerFromRoot = importer.replace(resolve(cfg.root), '')
336347
const resolvedCss = await this.resolve(id, importer, meta)
337348
devCSSMap.set(
@@ -438,3 +449,41 @@ function adexGuards() {
438449
},
439450
]
440451
}
452+
453+
/**
454+
* @returns {import("vite").Plugin}
455+
*/
456+
function preactPages({
457+
root = '/src/pages',
458+
id: virtualId = '~routes',
459+
extensions = ['js', 'ts', 'tsx', 'jsx'],
460+
replacer = '',
461+
} = {}) {
462+
return {
463+
name: 'routes',
464+
enforce: 'pre',
465+
resolveId(id) {
466+
if (id !== virtualId) {
467+
return
468+
}
469+
return `/0${virtualId}`
470+
},
471+
async load(id) {
472+
if (id !== `/0${virtualId}`) {
473+
return
474+
}
475+
476+
const extsString = extensions.join(',')
477+
const code = (
478+
await readFile(join(__dirname, '../runtime/pages.js'), 'utf8')
479+
)
480+
.replaceAll('#{__PLUGIN_PAGES_ROOT}', root + `/**/*.{${extsString}}`)
481+
.replaceAll('#{__PLUGIN_PAGES_ROOT_REGEX}', `^${root}`)
482+
.replaceAll('#{__PLUGIN_PAGES_ROOT_REGEX_REPLACER}', replacer)
483+
484+
return {
485+
code,
486+
}
487+
},
488+
}
489+
}

0 commit comments

Comments
 (0)