diff --git a/adex/runtime/handler.js b/adex/runtime/handler.js index cd491c5..e24f739 100644 --- a/adex/runtime/handler.js +++ b/adex/runtime/handler.js @@ -1,10 +1,12 @@ import { CONSTANTS, emitToHooked } from 'adex/hook' import { prepareRequest, prepareResponse } from 'adex/http' -import { normalizeRouteImports, renderToString, toStatic } from 'adex/ssr' +import { renderToString, toStatic } from 'adex/ssr' import { h } from 'preact' -const apiRoutes = import.meta.glob('/src/api/**/*.{js,ts}') -const pageRoutes = import.meta.glob('/src/pages/**/*.{tsx,jsx,js}') +// @ts-expect-error injected by vite +import { routes as apiRoutes } from '~apiRoutes' +// @ts-expect-error injected by vite +import { routes as pageRoutes } from '~routes' const html = String.raw @@ -14,7 +16,6 @@ export async function handler(req, res) { prepareRequest(req) prepareResponse(res) - const { pageRoutes, apiRoutes } = await getRouterMaps() const baseURL = req.url const { metas, links, title, lang } = toStatic() @@ -116,21 +117,6 @@ function HTMLTemplate({ ` } -async function getRouterMaps() { - const apiRouteMap = normalizeRouteImports(apiRoutes, [ - /^\/(src\/api)/, - '/api', - ]) - const pageRouteMap = normalizeRouteImports(pageRoutes, [ - /^\/(src\/pages)/, - '', - ]) - return { - pageRoutes: pageRouteMap, - apiRoutes: apiRouteMap, - } -} - function getRouteParams(baseURL, matchedRoute) { const matchedParams = baseURL.match(matchedRoute.regex.pattern) const routeParams = regexToParams(matchedRoute, matchedParams) diff --git a/adex/runtime/pages.js b/adex/runtime/pages.js new file mode 100644 index 0000000..4505c17 --- /dev/null +++ b/adex/runtime/pages.js @@ -0,0 +1,76 @@ +import { pathToRegex } from 'adex/ssr' + +const pages = import.meta.glob('#{__PLUGIN_PAGES_ROOT}') + +export const routes = normalizeRouteImports(pages, [ + new RegExp('#{__PLUGIN_PAGES_ROOT_REGEX}'), + '#{__PLUGIN_PAGES_ROOT_REGEX_REPLACER}', +]) + +// taken from +// https://github.com/cyco130/smf/blob/c4b601f48cd3b3b71bea6d76b52b9a85800813e4/smf/shared.ts#L22 +// as it's decently tested and aligns to what we want for our routing +function compareRoutePatterns(a, b) { + // Non-catch-all routes first: /foo before /$$rest + const catchAll = Number(a.match(/\$\$(\w+)$/)) - Number(b.match(/\$\$(\w+)$/)) + if (catchAll) return catchAll + + // Split into segments + const aSegments = a.split('/') + const bSegments = b.split('/') + + // Routes with fewer dynamic segments first: /foo/bar before /foo/$bar + const dynamicSegments = + aSegments.filter(segment => segment.includes('$')).length - + bSegments.filter(segment => segment.includes('$')).length + if (dynamicSegments) return dynamicSegments + + // Routes with fewer segments first: /foo/bar before /foo/bar + const segments = aSegments.length - bSegments.length + if (segments) return segments + + // Routes with earlier dynamic segments first: /foo/$bar before /$foo/bar + for (let i = 0; i < aSegments.length; i++) { + const aSegment = aSegments[i] + const bSegment = bSegments[i] + const dynamic = + Number(aSegment.includes('$')) - Number(bSegment.includes('$')) + if (dynamic) return dynamic + + // Routes with more dynamic subsegments at this position first: /foo/$a-$b before /foo/$a + const subsegments = aSegment.split('$').length - bSegment.split('$').length + if (subsegments) return subsegments + } + + // Equal as far as we can tell + return 0 +} + +function normalizeRouteImports(imports, baseKeyMatcher) { + return Object.keys(imports) + .sort(compareRoutePatterns) + .map(route => { + const routePath = simplifyPath(route).replace( + baseKeyMatcher[0], + baseKeyMatcher[1] + ) + + const regex = pathToRegex(routePath) + + return { + route, + regex, + routePath, + module: imports[route], + } + }) +} + +function simplifyPath(path) { + return path + .replace(/(\.(js|ts)x?)/, '') + .replace(/index/, '/') + .replace('//', '/') + .replace(/\$\$/, '*') + .replace(/\$/, ':') +} diff --git a/adex/src/ssr.d.ts b/adex/src/ssr.d.ts index 5f22bbc..a8ec8f3 100644 --- a/adex/src/ssr.d.ts +++ b/adex/src/ssr.d.ts @@ -1,17 +1,3 @@ export { toStatic } from 'hoofd/preact' export { renderToString } from 'preact-render-to-string' - -export function normalizeRouteImports( - obj: Routes, - matcher: [RegExp, string] -): { - route: string - regex: { - pattern: RegExp - keys: string[] - } - routePath: string - module: () => Promise<{ - default: () => any - }> -}[] +export { parse as pathToRegex } from 'regexparam' diff --git a/adex/src/ssr.js b/adex/src/ssr.js index a9cf57d..b00b5c9 100644 --- a/adex/src/ssr.js +++ b/adex/src/ssr.js @@ -1,76 +1,5 @@ export { renderToString } from 'preact-render-to-string' export { default as sirv } from 'sirv' export { default as mri } from 'mri' -import { parse } from 'regexparam' +export { parse as pathToRegex } from 'regexparam' export { toStatic } from 'hoofd/preact' - -// taken from -// https://github.com/cyco130/smf/blob/c4b601f48cd3b3b71bea6d76b52b9a85800813e4/smf/shared.ts#L22 -// as it's decently tested and aligns to what we want for our routing -export function compareRoutePatterns(a, b) { - // Non-catch-all routes first: /foo before /$$rest - const catchAll = Number(a.match(/\$\$(\w+)$/)) - Number(b.match(/\$\$(\w+)$/)) - if (catchAll) return catchAll - - // Split into segments - const aSegments = a.split('/') - const bSegments = b.split('/') - - // Routes with fewer dynamic segments first: /foo/bar before /foo/$bar - const dynamicSegments = - aSegments.filter(segment => segment.includes('$')).length - - bSegments.filter(segment => segment.includes('$')).length - if (dynamicSegments) return dynamicSegments - - // Routes with fewer segments first: /foo/bar before /foo/bar - const segments = aSegments.length - bSegments.length - if (segments) return segments - - // Routes with earlier dynamic segments first: /foo/$bar before /$foo/bar - for (let i = 0; i < aSegments.length; i++) { - const aSegment = aSegments[i] - const bSegment = bSegments[i] - const dynamic = - Number(aSegment.includes('$')) - Number(bSegment.includes('$')) - if (dynamic) return dynamic - - // Routes with more dynamic subsegments at this position first: /foo/$a-$b before /foo/$a - const subsegments = aSegment.split('$').length - bSegment.split('$').length - if (subsegments) return subsegments - } - - // Equal as far as we can tell - return 0 -} - -export function normalizeRouteImports(imports, baseKeyMatcher) { - return Object.keys(imports) - .sort(compareRoutePatterns) - .map(route => { - const routePath = simplifyPath(route).replace( - baseKeyMatcher[0], - baseKeyMatcher[1] - ) - const regex = pathToRegex(routePath) - - return { - route, - regex, - routePath, - module: imports[route], - } - }) -} - -function simplifyPath(path) { - return path - .replace(/(\.(js|ts)x?)/, '') - .replace(/index/, '/') - .replace('//', '/') - .replace(/\$\$/, '*') - .replace(/\$/, ':') -} - -function pathToRegex(path) { - return parse(path) -} diff --git a/adex/src/vite.js b/adex/src/vite.js index d341955..6324413 100644 --- a/adex/src/vite.js +++ b/adex/src/vite.js @@ -12,6 +12,7 @@ import { import { addImportToAST, codeFromAST } from '@dumbjs/preland/ast' import preact from '@preact/preset-vite' import { mkdirSync, readFileSync, writeFileSync } from 'fs' +import { readFile } from 'fs/promises' import { dirname, join, resolve } from 'path' import { fileURLToPath } from 'url' import { build } from 'vite' @@ -28,6 +29,15 @@ let runningIslandBuild = false */ export function adex({ fonts, islands = false } = {}) { return [ + preactPages({ + root: '/src/pages', + id: '~routes', + }), + preactPages({ + root: '/src/api', + id: '~apiRoutes', + replacer: '/api', + }), createUserDefaultVirtualModule( 'virtual:adex:global.css', '', @@ -332,6 +342,7 @@ function adexServerBuilder({ islands = false } = {}) { }, async resolveId(id, importer, meta) { if (id.endsWith('.css')) { + if (!importer) return const importerFromRoot = importer.replace(resolve(cfg.root), '') const resolvedCss = await this.resolve(id, importer, meta) devCSSMap.set( @@ -438,3 +449,41 @@ function adexGuards() { }, ] } + +/** + * @returns {import("vite").Plugin} + */ +function preactPages({ + root = '/src/pages', + id: virtualId = '~routes', + extensions = ['js', 'ts', 'tsx', 'jsx'], + replacer = '', +} = {}) { + return { + name: 'routes', + enforce: 'pre', + resolveId(id) { + if (id !== virtualId) { + return + } + return `/0${virtualId}` + }, + async load(id) { + if (id !== `/0${virtualId}`) { + return + } + + const extsString = extensions.join(',') + const code = ( + await readFile(join(__dirname, '../runtime/pages.js'), 'utf8') + ) + .replaceAll('#{__PLUGIN_PAGES_ROOT}', root + `/**/*.{${extsString}}`) + .replaceAll('#{__PLUGIN_PAGES_ROOT_REGEX}', `^${root}`) + .replaceAll('#{__PLUGIN_PAGES_ROOT_REGEX_REPLACER}', replacer) + + return { + code, + } + }, + } +}