From 6155a62abdac36a96bc6007d04deaba7fd8b6bb8 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Wed, 24 Jan 2024 20:04:22 +0200 Subject: [PATCH 01/18] feat: add initial implementation of @vaadin/hilla-file-router package --- packages/ts/generator-utils/src/ast.ts | 15 ++- packages/ts/hilla-file-router/.eslintrc | 6 + .../ts/hilla-file-router/.lintstagedrc.js | 6 + packages/ts/hilla-file-router/package.json | 73 +++++++++++ .../ts/hilla-file-router/src/collectRoutes.ts | 49 +++++++ .../ts/hilla-file-router/src/generateJson.ts | 27 ++++ .../hilla-file-router/src/generateRoutes.ts | 99 ++++++++++++++ packages/ts/hilla-file-router/src/index.ts | 17 +++ packages/ts/hilla-file-router/src/utils.ts | 26 ++++ .../src/vite-plugin-file-router.ts | 121 ++++++++++++++++++ .../test/collectRoutes.spec.ts | 68 ++++++++++ .../ts/hilla-file-router/test/generateJson.ts | 31 +++++ .../test/generateRoutes.spec.ts | 76 +++++++++++ packages/ts/hilla-file-router/test/utils.ts | 60 +++++++++ .../ts/hilla-file-router/tsconfig.build.json | 10 ++ packages/ts/hilla-file-router/tsconfig.json | 10 ++ 16 files changed, 692 insertions(+), 2 deletions(-) create mode 100644 packages/ts/hilla-file-router/.eslintrc create mode 100644 packages/ts/hilla-file-router/.lintstagedrc.js create mode 100644 packages/ts/hilla-file-router/package.json create mode 100644 packages/ts/hilla-file-router/src/collectRoutes.ts create mode 100644 packages/ts/hilla-file-router/src/generateJson.ts create mode 100644 packages/ts/hilla-file-router/src/generateRoutes.ts create mode 100644 packages/ts/hilla-file-router/src/index.ts create mode 100644 packages/ts/hilla-file-router/src/utils.ts create mode 100644 packages/ts/hilla-file-router/src/vite-plugin-file-router.ts create mode 100644 packages/ts/hilla-file-router/test/collectRoutes.spec.ts create mode 100644 packages/ts/hilla-file-router/test/generateJson.ts create mode 100644 packages/ts/hilla-file-router/test/generateRoutes.spec.ts create mode 100644 packages/ts/hilla-file-router/test/utils.ts create mode 100644 packages/ts/hilla-file-router/tsconfig.build.json create mode 100644 packages/ts/hilla-file-router/tsconfig.json diff --git a/packages/ts/generator-utils/src/ast.ts b/packages/ts/generator-utils/src/ast.ts index 55153eeb2d..09fd6ea265 100644 --- a/packages/ts/generator-utils/src/ast.ts +++ b/packages/ts/generator-utils/src/ast.ts @@ -1,5 +1,6 @@ import ts, { type Node, + type VisitResult, type SourceFile, type Statement, type TransformationContext, @@ -43,9 +44,19 @@ export function template( return selector?.(sourceFile.statements) ?? sourceFile.statements; } -export function transform(transformer: (node: Node) => Node): TransformerFactory { +export function transform( + transformer: (node: Node) => VisitResult, +): TransformerFactory { return (context: TransformationContext) => (root: T) => { - const visitor = (node: Node): Node => ts.visitEachChild(transformer(node), visitor, context); + const visitor = (node: Node): VisitResult => { + const transformed = transformer(node); + + if (transformed !== node) { + return transformed; + } + + return ts.visitEachChild(transformed, visitor, context); + }; return ts.visitEachChild(root, visitor, context); }; } diff --git a/packages/ts/hilla-file-router/.eslintrc b/packages/ts/hilla-file-router/.eslintrc new file mode 100644 index 0000000000..6ac92d4ca9 --- /dev/null +++ b/packages/ts/hilla-file-router/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": ["../../../.eslintrc"], + "parserOptions": { + "project": "./tsconfig.json" + } +} diff --git a/packages/ts/hilla-file-router/.lintstagedrc.js b/packages/ts/hilla-file-router/.lintstagedrc.js new file mode 100644 index 0000000000..937dc6639f --- /dev/null +++ b/packages/ts/hilla-file-router/.lintstagedrc.js @@ -0,0 +1,6 @@ +import { commands, extensions } from '../../../.lintstagedrc.js'; + +export default { + [`src/**/*.{${extensions}}`]: commands, + [`test/**/*.{${extensions}}`]: commands, +}; diff --git a/packages/ts/hilla-file-router/package.json b/packages/ts/hilla-file-router/package.json new file mode 100644 index 0000000000..8ee2d0c102 --- /dev/null +++ b/packages/ts/hilla-file-router/package.json @@ -0,0 +1,73 @@ +{ + "name": "@vaadin/hilla-file-router", + "version": "24.4.0-alpha1", + "description": "Hilla file-based router", + "main": "index.js", + "module": "index.js", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/vaadin/hilla.git", + "directory": "packages/ts/hilla-file-router" + }, + "keywords": [ + "Hilla", + "Vite", + "Plugin", + "File", + "Router", + "Routing" + ], + "scripts": { + "clean:build": "git clean -fx . -e .vite -e node_modules", + "build": "concurrently npm:build:*", + "build:esbuild": "tsx ../../../scripts/build.ts", + "build:dts": "tsc --isolatedModules -p tsconfig.build.json", + "build:copy": "cd src && copyfiles **/*.d.ts ..", + "lint": "eslint src test", + "lint:fix": "eslint src test --fix", + "test": "karma start ../../../karma.config.cjs --port 9878", + "test:coverage": "npm run test -- --coverage", + "test:watch": "npm run test -- --watch", + "typecheck": "tsc --noEmit" + }, + "exports": { + ".": { + "default": "./index.js" + }, + "./vite-plugin-file-router": { + "default": "./vite-plugin-file-router.js" + } + }, + "author": "Vaadin Ltd", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/vaadin/hilla/issues" + }, + "homepage": "https://vaadin.com", + "files": [ + "*.{d.ts.map,d.ts,js.map,js}" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "react-router": "^6.21.1" + }, + "devDependencies": { + "@esm-bundle/chai": "^4.3.4-fix.0", + "@types/deep-equal-in-any-order": "^1.0.3", + "@types/mocha": "^10.0.6", + "@types/sinon": "^17.0.3", + "deep-equal-in-any-order": "^2.0.6", + "mocha": "^10.2.0", + "rimraf": "^5.0.5", + "sinon": "^17.0.1", + "type-fest": "^4.9.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@vaadin/hilla-generator-utils": "^24.4.0-alpha1", + "react": "^18.2.0" + } +} diff --git a/packages/ts/hilla-file-router/src/collectRoutes.ts b/packages/ts/hilla-file-router/src/collectRoutes.ts new file mode 100644 index 0000000000..2d67272e4d --- /dev/null +++ b/packages/ts/hilla-file-router/src/collectRoutes.ts @@ -0,0 +1,49 @@ +import { opendir } from 'node:fs/promises'; +import { basename, extname, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export type RouteMeta = Readonly<{ + path: string; + file?: URL; + layout?: URL; + children: RouteMeta[]; +}>; + +export type CollectRoutesOptions = Readonly<{ + extensions: readonly string[]; + parent?: URL; +}>; + +export default async function collectRoutes( + dir: URL, + { extensions, parent = dir }: CollectRoutesOptions, +): Promise { + const path = relative(fileURLToPath(parent), fileURLToPath(dir)); + const children: RouteMeta[] = []; + let layout: URL | undefined; + + for await (const d of await opendir(dir)) { + if (d.isDirectory()) { + children.push(await collectRoutes(new URL(`${d.name}/`, dir), { extensions, parent: dir })); + } else if (d.isFile() && extensions.includes(extname(d.name))) { + const file = new URL(d.name, dir); + const name = basename(d.name, extname(d.name)); + + if (name.includes('.layout')) { + layout = file; + } else if (!name.startsWith('_')) { + children.push({ + path: name === 'index' ? '' : name, + file, + children: [], + }); + } + } + } + + return { + path, + layout, + children, + }; +} diff --git a/packages/ts/hilla-file-router/src/generateJson.ts b/packages/ts/hilla-file-router/src/generateJson.ts new file mode 100644 index 0000000000..5697536245 --- /dev/null +++ b/packages/ts/hilla-file-router/src/generateJson.ts @@ -0,0 +1,27 @@ +import type { RouteMeta } from './collectRoutes.js'; + +function* traverse( + views: RouteMeta, + parents: readonly string[] = [], +): Generator { + const chain = [...parents, views.path]; + + if (views.children.length === 0) { + yield chain; + } + + for (const child of views.children) { + yield* traverse(child, chain); + } +} + +export default function generateJson(views: RouteMeta): string { + const paths: string[] = []; + + for (const branch of traverse(views)) { + const path = branch.join('/'); + paths.push(path ? path : '/'); + } + + return JSON.stringify(paths, null, 2); +} diff --git a/packages/ts/hilla-file-router/src/generateRoutes.ts b/packages/ts/hilla-file-router/src/generateRoutes.ts new file mode 100644 index 0000000000..40322d44f9 --- /dev/null +++ b/packages/ts/hilla-file-router/src/generateRoutes.ts @@ -0,0 +1,99 @@ +import { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js'; +import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js'; +import { relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts, { + type ImportDeclaration, + type ObjectLiteralExpression, + type StringLiteral, + type VariableStatement, +} from 'typescript'; +import type { RouteMeta } from './collectRoutes.js'; +import { processPattern, transformRoute } from './utils.js'; + +const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + +function relativize(url: URL, outDir: URL): string { + const result = relative(fileURLToPath(outDir), fileURLToPath(url)); + + if (!result.startsWith('.')) { + return `./${result}`; + } + + return result; +} + +function createImport(component: string, meta: string, file: string): ImportDeclaration { + return template( + `import ${component}, {${meta}} from '${file}';\n`, + ([statement]) => statement as ts.ImportDeclaration, + ); +} + +function createRouteData( + path: string, + component: string | undefined, + meta: string, + children: readonly ObjectLiteralExpression[], +): ObjectLiteralExpression { + return template( + `const route = { + path: '${path}', + ${component ? `component: ${component},` : ''} + meta: ${meta}, + ${children.length > 0 ? `children: CHILDREN,` : ''} +}`, + ([statement]) => + (statement as VariableStatement).declarationList.declarations[0].initializer as ObjectLiteralExpression, + [ + transformer((node) => + ts.isIdentifier(node) && node.text === 'CHILDREN' ? ts.factory.createArrayLiteralExpression(children) : node, + ), + ], + ); +} + +export default function generateRoutes(views: RouteMeta, outDir: URL): string { + const imports: ImportDeclaration[] = []; + let id = 0; + + const routes = transformRoute( + views, + (view) => view.children.values(), + ({ file, layout, path }, children) => { + const currentId = id; + id += 1; + + const meta = `meta${currentId}`; + let component: string | undefined; + if (file) { + component = `Page${currentId}`; + imports.push(createImport(component, meta, relativize(file, outDir))); + } else if (layout) { + component = `Layout${currentId}`; + imports.push(createImport(component, meta, relativize(layout, outDir))); + } + + return createRouteData(processPattern(path), component, `meta${currentId}`, children); + }, + ); + + const routeDeclaration = template( + `import a from 'IMPORTS'; + +const routes = ROUTE; + +export default routes; +`, + [ + transformer((node) => + ts.isImportDeclaration(node) && (node.moduleSpecifier as StringLiteral).text === 'IMPORTS' ? imports : node, + ), + transformer((node) => (ts.isIdentifier(node) && node.text === 'ROUTE' ? routes : node)), + ], + ); + + const file = createSourceFile(routeDeclaration, 'views.ts'); + + return printer.printFile(file); +} diff --git a/packages/ts/hilla-file-router/src/index.ts b/packages/ts/hilla-file-router/src/index.ts new file mode 100644 index 0000000000..fe7429075d --- /dev/null +++ b/packages/ts/hilla-file-router/src/index.ts @@ -0,0 +1,17 @@ +import { type ComponentType, createElement } from 'react'; +import type { NonIndexRouteObject, RouteObject } from 'react-router'; +import { type AgnosticRoute, transformRoute } from './utils.js'; + +export function toReact(routes: AgnosticRoute): RouteObject { + return transformRoute( + routes, + (route) => route.children?.values(), + ({ path, component, meta }, children) => + ({ + ...meta, + path, + element: component ? createElement(component) : undefined, + children: children.length > 0 ? (children as RouteObject[]) : undefined, + }) as RouteObject, + ); +} diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/utils.ts new file mode 100644 index 0000000000..3a4fbae38d --- /dev/null +++ b/packages/ts/hilla-file-router/src/utils.ts @@ -0,0 +1,26 @@ +export type AgnosticRoute> = Readonly<{ + path: string; + component?: T; + meta?: M; + children?: ReadonlyArray>; +}>; + +export function processPattern(blank: string): string { + return blank + .replaceAll(/\[\.{3}.+\]/gu, '*') + .replaceAll(/\[{2}(.+)\]{2}/gu, ':$1?') + .replaceAll(/\[(.+)\]/gu, ':$1'); +} + +export function transformRoute( + route: T, + getChildren: (route: T) => IterableIterator | null | undefined, + transformer: (route: T, children: readonly U[]) => U, +): U { + const children = getChildren(route); + + return transformer( + route, + children ? Array.from(children, (child) => transformRoute(child, getChildren, transformer)) : [], + ); +} diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts new file mode 100644 index 0000000000..1bbb17b557 --- /dev/null +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -0,0 +1,121 @@ +import { opendir, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import type { Writable } from 'type-fest'; +import type { Plugin } from 'vite'; +import collectRoutes from './collectRoutes.js'; +import generateJson from './generateJson.js'; +import generateRoutes from './generateRoutes.js'; + +export type PluginOptions = Readonly<{ + /** + * The base directory for the router. The folders and files in this directory + * will be used as route paths. + * + * @defaultValue `frontend/views` + */ + viewsDir?: URL | string; + /** + * The directory where the generated view file will be stored. + * + * @defaultValue `frontend/generated` + */ + generatedDir?: URL | string; + /** + * The list of extensions that will be collected as routes of the file-based router. + * + * @defaultValue `['.tsx', '.jsx', '.ts', '.js']` + */ + extensions: readonly string[]; +}>; + +type RouteData = Readonly<{ + pattern: string; + file?: URL; +}>; + +async function* walk( + dir: URL, + parents: readonly RouteData[], +): AsyncGenerator { + for await (const d of await opendir(dir)) { + const entry = new URL(d.name, dir); + if (d.isDirectory()) { + yield* walk(entry, [...parents, { pattern: d.name }]); + } else if (d.isFile()) { + if (d.name.startsWith('layout')) { + if (parents.length > 0) { + (parents.at(-1)! as Writable).file = entry; + } + } else { + yield [...parents, { pattern: d.name, file: entry }]; + } + } + } +} + +type GeneratedUrls = Readonly<{ + json: URL; + code: URL; +}>; + +async function generate(code: string, json: string, urls: GeneratedUrls) { + await Promise.all([writeFile(urls.json, json, 'utf-8'), writeFile(urls.code, code, 'utf-8')]); +} + +async function build( + viewsDir: URL, + outDir: URL, + generatedUrls: GeneratedUrls, + extensions: readonly string[], +): Promise { + const routeMeta = await collectRoutes(viewsDir, { extensions }); + const code = generateRoutes(routeMeta, outDir); + const json = generateJson(routeMeta); + + await generate(code, json, generatedUrls); +} + +/** + * A Vite plugin that generates a router from the files in the specific directory. + * + * @param options - The plugin options. + * @returns A Vite plugin. + */ +export default function vitePluginFileSystemRouter({ + viewsDir = 'frontend/views/', + generatedDir = 'frontend/generated/', + extensions = ['.tsx', '.jsx', '.ts', '.js'], +}: PluginOptions): Plugin { + let _viewsDir: URL; + let _generatedDir: URL; + let _outDir: URL; + let generatedUrls: GeneratedUrls; + + return { + name: 'vite-plugin-file-router', + configResolved({ root, build: { outDir } }) { + const _root = new URL(root); + _viewsDir = new URL(viewsDir, _root); + _generatedDir = new URL(generatedDir, _root); + _outDir = new URL(outDir, _root); + generatedUrls = { + json: new URL('views.json', _outDir), + code: new URL('views.ts', _generatedDir), + }; + }, + async buildStart() { + await build(_viewsDir, _outDir, generatedUrls, extensions); + }, + configureServer(server) { + const dir = fileURLToPath(_viewsDir); + + server.watcher.on('unlink', (file) => { + if (!file.startsWith(dir)) { + return; + } + + build(_viewsDir, _outDir, generatedUrls, extensions).catch((error) => console.error(error)); + }); + }, + }; +} diff --git a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts new file mode 100644 index 0000000000..fcd5ef40b4 --- /dev/null +++ b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts @@ -0,0 +1,68 @@ +import { appendFile, mkdir, mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { expect, use } from '@esm-bundle/chai'; +import deepEqualInAnyOrder from 'deep-equal-in-any-order'; +import { rimraf } from 'rimraf'; +import collectRoutes from '../src/collectRoutes.js'; +import { createTestingRouteMeta } from './utils.js'; + +use(deepEqualInAnyOrder); + +describe('@vaadin/hilla-file-router', () => { + describe('collectFileRoutes', () => { + const extensions = ['.tsx', '.jsx', '.ts', '.js']; + let tmp: URL; + + before(async () => { + tmp = pathToFileURL(`${await mkdtemp(join(tmpdir(), 'hilla-file-router-'))}/`); + }); + + after(async () => { + await rimraf(fileURLToPath(tmp)); + }); + + beforeEach(async () => { + await rimraf(fileURLToPath(new URL('*', tmp)), { glob: true }); + }); + + it('should build a route tree', async () => { + // root + // ├── profile + // │ ├── account + // │ │ ├── layout.tsx + // │ │ └── security + // │ │ ├── password.tsx + // │ │ └── two-factor-auth.tsx + // │ ├── friends + // │ │ ├── layout.tsx + // │ │ ├── list.tsx + // │ │ └── [user].tsx + // │ ├── index.tsx + // │ └── layout.tsx + // └── about.tsx + + await Promise.all([ + mkdir(new URL('profile/account/security/', tmp), { recursive: true }), + mkdir(new URL('profile/friends/', tmp), { recursive: true }), + ]); + await Promise.all([ + appendFile(new URL('profile/account/account.layout.tsx', tmp), ''), + appendFile(new URL('profile/account/security/password.jsx', tmp), ''), + appendFile(new URL('profile/account/security/password.scss', tmp), ''), + appendFile(new URL('profile/account/security/two-factor-auth.ts', tmp), ''), + appendFile(new URL('profile/friends/friends.layout.tsx', tmp), ''), + appendFile(new URL('profile/friends/list.js', tmp), ''), + appendFile(new URL('profile/friends/[user].tsx', tmp), ''), + appendFile(new URL('profile/index.tsx', tmp), ''), + appendFile(new URL('profile/index.css', tmp), ''), + appendFile(new URL('about.tsx', tmp), ''), + ]); + + const result = await collectRoutes(tmp, { extensions }); + + expect(result).to.deep.equalInAnyOrder(createTestingRouteMeta(tmp)); + }); + }); +}); diff --git a/packages/ts/hilla-file-router/test/generateJson.ts b/packages/ts/hilla-file-router/test/generateJson.ts new file mode 100644 index 0000000000..3cff045f35 --- /dev/null +++ b/packages/ts/hilla-file-router/test/generateJson.ts @@ -0,0 +1,31 @@ +import { expect } from '@esm-bundle/chai'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import type { RouteMeta } from '../src/collectRoutes.js'; +import generateJson from '../src/generateJson.js'; +import { createTestingRouteMeta } from './utils.js'; + +describe('@vaadin/hilla-file-router', () => { + describe('generateJson', () => { + let meta: RouteMeta; + + beforeEach(() => { + const dir = pathToFileURL(join(tmpdir(), 'hilla-file-router/')); + meta = createTestingRouteMeta(new URL('./views/', dir)); + }); + + it('should generate a JSON representation of the route tree', () => { + const generated = generateJson(meta); + + expect(generated).to.equal(`[ + "/profile/", + "/profile/friends/list", + "/profile/friends/[user]", + "/profile/account/security/password", + "/profile/account/security/two-factor-auth", + "/about" +]`); + }); + }); +}); diff --git a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts new file mode 100644 index 0000000000..71e39dd163 --- /dev/null +++ b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts @@ -0,0 +1,76 @@ +import { expect } from '@esm-bundle/chai'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import type { RouteMeta } from '../src/collectRoutes.js'; +import generateRoutes from '../src/generateRoutes.js'; +import { createTestingRouteMeta } from './utils.js'; + +describe('@vaadin/hilla-file-router', () => { + describe('generateRoutes', () => { + let dir: URL; + let meta: RouteMeta; + + beforeEach(() => { + dir = pathToFileURL(join(tmpdir(), 'hilla-file-router/')); + meta = createTestingRouteMeta(new URL('./views/', dir)); + }); + + it('should generate a framework-agnostic tree of routes', () => { + const generated = generateRoutes(meta, new URL('./out/', dir)); + + expect(generated).to.equal(`import Page0, { meta0 } from "../views/profile/friends/list.tsx"; +import Page1, { meta1 } from "../views/profile/friends/[user].tsx"; +import Layout2, { meta2 } from "../views/profile/friends/friends.layout.tsx"; +import Page3, { meta3 } from "../views/profile/account/security/password.tsx"; +import Page4, { meta4 } from "../views/profile/account/security/two-factor-auth.tsx"; +import Layout6, { meta6 } from "../views/account.layout.tsx"; +import Page8, { meta8 } from "../views/about.tsx"; +const routes = { + path: "", + meta: meta9, + children: [{ + path: "profile", + meta: meta7, + children: [{ + path: "friends", + component: Layout2, + meta: meta2, + children: [{ + path: "list", + component: Page0, + meta: meta0, + }, { + path: ":user", + component: Page1, + meta: meta1, + }], + }, { + path: "account", + component: Layout6, + meta: meta6, + children: [{ + path: "security", + meta: meta5, + children: [{ + path: "password", + component: Page3, + meta: meta3, + }, { + path: "two-factor-auth", + component: Page4, + meta: meta4, + }], + }], + }], + }, { + path: "about", + component: Page8, + meta: meta8, + }], +}; +export default routes; +`); + }); + }); +}); diff --git a/packages/ts/hilla-file-router/test/utils.ts b/packages/ts/hilla-file-router/test/utils.ts new file mode 100644 index 0000000000..aef4077096 --- /dev/null +++ b/packages/ts/hilla-file-router/test/utils.ts @@ -0,0 +1,60 @@ +import type { RouteMeta } from '../src/collectRoutes.js'; + +export function createTestingRouteMeta(dir: URL): RouteMeta { + return { + path: '', + layout: undefined, + children: [ + { + path: 'profile', + layout: undefined, + children: [ + { path: '', file: new URL('profile/index.tsx', dir), children: [] }, + { + path: 'friends', + layout: new URL('profile/friends/friends.layout.tsx', dir), + children: [ + { + path: 'list', + file: new URL('profile/friends/list.tsx', dir), + children: [], + }, + { + path: '[user]', + file: new URL('profile/friends/[user].tsx', dir), + children: [], + }, + ], + }, + { + path: 'account', + layout: new URL('account.layout.tsx', dir), + children: [ + { + path: 'security', + layout: undefined, + children: [ + { + path: 'password', + file: new URL('profile/account/security/password.tsx', dir), + children: [], + }, + { + path: 'two-factor-auth', + file: new URL('profile/account/security/two-factor-auth.tsx', dir), + children: [], + }, + ], + }, + ], + }, + ], + }, + { + path: 'about', + file: new URL('about.tsx', dir), + children: [], + }, + ], + }; +} diff --git a/packages/ts/hilla-file-router/tsconfig.build.json b/packages/ts/hilla-file-router/tsconfig.build.json new file mode 100644 index 0000000000..a57d153410 --- /dev/null +++ b/packages/ts/hilla-file-router/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "emitDeclarationOnly": true, + "declarationMap": true, + "rootDir": "src", + "outDir": "." + }, + "include": ["src"] +} diff --git a/packages/ts/hilla-file-router/tsconfig.json b/packages/ts/hilla-file-router/tsconfig.json new file mode 100644 index 0000000000..440ed221a9 --- /dev/null +++ b/packages/ts/hilla-file-router/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "experimentalDecorators": true, + "target": "es2022" + }, + "include": ["src", "test"], + "exclude": ["test/**/*.snap.ts"] +} From d043acad55d2bf4e0d93fff7f9e35a3a8b08d615 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich Date: Thu, 25 Jan 2024 14:29:51 +0200 Subject: [PATCH 02/18] feat: add React implementation --- .../ts/hilla-file-router/src/collectRoutes.ts | 11 +- .../hilla-file-router/src/generateRoutes.ts | 22 ++-- packages/ts/hilla-file-router/src/index.ts | 17 --- packages/ts/hilla-file-router/src/react.ts | 56 ++++++++++ packages/ts/hilla-file-router/src/utils.ts | 9 +- .../test/collectRoutes.spec.ts | 6 +- .../{generateJson.ts => generateJson.spec.ts} | 4 +- .../test/generateRoutes.spec.ts | 60 +++++------ .../ts/hilla-file-router/test/react.spec.tsx | 101 ++++++++++++++++++ packages/ts/hilla-file-router/test/utils.ts | 48 ++++----- 10 files changed, 235 insertions(+), 99 deletions(-) delete mode 100644 packages/ts/hilla-file-router/src/index.ts create mode 100644 packages/ts/hilla-file-router/src/react.ts rename packages/ts/hilla-file-router/test/{generateJson.ts => generateJson.spec.ts} (96%) create mode 100644 packages/ts/hilla-file-router/test/react.spec.tsx diff --git a/packages/ts/hilla-file-router/src/collectRoutes.ts b/packages/ts/hilla-file-router/src/collectRoutes.ts index 2d67272e4d..f58474cf21 100644 --- a/packages/ts/hilla-file-router/src/collectRoutes.ts +++ b/packages/ts/hilla-file-router/src/collectRoutes.ts @@ -14,6 +14,15 @@ export type CollectRoutesOptions = Readonly<{ parent?: URL; }>; +function cleanUp(blank: string) { + return blank + .replaceAll(/\{\.{3}(.+)\}/gu, '$1') + .replaceAll(/\{{2}(.+)\}{2}/gu, '$1') + .replaceAll(/\{(.+)\}/gu, '$1'); +} + +const collator = new Intl.Collator('en-US'); + export default async function collectRoutes( dir: URL, { extensions, parent = dir }: CollectRoutesOptions, @@ -44,6 +53,6 @@ export default async function collectRoutes( return { path, layout, - children, + children: children.sort(({ path: a }, { path: b }) => collator.compare(cleanUp(a), cleanUp(b))), }; } diff --git a/packages/ts/hilla-file-router/src/generateRoutes.ts b/packages/ts/hilla-file-router/src/generateRoutes.ts index 40322d44f9..b5e57c6f56 100644 --- a/packages/ts/hilla-file-router/src/generateRoutes.ts +++ b/packages/ts/hilla-file-router/src/generateRoutes.ts @@ -1,7 +1,7 @@ -import { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js'; -import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js'; import { relative } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js'; +import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js'; import ts, { type ImportDeclaration, type ObjectLiteralExpression, @@ -23,24 +23,19 @@ function relativize(url: URL, outDir: URL): string { return result; } -function createImport(component: string, meta: string, file: string): ImportDeclaration { - return template( - `import ${component}, {${meta}} from '${file}';\n`, - ([statement]) => statement as ts.ImportDeclaration, - ); +function createImport(component: string, file: string): ImportDeclaration { + return template(`import ${component} from '${file}';\n`, ([statement]) => statement as ts.ImportDeclaration); } function createRouteData( path: string, component: string | undefined, - meta: string, children: readonly ObjectLiteralExpression[], ): ObjectLiteralExpression { return template( `const route = { path: '${path}', - ${component ? `component: ${component},` : ''} - meta: ${meta}, + ${component ? `component: ${component}` : ''} ${children.length > 0 ? `children: CHILDREN,` : ''} }`, ([statement]) => @@ -64,17 +59,16 @@ export default function generateRoutes(views: RouteMeta, outDir: URL): string { const currentId = id; id += 1; - const meta = `meta${currentId}`; let component: string | undefined; if (file) { component = `Page${currentId}`; - imports.push(createImport(component, meta, relativize(file, outDir))); + imports.push(createImport(component, relativize(file, outDir))); } else if (layout) { component = `Layout${currentId}`; - imports.push(createImport(component, meta, relativize(layout, outDir))); + imports.push(createImport(component, relativize(layout, outDir))); } - return createRouteData(processPattern(path), component, `meta${currentId}`, children); + return createRouteData(processPattern(path), component, children); }, ); diff --git a/packages/ts/hilla-file-router/src/index.ts b/packages/ts/hilla-file-router/src/index.ts deleted file mode 100644 index fe7429075d..0000000000 --- a/packages/ts/hilla-file-router/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type ComponentType, createElement } from 'react'; -import type { NonIndexRouteObject, RouteObject } from 'react-router'; -import { type AgnosticRoute, transformRoute } from './utils.js'; - -export function toReact(routes: AgnosticRoute): RouteObject { - return transformRoute( - routes, - (route) => route.children?.values(), - ({ path, component, meta }, children) => - ({ - ...meta, - path, - element: component ? createElement(component) : undefined, - children: children.length > 0 ? (children as RouteObject[]) : undefined, - }) as RouteObject, - ); -} diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/react.ts new file mode 100644 index 0000000000..e3777d5b96 --- /dev/null +++ b/packages/ts/hilla-file-router/src/react.ts @@ -0,0 +1,56 @@ +import { type ComponentType, createElement } from 'react'; +import type { RouteObject } from 'react-router'; +import { type AgnosticRoute, transformRoute } from './utils.js'; + +export type ViewConfig = Readonly<{ + /** + * View title used in the main layout header, as and as the default + * for the menu entry. If not defined, then the view function name is converted + * from CamelCase after removing any "View" postfix. + */ + title?: string; + + /** Same as in the explicit React Router configuration. */ + rolesAllowed?: string[]; + + /** Allows overriding the route path configuration. Uses the same syntax as the path property with React Router. This can be used to define a route that conflicts with the file name conventions, e.g. /foo/index */ + route?: string; + + /** Controls whether the view implementation will be lazy loaded the first time it's used or always included in the bundle. If set to undefined (which is the default), views mapped to / and /login will be eager and any other view will be lazy (this is in sync with defaults in Flow) */ + lazy?: boolean; + + /** If set to false, then the route will not be registered with React Router but it will still be included in the main menu and used to configure Spring Security */ + register?: boolean; + + menu?: Readonly<{ + /** Title to use in the menu. Falls back the title property of the view itself if not defined. */ + title?: string; + + /** + * Used to determine the order in the menu. Ties are resolved based on the + * used title. Entries without explicitly defined ordering are put below + * entries with an order. + */ + priority?: number; + /** Set to true to explicitly exclude a view from the automatically populated menu. */ + exclude?: boolean; + }>; +}>; + +export type RouteComponent<P = object> = ComponentType<P> & { + meta?: ViewConfig; +}; + +export function toReactRouter(routes: AgnosticRoute<RouteComponent>): RouteObject { + return transformRoute( + routes, + (route) => route.children?.values(), + ({ path, component }, children) => + ({ + path, + element: component ? createElement(component) : undefined, + children: children.length > 0 ? (children as RouteObject[]) : undefined, + handle: component ? component.meta : undefined, + }) satisfies RouteObject, + ); +} diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/utils.ts index 3a4fbae38d..aae87c70ce 100644 --- a/packages/ts/hilla-file-router/src/utils.ts +++ b/packages/ts/hilla-file-router/src/utils.ts @@ -1,15 +1,14 @@ -export type AgnosticRoute<T, M extends object = Record<string, unknown>> = Readonly<{ +export type AgnosticRoute<T> = Readonly<{ path: string; component?: T; - meta?: M; children?: ReadonlyArray<AgnosticRoute<T>>; }>; export function processPattern(blank: string): string { return blank - .replaceAll(/\[\.{3}.+\]/gu, '*') - .replaceAll(/\[{2}(.+)\]{2}/gu, ':$1?') - .replaceAll(/\[(.+)\]/gu, ':$1'); + .replaceAll(/\{\.{3}.+\}/gu, '*') + .replaceAll(/\{{2}(.+)\}{2}/gu, ':$1?') + .replaceAll(/\{(.+)\}/gu, ':$1'); } export function transformRoute<T, U>( diff --git a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts index fcd5ef40b4..707e599ff2 100644 --- a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts @@ -38,7 +38,7 @@ describe('@vaadin/hilla-file-router', () => { // │ ├── friends // │ │ ├── layout.tsx // │ │ ├── list.tsx - // │ │ └── [user].tsx + // │ │ └── {user}.tsx // │ ├── index.tsx // │ └── layout.tsx // └── about.tsx @@ -54,7 +54,7 @@ describe('@vaadin/hilla-file-router', () => { appendFile(new URL('profile/account/security/two-factor-auth.ts', tmp), ''), appendFile(new URL('profile/friends/friends.layout.tsx', tmp), ''), appendFile(new URL('profile/friends/list.js', tmp), ''), - appendFile(new URL('profile/friends/[user].tsx', tmp), ''), + appendFile(new URL('profile/friends/{user}.tsx', tmp), ''), appendFile(new URL('profile/index.tsx', tmp), ''), appendFile(new URL('profile/index.css', tmp), ''), appendFile(new URL('about.tsx', tmp), ''), @@ -62,7 +62,7 @@ describe('@vaadin/hilla-file-router', () => { const result = await collectRoutes(tmp, { extensions }); - expect(result).to.deep.equalInAnyOrder(createTestingRouteMeta(tmp)); + expect(result).to.deep.equals(createTestingRouteMeta(tmp)); }); }); }); diff --git a/packages/ts/hilla-file-router/test/generateJson.ts b/packages/ts/hilla-file-router/test/generateJson.spec.ts similarity index 96% rename from packages/ts/hilla-file-router/test/generateJson.ts rename to packages/ts/hilla-file-router/test/generateJson.spec.ts index 3cff045f35..07f372af00 100644 --- a/packages/ts/hilla-file-router/test/generateJson.ts +++ b/packages/ts/hilla-file-router/test/generateJson.spec.ts @@ -1,7 +1,7 @@ -import { expect } from '@esm-bundle/chai'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; +import { expect } from '@esm-bundle/chai'; import type { RouteMeta } from '../src/collectRoutes.js'; import generateJson from '../src/generateJson.js'; import { createTestingRouteMeta } from './utils.js'; @@ -21,7 +21,7 @@ describe('@vaadin/hilla-file-router', () => { expect(generated).to.equal(`[ "/profile/", "/profile/friends/list", - "/profile/friends/[user]", + "/profile/friends/{user}", "/profile/account/security/password", "/profile/account/security/two-factor-auth", "/about" diff --git a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts index 71e39dd163..d9f47e3f62 100644 --- a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts @@ -1,7 +1,7 @@ -import { expect } from '@esm-bundle/chai'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; +import { expect } from '@esm-bundle/chai'; import type { RouteMeta } from '../src/collectRoutes.js'; import generateRoutes from '../src/generateRoutes.js'; import { createTestingRouteMeta } from './utils.js'; @@ -19,54 +19,48 @@ describe('@vaadin/hilla-file-router', () => { it('should generate a framework-agnostic tree of routes', () => { const generated = generateRoutes(meta, new URL('./out/', dir)); - expect(generated).to.equal(`import Page0, { meta0 } from "../views/profile/friends/list.tsx"; -import Page1, { meta1 } from "../views/profile/friends/[user].tsx"; -import Layout2, { meta2 } from "../views/profile/friends/friends.layout.tsx"; -import Page3, { meta3 } from "../views/profile/account/security/password.tsx"; -import Page4, { meta4 } from "../views/profile/account/security/two-factor-auth.tsx"; -import Layout6, { meta6 } from "../views/account.layout.tsx"; -import Page8, { meta8 } from "../views/about.tsx"; + expect(generated).to.equal(`import Page0 from "../views/about.tsx"; +import Page1 from "../views/profile/index.tsx"; +import Page2 from "../views/profile/account/security/password.jsx"; +import Page3 from "../views/profile/account/security/two-factor-auth.ts"; +import Layout5 from "../views/profile/account/account.layout.tsx"; +import Page6 from "../views/profile/friends/list.js"; +import Page7 from "../views/profile/friends/{user}.tsx"; +import Layout8 from "../views/profile/friends/friends.layout.tsx"; const routes = { path: "", - meta: meta9, children: [{ + path: "about", + component: Page0 + }, { path: "profile", - meta: meta7, children: [{ - path: "friends", - component: Layout2, - meta: meta2, - children: [{ - path: "list", - component: Page0, - meta: meta0, - }, { - path: ":user", - component: Page1, - meta: meta1, - }], + path: "", + component: Page1 }, { path: "account", - component: Layout6, - meta: meta6, + component: Layout5, children: [{ path: "security", - meta: meta5, children: [{ path: "password", - component: Page3, - meta: meta3, + component: Page2 }, { path: "two-factor-auth", - component: Page4, - meta: meta4, + component: Page3 }], }], + }, { + path: "friends", + component: Layout8, + children: [{ + path: "list", + component: Page6 + }, { + path: ":user", + component: Page7 + }], }], - }, { - path: "about", - component: Page8, - meta: meta8, }], }; export default routes; diff --git a/packages/ts/hilla-file-router/test/react.spec.tsx b/packages/ts/hilla-file-router/test/react.spec.tsx new file mode 100644 index 0000000000..152a447c78 --- /dev/null +++ b/packages/ts/hilla-file-router/test/react.spec.tsx @@ -0,0 +1,101 @@ +import { expect, use } from '@esm-bundle/chai'; +import chaiLike from 'chai-like'; +import type { JSX } from 'react'; +import { type RouteComponent, toReactRouter } from '../src/react.js'; +import type { AgnosticRoute } from '../utils.js'; + +use(chaiLike); + +describe('@vaadin/hilla-file-router', () => { + describe('react', () => { + function About(): JSX.Element { + return <></>; + } + + About.meta = { title: 'About' }; + + function Friends(): JSX.Element { + return <></>; + } + + Friends.meta = { title: 'Friends' }; + + function FriendsList(): JSX.Element { + return <></>; + } + + FriendsList.meta = { title: 'Friends List' }; + + function Friend(): JSX.Element { + return <></>; + } + + Friend.meta = { title: 'Friend' }; + + it('should be able to convert an agnostic routes to React Router routes', () => { + const routes = { + path: '', + children: [ + { + path: 'about', + component: About, + }, + { + path: 'profile', + children: [ + { + path: 'friends', + component: Friends, + children: [ + { + path: 'list', + component: FriendsList, + }, + { + path: '{user}', + component: Friend, + }, + ], + }, + ], + }, + ], + } satisfies AgnosticRoute<RouteComponent>; + + const result = toReactRouter(routes); + + expect(result).to.be.like({ + path: '', + children: [ + { + path: 'about', + element: <About />, + handle: About.meta, + }, + { + path: 'profile', + children: [ + { + path: 'friends', + element: <Friends />, + handle: Friends.meta, + children: [ + { + path: 'list', + element: <FriendsList />, + handle: FriendsList.meta, + }, + { + path: '{user}', + element: <Friend />, + handle: Friend.meta, + }, + ], + }, + ], + }, + ], + }); + }); + }); +}); diff --git a/packages/ts/hilla-file-router/test/utils.ts b/packages/ts/hilla-file-router/test/utils.ts index aef4077096..642015205e 100644 --- a/packages/ts/hilla-file-router/test/utils.ts +++ b/packages/ts/hilla-file-router/test/utils.ts @@ -5,30 +5,19 @@ export function createTestingRouteMeta(dir: URL): RouteMeta { path: '', layout: undefined, children: [ + { + path: 'about', + file: new URL('about.tsx', dir), + children: [], + }, { path: 'profile', layout: undefined, children: [ { path: '', file: new URL('profile/index.tsx', dir), children: [] }, - { - path: 'friends', - layout: new URL('profile/friends/friends.layout.tsx', dir), - children: [ - { - path: 'list', - file: new URL('profile/friends/list.tsx', dir), - children: [], - }, - { - path: '[user]', - file: new URL('profile/friends/[user].tsx', dir), - children: [], - }, - ], - }, { path: 'account', - layout: new URL('account.layout.tsx', dir), + layout: new URL('profile/account/account.layout.tsx', dir), children: [ { path: 'security', @@ -36,25 +25,36 @@ export function createTestingRouteMeta(dir: URL): RouteMeta { children: [ { path: 'password', - file: new URL('profile/account/security/password.tsx', dir), + file: new URL('profile/account/security/password.jsx', dir), children: [], }, { path: 'two-factor-auth', - file: new URL('profile/account/security/two-factor-auth.tsx', dir), + file: new URL('profile/account/security/two-factor-auth.ts', dir), children: [], }, ], }, ], }, + { + path: 'friends', + layout: new URL('profile/friends/friends.layout.tsx', dir), + children: [ + { + path: 'list', + file: new URL('profile/friends/list.js', dir), + children: [], + }, + { + path: '{user}', + file: new URL('profile/friends/{user}.tsx', dir), + children: [], + }, + ], + }, ], }, - { - path: 'about', - file: new URL('about.tsx', dir), - children: [], - }, ], }; } From 658759338a603af27223fc822f73e9e2fe23023b Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Thu, 25 Jan 2024 14:31:02 +0200 Subject: [PATCH 03/18] chore(file-router): export react as a separate entrypoint --- packages/ts/hilla-file-router/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ts/hilla-file-router/package.json b/packages/ts/hilla-file-router/package.json index 8ee2d0c102..8cb24960f1 100644 --- a/packages/ts/hilla-file-router/package.json +++ b/packages/ts/hilla-file-router/package.json @@ -32,10 +32,10 @@ "typecheck": "tsc --noEmit" }, "exports": { - ".": { - "default": "./index.js" + "./react.js": { + "default": "./react.js" }, - "./vite-plugin-file-router": { + "./vite-plugin-file-router.js": { "default": "./vite-plugin-file-router.js" } }, @@ -56,9 +56,11 @@ }, "devDependencies": { "@esm-bundle/chai": "^4.3.4-fix.0", + "@types/chai-like": "^1.1.3", "@types/deep-equal-in-any-order": "^1.0.3", "@types/mocha": "^10.0.6", "@types/sinon": "^17.0.3", + "chai-like": "^1.1.1", "deep-equal-in-any-order": "^2.0.6", "mocha": "^10.2.0", "rimraf": "^5.0.5", From 7f96e487f2a55f0565c3e9e2e4ee8a7573bc1fcb Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Thu, 25 Jan 2024 14:33:30 +0200 Subject: [PATCH 04/18] docs(file-router): improve comments --- packages/ts/hilla-file-router/src/react.ts | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/react.ts index e3777d5b96..29f46fd8c5 100644 --- a/packages/ts/hilla-file-router/src/react.ts +++ b/packages/ts/hilla-file-router/src/react.ts @@ -10,20 +10,34 @@ export type ViewConfig = Readonly<{ */ title?: string; - /** Same as in the explicit React Router configuration. */ + /** + * Same as in the explicit React Router configuration. + */ rolesAllowed?: string[]; - /** Allows overriding the route path configuration. Uses the same syntax as the path property with React Router. This can be used to define a route that conflicts with the file name conventions, e.g. /foo/index */ + /** Allows overriding the route path configuration. Uses the same syntax as + * the path property with React Router.This can be used to define a route + * that conflicts with the file name conventions, e.g. /foo/index + */ route?: string; - /** Controls whether the view implementation will be lazy loaded the first time it's used or always included in the bundle. If set to undefined (which is the default), views mapped to / and /login will be eager and any other view will be lazy (this is in sync with defaults in Flow) */ + /** Controls whether the view implementation will be lazy loaded the first time + * it's used or always included in the bundle. If set to undefined (which is + * the default), views mapped to / and /login will be eager and any other view + * will be lazy (this is in sync with defaults in Flow) + */ lazy?: boolean; - /** If set to false, then the route will not be registered with React Router but it will still be included in the main menu and used to configure Spring Security */ + /** If set to false, then the route will not be registered with React Router, + * but it will still be included in the main menu and used to configure + * Spring Security + */ register?: boolean; menu?: Readonly<{ - /** Title to use in the menu. Falls back the title property of the view itself if not defined. */ + /** Title to use in the menu. Falls back the title property of the view + * itself if not defined. + */ title?: string; /** @@ -32,7 +46,9 @@ export type ViewConfig = Readonly<{ * entries with an order. */ priority?: number; - /** Set to true to explicitly exclude a view from the automatically populated menu. */ + /** Set to true to explicitly exclude a view from the automatically + * populated menu. + */ exclude?: boolean; }>; }>; From 0a2547fe0e571926bc07f2f549b5107bf9fa1660 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Thu, 25 Jan 2024 15:29:11 +0200 Subject: [PATCH 05/18] feat(file-router): add useViewConfig hook --- packages/ts/hilla-file-router/src/react.ts | 31 ++++++++++++++---- .../src/vite-plugin-file-router.ts | 32 ++----------------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/react.ts index 29f46fd8c5..d628be2d3c 100644 --- a/packages/ts/hilla-file-router/src/react.ts +++ b/packages/ts/hilla-file-router/src/react.ts @@ -1,5 +1,6 @@ +import type { UIMatch } from '@remix-run/router'; import { type ComponentType, createElement } from 'react'; -import type { RouteObject } from 'react-router'; +import { type RouteObject, useMatches } from 'react-router'; import { type AgnosticRoute, transformRoute } from './utils.js'; export type ViewConfig = Readonly<{ @@ -15,27 +16,31 @@ export type ViewConfig = Readonly<{ */ rolesAllowed?: string[]; - /** Allows overriding the route path configuration. Uses the same syntax as + /** + * Allows overriding the route path configuration. Uses the same syntax as * the path property with React Router.This can be used to define a route * that conflicts with the file name conventions, e.g. /foo/index */ route?: string; - /** Controls whether the view implementation will be lazy loaded the first time + /** + * Controls whether the view implementation will be lazy loaded the first time * it's used or always included in the bundle. If set to undefined (which is * the default), views mapped to / and /login will be eager and any other view * will be lazy (this is in sync with defaults in Flow) */ lazy?: boolean; - /** If set to false, then the route will not be registered with React Router, + /** + * If set to false, then the route will not be registered with React Router, * but it will still be included in the main menu and used to configure * Spring Security */ register?: boolean; menu?: Readonly<{ - /** Title to use in the menu. Falls back the title property of the view + /** + * Title to use in the menu. Falls back the title property of the view * itself if not defined. */ title?: string; @@ -46,7 +51,8 @@ export type ViewConfig = Readonly<{ * entries with an order. */ priority?: number; - /** Set to true to explicitly exclude a view from the automatically + /** + * Set to true to explicitly exclude a view from the automatically * populated menu. */ exclude?: boolean; @@ -57,6 +63,11 @@ export type RouteComponent<P = object> = ComponentType<P> & { meta?: ViewConfig; }; +/** + * Transforms generated routes into a format that can be used by React Router. + * + * @param routes - Generated routes + */ export function toReactRouter(routes: AgnosticRoute<RouteComponent>): RouteObject { return transformRoute( routes, @@ -70,3 +81,11 @@ export function toReactRouter(routes: AgnosticRoute<RouteComponent>): RouteObjec }) satisfies RouteObject, ); } + +/** + * Hook to return the {@link ViewConfig} for the current route. + */ +export function useViewConfig<M extends ViewConfig>(): M | undefined { + const matches = useMatches() as ReadonlyArray<UIMatch<unknown, M>>; + return matches[matches.length - 1]?.handle; +} diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts index 1bbb17b557..b0d787f8c4 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -1,6 +1,5 @@ -import { opendir, writeFile } from 'node:fs/promises'; +import { writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; -import type { Writable } from 'type-fest'; import type { Plugin } from 'vite'; import collectRoutes from './collectRoutes.js'; import generateJson from './generateJson.js'; @@ -25,34 +24,9 @@ export type PluginOptions = Readonly<{ * * @defaultValue `['.tsx', '.jsx', '.ts', '.js']` */ - extensions: readonly string[]; + extensions?: readonly string[]; }>; -type RouteData = Readonly<{ - pattern: string; - file?: URL; -}>; - -async function* walk( - dir: URL, - parents: readonly RouteData[], -): AsyncGenerator<readonly RouteData[], undefined, undefined> { - for await (const d of await opendir(dir)) { - const entry = new URL(d.name, dir); - if (d.isDirectory()) { - yield* walk(entry, [...parents, { pattern: d.name }]); - } else if (d.isFile()) { - if (d.name.startsWith('layout')) { - if (parents.length > 0) { - (parents.at(-1)! as Writable<RouteData>).file = entry; - } - } else { - yield [...parents, { pattern: d.name, file: entry }]; - } - } - } -} - type GeneratedUrls = Readonly<{ json: URL; code: URL; @@ -85,7 +59,7 @@ export default function vitePluginFileSystemRouter({ viewsDir = 'frontend/views/', generatedDir = 'frontend/generated/', extensions = ['.tsx', '.jsx', '.ts', '.js'], -}: PluginOptions): Plugin { +}: PluginOptions = {}): Plugin { let _viewsDir: URL; let _generatedDir: URL; let _outDir: URL; From de38f7e634d0f04c8e905336c3ffe96d312d59cd Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Tue, 30 Jan 2024 13:03:41 +0200 Subject: [PATCH 06/18] refactor(file-router): generate meta info in JSON file --- .../ts/hilla-file-router/src/generateJson.ts | 39 ++++++++++---- .../hilla-file-router/src/generateRoutes.ts | 29 ++++++----- packages/ts/hilla-file-router/src/react.ts | 13 ++--- packages/ts/hilla-file-router/src/utils.ts | 2 +- .../src/vite-plugin-file-router.ts | 10 ++-- .../test/collectRoutes.spec.ts | 23 ++------- .../test/generateJson.spec.ts | 40 ++++++++++----- .../test/generateRoutes.spec.ts | 14 ++--- .../ts/hilla-file-router/test/react.spec.tsx | 4 +- packages/ts/hilla-file-router/test/utils.ts | 51 +++++++++++++++++++ 10 files changed, 146 insertions(+), 79 deletions(-) diff --git a/packages/ts/hilla-file-router/src/generateJson.ts b/packages/ts/hilla-file-router/src/generateJson.ts index 5697536245..e6d2078dec 100644 --- a/packages/ts/hilla-file-router/src/generateJson.ts +++ b/packages/ts/hilla-file-router/src/generateJson.ts @@ -1,10 +1,12 @@ import type { RouteMeta } from './collectRoutes.js'; +import type { ViewConfig } from './react.js'; +import { processPattern } from './utils.js'; function* traverse( views: RouteMeta, - parents: readonly string[] = [], -): Generator<readonly string[], undefined, undefined> { - const chain = [...parents, views.path]; + parents: readonly RouteMeta[] = [], +): Generator<readonly RouteMeta[], undefined, undefined> { + const chain = [...parents, views]; if (views.children.length === 0) { yield chain; @@ -15,13 +17,30 @@ function* traverse( } } -export default function generateJson(views: RouteMeta): string { - const paths: string[] = []; +type RouteModule = Readonly<{ + default: unknown; + meta?: ViewConfig; +}>; - for (const branch of traverse(views)) { - const path = branch.join('/'); - paths.push(path ? path : '/'); - } +export default async function generateJson(views: RouteMeta): Promise<string> { + const res = await Promise.all( + Array.from(traverse(views), async (branch) => { + const configs = await Promise.all( + branch + .filter(({ file, layout }) => !!file || !!layout) + .map(({ file, layout }) => (file ? file : layout!).toString()) + .map(async (path) => { + const { meta }: RouteModule = await import(`${path.substring(0, path.lastIndexOf('.'))}.js`); + return meta; + }), + ); + + const key = branch.map(({ path }) => processPattern(path)).join('/'); + const value = configs[configs.length - 1]; + + return [key, value] satisfies readonly [string, ViewConfig | undefined]; + }), + ); - return JSON.stringify(paths, null, 2); + return JSON.stringify(Object.fromEntries(res)); } diff --git a/packages/ts/hilla-file-router/src/generateRoutes.ts b/packages/ts/hilla-file-router/src/generateRoutes.ts index b5e57c6f56..0a618a6de1 100644 --- a/packages/ts/hilla-file-router/src/generateRoutes.ts +++ b/packages/ts/hilla-file-router/src/generateRoutes.ts @@ -1,4 +1,4 @@ -import { relative } from 'node:path'; +import { extname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { template, transform as transformer } from '@vaadin/hilla-generator-utils/ast.js'; import createSourceFile from '@vaadin/hilla-generator-utils/createSourceFile.js'; @@ -13,8 +13,8 @@ import { processPattern, transformRoute } from './utils.js'; const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); -function relativize(url: URL, outDir: URL): string { - const result = relative(fileURLToPath(outDir), fileURLToPath(url)); +function relativize(url: URL, generatedDir: URL): string { + const result = relative(fileURLToPath(generatedDir), fileURLToPath(url)); if (!result.startsWith('.')) { return `./${result}`; @@ -23,19 +23,20 @@ function relativize(url: URL, outDir: URL): string { return result; } -function createImport(component: string, file: string): ImportDeclaration { - return template(`import ${component} from '${file}';\n`, ([statement]) => statement as ts.ImportDeclaration); +function createImport(mod: string, file: string): ImportDeclaration { + const path = `${file.substring(0, file.lastIndexOf('.'))}.js`; + return template(`import * as ${mod} from '${path}';\n`, ([statement]) => statement as ts.ImportDeclaration); } function createRouteData( path: string, - component: string | undefined, + mod: string | undefined, children: readonly ObjectLiteralExpression[], ): ObjectLiteralExpression { return template( `const route = { path: '${path}', - ${component ? `component: ${component}` : ''} + ${mod ? `module: ${mod}` : ''} ${children.length > 0 ? `children: CHILDREN,` : ''} }`, ([statement]) => @@ -48,7 +49,7 @@ function createRouteData( ); } -export default function generateRoutes(views: RouteMeta, outDir: URL): string { +export default function generateRoutes(views: RouteMeta, generatedDir: URL): string { const imports: ImportDeclaration[] = []; let id = 0; @@ -59,16 +60,16 @@ export default function generateRoutes(views: RouteMeta, outDir: URL): string { const currentId = id; id += 1; - let component: string | undefined; + let mod: string | undefined; if (file) { - component = `Page${currentId}`; - imports.push(createImport(component, relativize(file, outDir))); + mod = `Page${currentId}`; + imports.push(createImport(mod, relativize(file, generatedDir))); } else if (layout) { - component = `Layout${currentId}`; - imports.push(createImport(component, relativize(layout, outDir))); + mod = `Layout${currentId}`; + imports.push(createImport(mod, relativize(layout, generatedDir))); } - return createRouteData(processPattern(path), component, children); + return createRouteData(processPattern(path), mod, children); }, ); diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/react.ts index d628be2d3c..66aca2b909 100644 --- a/packages/ts/hilla-file-router/src/react.ts +++ b/packages/ts/hilla-file-router/src/react.ts @@ -59,25 +59,26 @@ export type ViewConfig = Readonly<{ }>; }>; -export type RouteComponent<P = object> = ComponentType<P> & { +export type RouteModule<P = object> = Readonly<{ + default: ComponentType<P>; meta?: ViewConfig; -}; +}>; /** * Transforms generated routes into a format that can be used by React Router. * * @param routes - Generated routes */ -export function toReactRouter(routes: AgnosticRoute<RouteComponent>): RouteObject { +export function toReactRouter(routes: AgnosticRoute<RouteModule>): RouteObject { return transformRoute( routes, (route) => route.children?.values(), - ({ path, component }, children) => + ({ path, module }, children) => ({ path, - element: component ? createElement(component) : undefined, + element: module?.default ? createElement(module.default) : undefined, children: children.length > 0 ? (children as RouteObject[]) : undefined, - handle: component ? component.meta : undefined, + handle: module?.meta, }) satisfies RouteObject, ); } diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/utils.ts index aae87c70ce..affa81371e 100644 --- a/packages/ts/hilla-file-router/src/utils.ts +++ b/packages/ts/hilla-file-router/src/utils.ts @@ -1,6 +1,6 @@ export type AgnosticRoute<T> = Readonly<{ path: string; - component?: T; + module?: T; children?: ReadonlyArray<AgnosticRoute<T>>; }>; diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts index b0d787f8c4..39e327b117 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -1,5 +1,5 @@ import { writeFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin } from 'vite'; import collectRoutes from './collectRoutes.js'; import generateJson from './generateJson.js'; @@ -44,7 +44,7 @@ async function build( ): Promise<void> { const routeMeta = await collectRoutes(viewsDir, { extensions }); const code = generateRoutes(routeMeta, outDir); - const json = generateJson(routeMeta); + const json = await generateJson(routeMeta); await generate(code, json, generatedUrls); } @@ -68,17 +68,17 @@ export default function vitePluginFileSystemRouter({ return { name: 'vite-plugin-file-router', configResolved({ root, build: { outDir } }) { - const _root = new URL(root); + const _root = pathToFileURL(root); _viewsDir = new URL(viewsDir, _root); _generatedDir = new URL(generatedDir, _root); - _outDir = new URL(outDir, _root); + _outDir = pathToFileURL(outDir); generatedUrls = { json: new URL('views.json', _outDir), code: new URL('views.ts', _generatedDir), }; }, async buildStart() { - await build(_viewsDir, _outDir, generatedUrls, extensions); + await build(_viewsDir, _generatedDir, generatedUrls, extensions); }, configureServer(server) { const dir = fileURLToPath(_viewsDir); diff --git a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts index 707e599ff2..83b34a184d 100644 --- a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts @@ -6,7 +6,7 @@ import { expect, use } from '@esm-bundle/chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { rimraf } from 'rimraf'; import collectRoutes from '../src/collectRoutes.js'; -import { createTestingRouteMeta } from './utils.js'; +import { createTestingRouteFiles, createTestingRouteMeta, createTmpDir } from './utils.js'; use(deepEqualInAnyOrder); @@ -16,7 +16,8 @@ describe('@vaadin/hilla-file-router', () => { let tmp: URL; before(async () => { - tmp = pathToFileURL(`${await mkdtemp(join(tmpdir(), 'hilla-file-router-'))}/`); + tmp = await createTmpDir(); + await createTestingRouteFiles(tmp); }); after(async () => { @@ -42,24 +43,6 @@ describe('@vaadin/hilla-file-router', () => { // │ ├── index.tsx // │ └── layout.tsx // └── about.tsx - - await Promise.all([ - mkdir(new URL('profile/account/security/', tmp), { recursive: true }), - mkdir(new URL('profile/friends/', tmp), { recursive: true }), - ]); - await Promise.all([ - appendFile(new URL('profile/account/account.layout.tsx', tmp), ''), - appendFile(new URL('profile/account/security/password.jsx', tmp), ''), - appendFile(new URL('profile/account/security/password.scss', tmp), ''), - appendFile(new URL('profile/account/security/two-factor-auth.ts', tmp), ''), - appendFile(new URL('profile/friends/friends.layout.tsx', tmp), ''), - appendFile(new URL('profile/friends/list.js', tmp), ''), - appendFile(new URL('profile/friends/{user}.tsx', tmp), ''), - appendFile(new URL('profile/index.tsx', tmp), ''), - appendFile(new URL('profile/index.css', tmp), ''), - appendFile(new URL('about.tsx', tmp), ''), - ]); - const result = await collectRoutes(tmp, { extensions }); expect(result).to.deep.equals(createTestingRouteMeta(tmp)); diff --git a/packages/ts/hilla-file-router/test/generateJson.spec.ts b/packages/ts/hilla-file-router/test/generateJson.spec.ts index 07f372af00..fb4d2fe263 100644 --- a/packages/ts/hilla-file-router/test/generateJson.spec.ts +++ b/packages/ts/hilla-file-router/test/generateJson.spec.ts @@ -1,31 +1,43 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { expect } from '@esm-bundle/chai'; +import { rimraf } from 'rimraf'; import type { RouteMeta } from '../src/collectRoutes.js'; import generateJson from '../src/generateJson.js'; -import { createTestingRouteMeta } from './utils.js'; +import { createTestingRouteFiles, createTestingRouteMeta, createTmpDir } from './utils.js'; describe('@vaadin/hilla-file-router', () => { describe('generateJson', () => { + let tmp: URL; let meta: RouteMeta; + before(async () => { + tmp = await createTmpDir(); + await createTestingRouteFiles(tmp); + }); + + after(async () => { + await rimraf(fileURLToPath(tmp)); + }); + beforeEach(() => { - const dir = pathToFileURL(join(tmpdir(), 'hilla-file-router/')); - meta = createTestingRouteMeta(new URL('./views/', dir)); + meta = createTestingRouteMeta(tmp); }); - it('should generate a JSON representation of the route tree', () => { - const generated = generateJson(meta); + it('should generate a JSON representation of the route tree', async () => { + const generated = await generateJson(meta); - expect(generated).to.equal(`[ - "/profile/", - "/profile/friends/list", - "/profile/friends/{user}", - "/profile/account/security/password", - "/profile/account/security/two-factor-auth", - "/about" -]`); + expect(generated).to.equal( + JSON.stringify({ + '/about': { title: 'About' }, + '/profile/': { title: 'Profile' }, + '/profile/account/security/password': { title: 'Password' }, + '/profile/account/security/two-factor-auth': { title: 'Two-Factor Auth' }, + '/profile/friends/list': { title: 'List' }, + '/profile/friends/:user': { title: 'User' }, + }), + ); }); }); }); diff --git a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts index d9f47e3f62..0139161f46 100644 --- a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts @@ -19,14 +19,14 @@ describe('@vaadin/hilla-file-router', () => { it('should generate a framework-agnostic tree of routes', () => { const generated = generateRoutes(meta, new URL('./out/', dir)); - expect(generated).to.equal(`import Page0 from "../views/about.tsx"; -import Page1 from "../views/profile/index.tsx"; -import Page2 from "../views/profile/account/security/password.jsx"; -import Page3 from "../views/profile/account/security/two-factor-auth.ts"; -import Layout5 from "../views/profile/account/account.layout.tsx"; + expect(generated).to.equal(`import Page0 from "../views/about.js"; +import Page1 from "../views/profile/index.js"; +import Page2 from "../views/profile/account/security/password.js"; +import Page3 from "../views/profile/account/security/two-factor-auth.js"; +import Layout5 from "../views/profile/account/account.layout.js"; import Page6 from "../views/profile/friends/list.js"; -import Page7 from "../views/profile/friends/{user}.tsx"; -import Layout8 from "../views/profile/friends/friends.layout.tsx"; +import Page7 from "../views/profile/friends/{user}.js"; +import Layout8 from "../views/profile/friends/friends.layout.js"; const routes = { path: "", children: [{ diff --git a/packages/ts/hilla-file-router/test/react.spec.tsx b/packages/ts/hilla-file-router/test/react.spec.tsx index 152a447c78..2c9d805b9a 100644 --- a/packages/ts/hilla-file-router/test/react.spec.tsx +++ b/packages/ts/hilla-file-router/test/react.spec.tsx @@ -1,7 +1,7 @@ import { expect, use } from '@esm-bundle/chai'; import chaiLike from 'chai-like'; import type { JSX } from 'react'; -import { type RouteComponent, toReactRouter } from '../src/react.js'; +import { type RouteModule, toReactRouter } from '../src/react.js'; import type { AgnosticRoute } from '../utils.js'; use(chaiLike); @@ -60,7 +60,7 @@ describe('@vaadin/hilla-file-router', () => { ], }, ], - } satisfies AgnosticRoute<RouteComponent>; + } satisfies AgnosticRoute<RouteModule>; const result = toReactRouter(routes); diff --git a/packages/ts/hilla-file-router/test/utils.ts b/packages/ts/hilla-file-router/test/utils.ts index 642015205e..7e810f31eb 100644 --- a/packages/ts/hilla-file-router/test/utils.ts +++ b/packages/ts/hilla-file-router/test/utils.ts @@ -1,5 +1,56 @@ +import { appendFile, mkdir, mkdtemp } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; import type { RouteMeta } from '../src/collectRoutes.js'; +export async function createTmpDir(): Promise<URL> { + return pathToFileURL(`${await mkdtemp(join(tmpdir(), 'hilla-file-router-'))}/`); +} + +export async function createTestingRouteFiles(dir: URL): Promise<void> { + await Promise.all([ + mkdir(new URL('profile/account/security/', dir), { recursive: true }), + mkdir(new URL('profile/friends/', dir), { recursive: true }), + ]); + await Promise.all([ + appendFile( + new URL('profile/account/account.layout.tsx', dir), + "export const meta = { title: 'Account' };\nexport default function AccountLayout() {};", + ), + appendFile( + new URL('profile/account/security/password.jsx', dir), + "export const meta = { title: 'Password' };\nexport default function Password() {};", + ), + appendFile(new URL('profile/account/security/password.scss', dir), ''), + appendFile( + new URL('profile/account/security/two-factor-auth.ts', dir), + "export const meta = { title: 'Two-Factor Auth' };\nexport default function TwoFactorAuth() {};", + ), + appendFile( + new URL('profile/friends/friends.layout.tsx', dir), + "export const meta = { title: 'Friends Layout' };\nexport default function FriendsLayout() {};", + ), + appendFile( + new URL('profile/friends/list.js', dir), + "export const meta = { title: 'List' };\nexport default function List() {};", + ), + appendFile( + new URL('profile/friends/{user}.tsx', dir), + "export const meta = { title: 'User' };\nexport default function User() {};", + ), + appendFile( + new URL('profile/index.tsx', dir), + "export const meta = { title: 'Profile' };\nexport default function Profile() {};", + ), + appendFile(new URL('profile/index.css', dir), ''), + appendFile( + new URL('about.tsx', dir), + "export const meta = { title: 'About' };\nexport default function About() {};", + ), + ]); +} + export function createTestingRouteMeta(dir: URL): RouteMeta { return { path: '', From 46bd2bf4f230bbf17f0f0d088a3022ccbb7e622d Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Wed, 31 Jan 2024 16:29:11 +0200 Subject: [PATCH 07/18] refactor(file-router): get route title from component if there is no config title. --- .../ts/hilla-file-router/src/generateJson.ts | 9 +-- packages/ts/hilla-file-router/src/react.ts | 62 +--------------- packages/ts/hilla-file-router/src/utils.ts | 73 +++++++++++++++++++ 3 files changed, 80 insertions(+), 64 deletions(-) diff --git a/packages/ts/hilla-file-router/src/generateJson.ts b/packages/ts/hilla-file-router/src/generateJson.ts index e6d2078dec..6113213ae6 100644 --- a/packages/ts/hilla-file-router/src/generateJson.ts +++ b/packages/ts/hilla-file-router/src/generateJson.ts @@ -1,6 +1,5 @@ import type { RouteMeta } from './collectRoutes.js'; -import type { ViewConfig } from './react.js'; -import { processPattern } from './utils.js'; +import { processPattern, prepareConfig, type ViewConfig } from './utils.js'; function* traverse( views: RouteMeta, @@ -19,7 +18,7 @@ function* traverse( type RouteModule = Readonly<{ default: unknown; - meta?: ViewConfig; + config?: ViewConfig; }>; export default async function generateJson(views: RouteMeta): Promise<string> { @@ -30,8 +29,8 @@ export default async function generateJson(views: RouteMeta): Promise<string> { .filter(({ file, layout }) => !!file || !!layout) .map(({ file, layout }) => (file ? file : layout!).toString()) .map(async (path) => { - const { meta }: RouteModule = await import(`${path.substring(0, path.lastIndexOf('.'))}.js`); - return meta; + const { config, default: fn }: RouteModule = await import(`${path.substring(0, path.lastIndexOf('.'))}.js`); + return prepareConfig(config, fn); }), ); diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/react.ts index 66aca2b909..e58102b14d 100644 --- a/packages/ts/hilla-file-router/src/react.ts +++ b/packages/ts/hilla-file-router/src/react.ts @@ -1,67 +1,11 @@ import type { UIMatch } from '@remix-run/router'; import { type ComponentType, createElement } from 'react'; import { type RouteObject, useMatches } from 'react-router'; -import { type AgnosticRoute, transformRoute } from './utils.js'; - -export type ViewConfig = Readonly<{ - /** - * View title used in the main layout header, as <title> and as the default - * for the menu entry. If not defined, then the view function name is converted - * from CamelCase after removing any "View" postfix. - */ - title?: string; - - /** - * Same as in the explicit React Router configuration. - */ - rolesAllowed?: string[]; - - /** - * Allows overriding the route path configuration. Uses the same syntax as - * the path property with React Router.This can be used to define a route - * that conflicts with the file name conventions, e.g. /foo/index - */ - route?: string; - - /** - * Controls whether the view implementation will be lazy loaded the first time - * it's used or always included in the bundle. If set to undefined (which is - * the default), views mapped to / and /login will be eager and any other view - * will be lazy (this is in sync with defaults in Flow) - */ - lazy?: boolean; - - /** - * If set to false, then the route will not be registered with React Router, - * but it will still be included in the main menu and used to configure - * Spring Security - */ - register?: boolean; - - menu?: Readonly<{ - /** - * Title to use in the menu. Falls back the title property of the view - * itself if not defined. - */ - title?: string; - - /** - * Used to determine the order in the menu. Ties are resolved based on the - * used title. Entries without explicitly defined ordering are put below - * entries with an order. - */ - priority?: number; - /** - * Set to true to explicitly exclude a view from the automatically - * populated menu. - */ - exclude?: boolean; - }>; -}>; +import { type AgnosticRoute, transformRoute, prepareConfig, type ViewConfig } from './utils.js'; export type RouteModule<P = object> = Readonly<{ default: ComponentType<P>; - meta?: ViewConfig; + config?: ViewConfig; }>; /** @@ -78,7 +22,7 @@ export function toReactRouter(routes: AgnosticRoute<RouteModule>): RouteObject { path, element: module?.default ? createElement(module.default) : undefined, children: children.length > 0 ? (children as RouteObject[]) : undefined, - handle: module?.meta, + handle: prepareConfig(module?.config, module?.default), }) satisfies RouteObject, ); } diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/utils.ts index affa81371e..67057f3f8f 100644 --- a/packages/ts/hilla-file-router/src/utils.ts +++ b/packages/ts/hilla-file-router/src/utils.ts @@ -1,3 +1,59 @@ +export type ViewConfig = Readonly<{ + /** + * View title used in the main layout header, as <title> and as the default + * for the menu entry. If not defined, then the view function name is converted + * from CamelCase after removing any "View" postfix. + */ + title?: string; + + /** + * Same as in the explicit React Router configuration. + */ + rolesAllowed?: string[]; + + /** + * Allows overriding the route path configuration. Uses the same syntax as + * the path property with React Router.This can be used to define a route + * that conflicts with the file name conventions, e.g. /foo/index + */ + route?: string; + + /** + * Controls whether the view implementation will be lazy loaded the first time + * it's used or always included in the bundle. If set to undefined (which is + * the default), views mapped to / and /login will be eager and any other view + * will be lazy (this is in sync with defaults in Flow) + */ + lazy?: boolean; + + /** + * If set to false, then the route will not be registered with React Router, + * but it will still be included in the main menu and used to configure + * Spring Security + */ + register?: boolean; + + menu?: Readonly<{ + /** + * Title to use in the menu. Falls back the title property of the view + * itself if not defined. + */ + title?: string; + + /** + * Used to determine the order in the menu. Ties are resolved based on the + * used title. Entries without explicitly defined ordering are put below + * entries with an order. + */ + priority?: number; + /** + * Set to true to explicitly exclude a view from the automatically + * populated menu. + */ + exclude?: boolean; + }>; +}>; + export type AgnosticRoute<T> = Readonly<{ path: string; module?: T; @@ -23,3 +79,20 @@ export function transformRoute<T, U>( children ? Array.from(children, (child) => transformRoute(child, getChildren, transformer)) : [], ); } + +const viewPattern = /view/giu; + +export function prepareConfig(config?: ViewConfig, component?: unknown): ViewConfig | undefined { + if (config?.title) { + return config; + } + + if (component && typeof component === 'object' && 'name' in component && typeof component.name === 'string') { + return { + ...config, + title: component.name.replace(viewPattern, ''), + }; + } + + return config; +} From 351a60a39be08186e668c7d02d43ab9aa626e565 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Thu, 1 Feb 2024 11:44:20 +0200 Subject: [PATCH 08/18] refactor(file-router): improve file watching & fix title replacement --- packages/ts/hilla-file-router/src/utils.ts | 10 +++++++-- .../src/vite-plugin-file-router.ts | 8 +++++-- .../test/generateJson.spec.ts | 2 +- packages/ts/hilla-file-router/test/utils.ts | 22 +++++++------------ 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/utils.ts index 67057f3f8f..fdd2fe440c 100644 --- a/packages/ts/hilla-file-router/src/utils.ts +++ b/packages/ts/hilla-file-router/src/utils.ts @@ -81,16 +81,22 @@ export function transformRoute<T, U>( } const viewPattern = /view/giu; +const upperCaseSplitPattern = /(?=[A-Z])/gu; export function prepareConfig(config?: ViewConfig, component?: unknown): ViewConfig | undefined { if (config?.title) { return config; } - if (component && typeof component === 'object' && 'name' in component && typeof component.name === 'string') { + if ( + component && + (typeof component === 'object' || typeof component === 'function') && + 'name' in component && + typeof component.name === 'string' + ) { return { ...config, - title: component.name.replace(viewPattern, ''), + title: component.name.replace(viewPattern, '').split(upperCaseSplitPattern).join(' '), }; } diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts index 39e327b117..014dfc132a 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -83,13 +83,17 @@ export default function vitePluginFileSystemRouter({ configureServer(server) { const dir = fileURLToPath(_viewsDir); - server.watcher.on('unlink', (file) => { + const changeListener = (file: string): void => { if (!file.startsWith(dir)) { return; } build(_viewsDir, _outDir, generatedUrls, extensions).catch((error) => console.error(error)); - }); + }; + + server.watcher.on('add', changeListener); + server.watcher.on('change', changeListener); + server.watcher.on('unlink', changeListener); }, }; } diff --git a/packages/ts/hilla-file-router/test/generateJson.spec.ts b/packages/ts/hilla-file-router/test/generateJson.spec.ts index fb4d2fe263..10f64b07d8 100644 --- a/packages/ts/hilla-file-router/test/generateJson.spec.ts +++ b/packages/ts/hilla-file-router/test/generateJson.spec.ts @@ -33,7 +33,7 @@ describe('@vaadin/hilla-file-router', () => { '/about': { title: 'About' }, '/profile/': { title: 'Profile' }, '/profile/account/security/password': { title: 'Password' }, - '/profile/account/security/two-factor-auth': { title: 'Two-Factor Auth' }, + '/profile/account/security/two-factor-auth': { title: 'Two Factor Auth' }, '/profile/friends/list': { title: 'List' }, '/profile/friends/:user': { title: 'User' }, }), diff --git a/packages/ts/hilla-file-router/test/utils.ts b/packages/ts/hilla-file-router/test/utils.ts index 7e810f31eb..9765b42597 100644 --- a/packages/ts/hilla-file-router/test/utils.ts +++ b/packages/ts/hilla-file-router/test/utils.ts @@ -16,37 +16,31 @@ export async function createTestingRouteFiles(dir: URL): Promise<void> { await Promise.all([ appendFile( new URL('profile/account/account.layout.tsx', dir), - "export const meta = { title: 'Account' };\nexport default function AccountLayout() {};", - ), - appendFile( - new URL('profile/account/security/password.jsx', dir), - "export const meta = { title: 'Password' };\nexport default function Password() {};", + "export const config = { title: 'Account' };\nexport default function AccountLayout() {};", ), + appendFile(new URL('profile/account/security/password.jsx', dir), 'export default function Password() {};'), appendFile(new URL('profile/account/security/password.scss', dir), ''), appendFile( new URL('profile/account/security/two-factor-auth.ts', dir), - "export const meta = { title: 'Two-Factor Auth' };\nexport default function TwoFactorAuth() {};", - ), - appendFile( - new URL('profile/friends/friends.layout.tsx', dir), - "export const meta = { title: 'Friends Layout' };\nexport default function FriendsLayout() {};", + 'export default function TwoFactorAuth() {};', ), + appendFile(new URL('profile/friends/friends.layout.tsx', dir), 'export default function FriendsLayout() {};'), appendFile( new URL('profile/friends/list.js', dir), - "export const meta = { title: 'List' };\nexport default function List() {};", + "export const config = { title: 'List' };\nexport default function List() {};", ), appendFile( new URL('profile/friends/{user}.tsx', dir), - "export const meta = { title: 'User' };\nexport default function User() {};", + "export const config = { title: 'User' };\nexport default function User() {};", ), appendFile( new URL('profile/index.tsx', dir), - "export const meta = { title: 'Profile' };\nexport default function Profile() {};", + "export const config = { title: 'Profile' };\nexport default function Profile() {};", ), appendFile(new URL('profile/index.css', dir), ''), appendFile( new URL('about.tsx', dir), - "export const meta = { title: 'About' };\nexport default function About() {};", + "export const config = { title: 'About' };\nexport default function About() {};", ), ]); } From e93eb5dc5ab67a8c190cc140b7e8062a42f5af11 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Thu, 1 Feb 2024 12:35:20 +0200 Subject: [PATCH 09/18] test(file-router): update test implementation --- .../ts/hilla-file-router/test/react.spec.tsx | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/ts/hilla-file-router/test/react.spec.tsx b/packages/ts/hilla-file-router/test/react.spec.tsx index 2c9d805b9a..183ae716c6 100644 --- a/packages/ts/hilla-file-router/test/react.spec.tsx +++ b/packages/ts/hilla-file-router/test/react.spec.tsx @@ -2,7 +2,7 @@ import { expect, use } from '@esm-bundle/chai'; import chaiLike from 'chai-like'; import type { JSX } from 'react'; import { type RouteModule, toReactRouter } from '../src/react.js'; -import type { AgnosticRoute } from '../utils.js'; +import type { AgnosticRoute } from '../src/utils.js'; use(chaiLike); @@ -12,55 +12,67 @@ describe('@vaadin/hilla-file-router', () => { return <></>; } - About.meta = { title: 'About' }; + About.config = { title: 'About' }; function Friends(): JSX.Element { return <></>; } - Friends.meta = { title: 'Friends' }; + Friends.config = { title: 'Friends' }; function FriendsList(): JSX.Element { return <></>; } - FriendsList.meta = { title: 'Friends List' }; + FriendsList.config = { title: 'Friends List' }; function Friend(): JSX.Element { return <></>; } - Friend.meta = { title: 'Friend' }; + Friend.config = { title: 'Friend' }; it('should be able to convert an agnostic routes to React Router routes', () => { - const routes = { + const routes: AgnosticRoute<RouteModule> = { path: '', children: [ { path: 'about', - component: About, + module: { + default: About, + config: About.config, + }, }, { path: 'profile', children: [ { path: 'friends', - component: Friends, + module: { + default: Friends, + config: Friends.config, + }, children: [ { path: 'list', - component: FriendsList, + module: { + default: FriendsList, + config: FriendsList.config, + }, }, { path: '{user}', - component: Friend, + module: { + default: Friend, + config: Friend.config, + }, }, ], }, ], }, ], - } satisfies AgnosticRoute<RouteModule>; + }; const result = toReactRouter(routes); @@ -70,7 +82,7 @@ describe('@vaadin/hilla-file-router', () => { { path: 'about', element: <About />, - handle: About.meta, + handle: About.config, }, { path: 'profile', @@ -78,17 +90,17 @@ describe('@vaadin/hilla-file-router', () => { { path: 'friends', element: <Friends />, - handle: Friends.meta, + handle: Friends.config, children: [ { path: 'list', element: <FriendsList />, - handle: FriendsList.meta, + handle: FriendsList.config, }, { path: '{user}', element: <Friend />, - handle: Friend.meta, + handle: Friend.config, }, ], }, From 8fa9be882fb483ee89814d4eeb6748d8daf10617 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Thu, 1 Feb 2024 12:56:32 +0200 Subject: [PATCH 10/18] test(file-router): fix test execution --- packages/ts/hilla-file-router/package.json | 5 ++- .../test/collectRoutes.spec.ts | 9 +----- .../test/generateRoutes.spec.ts | 32 +++++++++---------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/packages/ts/hilla-file-router/package.json b/packages/ts/hilla-file-router/package.json index 8cb24960f1..dcdbc224a1 100644 --- a/packages/ts/hilla-file-router/package.json +++ b/packages/ts/hilla-file-router/package.json @@ -26,9 +26,8 @@ "build:copy": "cd src && copyfiles **/*.d.ts ..", "lint": "eslint src test", "lint:fix": "eslint src test --fix", - "test": "karma start ../../../karma.config.cjs --port 9878", - "test:coverage": "npm run test -- --coverage", - "test:watch": "npm run test -- --watch", + "test": "mocha test/**/*.spec.ts --config ../../../.mocharc.cjs", + "test:coverage": "c8 -c ../../../.c8rc.json npm test", "typecheck": "tsc --noEmit" }, "exports": { diff --git a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts index 83b34a184d..90376a4367 100644 --- a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/collectRoutes.spec.ts @@ -1,7 +1,4 @@ -import { appendFile, mkdir, mkdtemp } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { fileURLToPath } from 'node:url'; import { expect, use } from '@esm-bundle/chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { rimraf } from 'rimraf'; @@ -24,10 +21,6 @@ describe('@vaadin/hilla-file-router', () => { await rimraf(fileURLToPath(tmp)); }); - beforeEach(async () => { - await rimraf(fileURLToPath(new URL('*', tmp)), { glob: true }); - }); - it('should build a route tree', async () => { // root // ├── profile diff --git a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts index 0139161f46..1d32e0fde4 100644 --- a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts @@ -19,46 +19,46 @@ describe('@vaadin/hilla-file-router', () => { it('should generate a framework-agnostic tree of routes', () => { const generated = generateRoutes(meta, new URL('./out/', dir)); - expect(generated).to.equal(`import Page0 from "../views/about.js"; -import Page1 from "../views/profile/index.js"; -import Page2 from "../views/profile/account/security/password.js"; -import Page3 from "../views/profile/account/security/two-factor-auth.js"; -import Layout5 from "../views/profile/account/account.layout.js"; -import Page6 from "../views/profile/friends/list.js"; -import Page7 from "../views/profile/friends/{user}.js"; -import Layout8 from "../views/profile/friends/friends.layout.js"; + expect(generated).to.equal(`import * as Page0 from "../views/about.js"; +import * as Page1 from "../views/profile/index.js"; +import * as Page2 from "../views/profile/account/security/password.js"; +import * as Page3 from "../views/profile/account/security/two-factor-auth.js"; +import * as Layout5 from "../views/profile/account/account.layout.js"; +import * as Page6 from "../views/profile/friends/list.js"; +import * as Page7 from "../views/profile/friends/{user}.js"; +import * as Layout8 from "../views/profile/friends/friends.layout.js"; const routes = { path: "", children: [{ path: "about", - component: Page0 + module: Page0 }, { path: "profile", children: [{ path: "", - component: Page1 + module: Page1 }, { path: "account", - component: Layout5, + module: Layout5, children: [{ path: "security", children: [{ path: "password", - component: Page2 + module: Page2 }, { path: "two-factor-auth", - component: Page3 + module: Page3 }], }], }, { path: "friends", - component: Layout8, + module: Layout8, children: [{ path: "list", - component: Page6 + module: Page6 }, { path: ":user", - component: Page7 + module: Page7 }], }], }], From 09d3088308abce921f06511a1b6f4f0bc9977590 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Sat, 3 Feb 2024 13:37:22 +0200 Subject: [PATCH 11/18] refactor(file-router): fixes & implementations * resolve issue with non-transpiled TS file during JSON generation. * implement adjusted file naming conventions --- .../ts/hilla-file-router/src/collectRoutes.ts | 16 ++++++-- .../ts/hilla-file-router/src/generateJson.ts | 39 ++++++++++++++++--- packages/ts/hilla-file-router/src/react.ts | 4 +- packages/ts/hilla-file-router/src/utils.ts | 26 +++++++++---- .../test/generateJson.spec.ts | 2 +- .../test/generateRoutes.spec.ts | 6 +-- packages/ts/hilla-file-router/test/utils.ts | 12 +++--- 7 files changed, 77 insertions(+), 28 deletions(-) diff --git a/packages/ts/hilla-file-router/src/collectRoutes.ts b/packages/ts/hilla-file-router/src/collectRoutes.ts index f58474cf21..b93758e544 100644 --- a/packages/ts/hilla-file-router/src/collectRoutes.ts +++ b/packages/ts/hilla-file-router/src/collectRoutes.ts @@ -38,11 +38,21 @@ export default async function collectRoutes( const file = new URL(d.name, dir); const name = basename(d.name, extname(d.name)); - if (name.includes('.layout')) { - layout = file; + if (name.startsWith('$')) { + if (name === '$layout') { + layout = file; + } else if (name === '$index') { + children.push({ + path: '', + file, + children: [], + }); + } else { + throw new Error('Symbol "$" is reserved for special files; only "$layout" and "$index" are allowed'); + } } else if (!name.startsWith('_')) { children.push({ - path: name === 'index' ? '' : name, + path: name, file, children: [], }); diff --git a/packages/ts/hilla-file-router/src/generateJson.ts b/packages/ts/hilla-file-router/src/generateJson.ts index 6113213ae6..7db89763af 100644 --- a/packages/ts/hilla-file-router/src/generateJson.ts +++ b/packages/ts/hilla-file-router/src/generateJson.ts @@ -1,5 +1,8 @@ +import { readFile } from 'node:fs/promises'; +import { Script } from 'node:vm'; +import ts, { type Node } from 'typescript'; import type { RouteMeta } from './collectRoutes.js'; -import { processPattern, prepareConfig, type ViewConfig } from './utils.js'; +import { prepareConfig, processPattern, type ViewConfig } from './utils.js'; function* traverse( views: RouteMeta, @@ -21,16 +24,42 @@ type RouteModule = Readonly<{ config?: ViewConfig; }>; -export default async function generateJson(views: RouteMeta): Promise<string> { +function* walkAST(node: Node): Generator<Node> { + yield node; + + for (const child of node.getChildren()) { + yield* walkAST(child); + } +} + +export default async function generateJson(views: RouteMeta, exportName: string): Promise<string> { const res = await Promise.all( Array.from(traverse(views), async (branch) => { const configs = await Promise.all( branch .filter(({ file, layout }) => !!file || !!layout) - .map(({ file, layout }) => (file ? file : layout!).toString()) + .map(({ file, layout }) => file ?? layout!) .map(async (path) => { - const { config, default: fn }: RouteModule = await import(`${path.substring(0, path.lastIndexOf('.'))}.js`); - return prepareConfig(config, fn); + const file = ts.createSourceFile('f.ts', await readFile(path, 'utf8'), ts.ScriptTarget.ESNext, true); + let config: ViewConfig | undefined; + let waitingForIdentifier = false; + let componentName: string | undefined; + + for (const node of walkAST(file)) { + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === exportName) { + if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) { + const code = node.initializer.getText(file); + const script = new Script(`(${code})`); + config = script.runInThisContext() as ViewConfig; + } + } else if (node.getText(file).includes('export default')) { + waitingForIdentifier = true; + } else if (waitingForIdentifier && ts.isIdentifier(node)) { + componentName = node.text; + } + } + + return prepareConfig(config, componentName); }), ); diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/react.ts index e58102b14d..b8acab4986 100644 --- a/packages/ts/hilla-file-router/src/react.ts +++ b/packages/ts/hilla-file-router/src/react.ts @@ -1,7 +1,7 @@ import type { UIMatch } from '@remix-run/router'; import { type ComponentType, createElement } from 'react'; import { type RouteObject, useMatches } from 'react-router'; -import { type AgnosticRoute, transformRoute, prepareConfig, type ViewConfig } from './utils.js'; +import { type AgnosticRoute, transformRoute, prepareConfig, type ViewConfig, extractComponentName } from './utils.js'; export type RouteModule<P = object> = Readonly<{ default: ComponentType<P>; @@ -22,7 +22,7 @@ export function toReactRouter(routes: AgnosticRoute<RouteModule>): RouteObject { path, element: module?.default ? createElement(module.default) : undefined, children: children.length > 0 ? (children as RouteObject[]) : undefined, - handle: prepareConfig(module?.config, module?.default), + handle: prepareConfig(module?.config, extractComponentName(module?.default)), }) satisfies RouteObject, ); } diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/utils.ts index fdd2fe440c..d72ed922e5 100644 --- a/packages/ts/hilla-file-router/src/utils.ts +++ b/packages/ts/hilla-file-router/src/utils.ts @@ -1,3 +1,5 @@ +import { Script } from 'node:vm'; + export type ViewConfig = Readonly<{ /** * View title used in the main layout header, as <title> and as the default @@ -80,23 +82,31 @@ export function transformRoute<T, U>( ); } +export function extractComponentName(component?: unknown): string | undefined { + if ( + component && + (typeof component === 'object' || typeof component === 'function') && + 'name' in component && + typeof component.name === 'string' + ) { + return component.name; + } + + return undefined; +} + const viewPattern = /view/giu; const upperCaseSplitPattern = /(?=[A-Z])/gu; -export function prepareConfig(config?: ViewConfig, component?: unknown): ViewConfig | undefined { +export function prepareConfig(config?: ViewConfig, componentName?: string): ViewConfig | undefined { if (config?.title) { return config; } - if ( - component && - (typeof component === 'object' || typeof component === 'function') && - 'name' in component && - typeof component.name === 'string' - ) { + if (componentName) { return { ...config, - title: component.name.replace(viewPattern, '').split(upperCaseSplitPattern).join(' '), + title: componentName.replace(viewPattern, '').split(upperCaseSplitPattern).join(' '), }; } diff --git a/packages/ts/hilla-file-router/test/generateJson.spec.ts b/packages/ts/hilla-file-router/test/generateJson.spec.ts index 10f64b07d8..001fbb3903 100644 --- a/packages/ts/hilla-file-router/test/generateJson.spec.ts +++ b/packages/ts/hilla-file-router/test/generateJson.spec.ts @@ -26,7 +26,7 @@ describe('@vaadin/hilla-file-router', () => { }); it('should generate a JSON representation of the route tree', async () => { - const generated = await generateJson(meta); + const generated = await generateJson(meta, 'config'); expect(generated).to.equal( JSON.stringify({ diff --git a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts index 1d32e0fde4..68e963e7ab 100644 --- a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/generateRoutes.spec.ts @@ -20,13 +20,13 @@ describe('@vaadin/hilla-file-router', () => { const generated = generateRoutes(meta, new URL('./out/', dir)); expect(generated).to.equal(`import * as Page0 from "../views/about.js"; -import * as Page1 from "../views/profile/index.js"; +import * as Page1 from "../views/profile/$index.js"; import * as Page2 from "../views/profile/account/security/password.js"; import * as Page3 from "../views/profile/account/security/two-factor-auth.js"; -import * as Layout5 from "../views/profile/account/account.layout.js"; +import * as Layout5 from "../views/profile/account/$layout.js"; import * as Page6 from "../views/profile/friends/list.js"; import * as Page7 from "../views/profile/friends/{user}.js"; -import * as Layout8 from "../views/profile/friends/friends.layout.js"; +import * as Layout8 from "../views/profile/friends/$layout.js"; const routes = { path: "", children: [{ diff --git a/packages/ts/hilla-file-router/test/utils.ts b/packages/ts/hilla-file-router/test/utils.ts index 9765b42597..cd8044b591 100644 --- a/packages/ts/hilla-file-router/test/utils.ts +++ b/packages/ts/hilla-file-router/test/utils.ts @@ -15,7 +15,7 @@ export async function createTestingRouteFiles(dir: URL): Promise<void> { ]); await Promise.all([ appendFile( - new URL('profile/account/account.layout.tsx', dir), + new URL('profile/account/$layout.tsx', dir), "export const config = { title: 'Account' };\nexport default function AccountLayout() {};", ), appendFile(new URL('profile/account/security/password.jsx', dir), 'export default function Password() {};'), @@ -24,7 +24,7 @@ export async function createTestingRouteFiles(dir: URL): Promise<void> { new URL('profile/account/security/two-factor-auth.ts', dir), 'export default function TwoFactorAuth() {};', ), - appendFile(new URL('profile/friends/friends.layout.tsx', dir), 'export default function FriendsLayout() {};'), + appendFile(new URL('profile/friends/$layout.tsx', dir), 'export default function FriendsLayout() {};'), appendFile( new URL('profile/friends/list.js', dir), "export const config = { title: 'List' };\nexport default function List() {};", @@ -34,7 +34,7 @@ export async function createTestingRouteFiles(dir: URL): Promise<void> { "export const config = { title: 'User' };\nexport default function User() {};", ), appendFile( - new URL('profile/index.tsx', dir), + new URL('profile/$index.tsx', dir), "export const config = { title: 'Profile' };\nexport default function Profile() {};", ), appendFile(new URL('profile/index.css', dir), ''), @@ -59,10 +59,10 @@ export function createTestingRouteMeta(dir: URL): RouteMeta { path: 'profile', layout: undefined, children: [ - { path: '', file: new URL('profile/index.tsx', dir), children: [] }, + { path: '', file: new URL('profile/$index.tsx', dir), children: [] }, { path: 'account', - layout: new URL('profile/account/account.layout.tsx', dir), + layout: new URL('profile/account/$layout.tsx', dir), children: [ { path: 'security', @@ -84,7 +84,7 @@ export function createTestingRouteMeta(dir: URL): RouteMeta { }, { path: 'friends', - layout: new URL('profile/friends/friends.layout.tsx', dir), + layout: new URL('profile/friends/$layout.tsx', dir), children: [ { path: 'list', From aa554da5cf0b5f2519809304eb881f5bf04c76aa Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Mon, 5 Feb 2024 11:34:58 +0200 Subject: [PATCH 12/18] fix(file-router): resolve build issue --- .../ts/hilla-file-router/src/generateJson.ts | 4 ++-- .../src/vite-plugin-file-router.ts | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/ts/hilla-file-router/src/generateJson.ts b/packages/ts/hilla-file-router/src/generateJson.ts index 7db89763af..695e6627e2 100644 --- a/packages/ts/hilla-file-router/src/generateJson.ts +++ b/packages/ts/hilla-file-router/src/generateJson.ts @@ -32,7 +32,7 @@ function* walkAST(node: Node): Generator<Node> { } } -export default async function generateJson(views: RouteMeta, exportName: string): Promise<string> { +export default async function generateJson(views: RouteMeta, configExportName: string): Promise<string> { const res = await Promise.all( Array.from(traverse(views), async (branch) => { const configs = await Promise.all( @@ -46,7 +46,7 @@ export default async function generateJson(views: RouteMeta, exportName: string) let componentName: string | undefined; for (const node of walkAST(file)) { - if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === exportName) { + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === configExportName) { if (node.initializer && ts.isObjectLiteralExpression(node.initializer)) { const code = node.initializer.getText(file); const script = new Script(`(${code})`); diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts index 014dfc132a..ecbbb98537 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -20,11 +20,19 @@ export type PluginOptions = Readonly<{ */ generatedDir?: URL | string; /** - * The list of extensions that will be collected as routes of the file-based router. + * The list of extensions that will be collected as routes of the file-based + * router. * * @defaultValue `['.tsx', '.jsx', '.ts', '.js']` */ extensions?: readonly string[]; + /** + * The name of the export that will be used for the {@link ViewConfig} in the + * route file. + * + * @defaultValue `config` + */ + configExportName?: string; }>; type GeneratedUrls = Readonly<{ @@ -41,10 +49,11 @@ async function build( outDir: URL, generatedUrls: GeneratedUrls, extensions: readonly string[], + configExportName: string, ): Promise<void> { const routeMeta = await collectRoutes(viewsDir, { extensions }); const code = generateRoutes(routeMeta, outDir); - const json = await generateJson(routeMeta); + const json = await generateJson(routeMeta, configExportName); await generate(code, json, generatedUrls); } @@ -59,6 +68,7 @@ export default function vitePluginFileSystemRouter({ viewsDir = 'frontend/views/', generatedDir = 'frontend/generated/', extensions = ['.tsx', '.jsx', '.ts', '.js'], + configExportName = 'config', }: PluginOptions = {}): Plugin { let _viewsDir: URL; let _generatedDir: URL; @@ -78,7 +88,7 @@ export default function vitePluginFileSystemRouter({ }; }, async buildStart() { - await build(_viewsDir, _generatedDir, generatedUrls, extensions); + await build(_viewsDir, _generatedDir, generatedUrls, extensions, configExportName); }, configureServer(server) { const dir = fileURLToPath(_viewsDir); @@ -88,7 +98,7 @@ export default function vitePluginFileSystemRouter({ return; } - build(_viewsDir, _outDir, generatedUrls, extensions).catch((error) => console.error(error)); + build(_viewsDir, _outDir, generatedUrls, extensions, configExportName).catch((error) => console.error(error)); }; server.watcher.on('add', changeListener); From 8810cdb8ecff6416aaa0a08741dcd5a962192a07 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Mon, 5 Feb 2024 14:29:42 +0200 Subject: [PATCH 13/18] chore(file-router): update package-lock.json --- package-lock.json | 424 +++++++++++++++++++++++++++++++++------------- 1 file changed, 307 insertions(+), 117 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca6d4c00d5..969972d96b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2913,24 +2913,24 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz", - "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" }, "node_modules/@lit/react": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.2.tgz", - "integrity": "sha512-UJ5TQ46DPcJDIzyjbwbj6Iye0XcpCxL2yb03zcWq1BpWchpXS3Z0BPVhg7zDfZLF6JemPml8u/gt/+KwJ/23sg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.3.tgz", + "integrity": "sha512-RGoPMrAPbFjQFXFbfmYdotw000DyChehTim+d562HRXvFGw//KxouI8jNOcc3Kw/1uqUA1SJqXFtKKxK0NUrww==", "peerDependencies": { "@types/react": "17 || 18" } }, "node_modules/@lit/reactive-element": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.3.tgz", - "integrity": "sha512-e067EuTNNgOHm1tZcc0Ia7TCzD/9ZpoPegHKgesrGK6pSDRGkGDAQbYuQclqLPIoJ9eC8Kb9mYtGryWcM5AywA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.2" + "@lit-labs/ssr-dom-shim": "^1.2.0" } }, "node_modules/@microsoft/tsdoc": { @@ -3444,10 +3444,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", - "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==", - "dev": true, + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", + "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==", "engines": { "node": ">=14.0.0" } @@ -3560,9 +3559,9 @@ "dev": true }, "node_modules/@testing-library/react": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.0.tgz", - "integrity": "sha512-7uBnPHyOG6nDGCzv8SLeJbSa33ZoYw7swYpSLIgJvBALdq7l9zPNk33om4USrxy1lKTxXaVfufzLmq83WNfWIw==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.1.tgz", + "integrity": "sha512-sGdjws32ai5TLerhvzThYFbpnF9XtL65Cjf+gB0Dhr29BGqK+mAeN7SURSdu+eqgET4ANcWoC7FQpkaiGvBr+A==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5", @@ -3684,6 +3683,15 @@ "@types/chai": "*" } }, + "node_modules/@types/chai-like": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/chai-like/-/chai-like-1.1.3.tgz", + "integrity": "sha512-AEGBQz8wcPhvytKR5EP3HiQrmUeg6HP/ZgNnGWnLaQA4fyZ7kDS1/wbSBLN4CBTMobK4wM2SpksVWzTXWQ8r3w==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/co-body": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", @@ -3757,6 +3765,12 @@ "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", "dev": true }, + "node_modules/@types/deep-equal-in-any-order": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/deep-equal-in-any-order/-/deep-equal-in-any-order-1.0.3.tgz", + "integrity": "sha512-jT0O3hAILDKeKbdWJ9FZLD0Xdfhz7hMvfyFlRWpirjiEVr8G+GZ4kVIzPIqM6x6Rpp93TNPgOAed4XmvcuV6Qg==", + "dev": true + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -3770,9 +3784,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.42", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz", - "integrity": "sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -3898,9 +3912,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.13.tgz", - "integrity": "sha512-5G4zQwdiQBSWYTDAH1ctw2eidqdhMJaNsiIDKHFr55ihz5Trl2qqR8fdrT732yPBho5gkNxXm67OxWFBqX9aPg==", + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3936,9 +3950,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.48", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", - "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", + "version": "18.2.53", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.53.tgz", + "integrity": "sha512-52IHsMDT8qATp9B9zoOyobW8W3/0QhaJQTw1HwRj0UY2yBpCAQ7+S/CqHYQ8niAm3p4ji+rWUQ9UCib0GxQ60w==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4708,6 +4722,10 @@ "resolved": "packages/ts/core", "link": true }, + "node_modules/@vaadin/hilla-file-router": { + "resolved": "packages/ts/hilla-file-router", + "link": true + }, "node_modules/@vaadin/hilla-generator-cli": { "resolved": "packages/ts/generator-cli", "link": true @@ -5924,13 +5942,16 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5964,6 +5985,25 @@ "node": ">=8" } }, + "node_modules/array.prototype.filter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", + "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", @@ -6020,17 +6060,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -6108,9 +6149,9 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", "dev": true, "engines": { "node": ">= 0.4" @@ -6540,9 +6581,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001581", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz", - "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==", + "version": "1.0.30001584", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001584.tgz", + "integrity": "sha512-LOz7CCQ9M1G7OjJOF9/mzmqmj3jE/7VOmrfw6Mgs0E8cjOsbRXQJHsPBfmBOXDskXKrHLyyW3n7kpDW/4BsfpQ==", "dev": true, "funding": [ { @@ -6616,6 +6657,15 @@ "chai": ">= 3" } }, + "node_modules/chai-like": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/chai-like/-/chai-like-1.1.1.tgz", + "integrity": "sha512-VKa9z/SnhXhkT1zIjtPACFWSoWsqVoaz1Vg+ecrKo5DCKVlgL30F/pEyEvXPBOVwCgLZcWUleCM/C1okaKdTTA==", + "dev": true, + "peerDependencies": { + "chai": "2 - 4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7583,6 +7633,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-equal-in-any-order": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.0.6.tgz", + "integrity": "sha512-RfnWHQzph10YrUjvWwhd15Dne8ciSJcZ3U6OD7owPwiVwsdE5IFSoZGg8rlwJD11ES+9H5y8j3fCofviRHOqLQ==", + "dev": true, + "dependencies": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, "node_modules/deep-equal/node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -7880,9 +7940,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.651", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.651.tgz", - "integrity": "sha512-jjks7Xx+4I7dslwsbaFocSwqBbGHQmuXBJUK9QBZTIrzPq3pzn6Uf2szFSP728FtLYE3ldiccmlkOM/zhGKCpA==", + "version": "1.4.656", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", + "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", "dev": true }, "node_modules/emoji-regex": { @@ -8049,6 +8109,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -9368,16 +9443,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", + "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", "dev": true, "dependencies": { + "es-errors": "^1.0.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9694,12 +9773,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -9915,9 +9994,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -10010,14 +10089,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10329,12 +10410,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -11233,29 +11314,29 @@ "dev": true }, "node_modules/lit": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.1.tgz", - "integrity": "sha512-hF1y4K58+Gqrz+aAPS0DNBwPqPrg6P04DuWK52eMkt/SM9Qe9keWLcFgRcEKOLuDlRZlDsDbNL37Vr7ew1VCuw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.2.tgz", + "integrity": "sha512-VZx5iAyMtX7CV4K8iTLdCkMaYZ7ipjJZ0JcSdJ0zIdGxxyurjIn7yuuSxNBD7QmjvcNJwr0JS4cAdAtsy7gZ6w==", "dependencies": { - "@lit/reactive-element": "^2.0.0", - "lit-element": "^4.0.0", - "lit-html": "^3.1.0" + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.0.4", + "lit-html": "^3.1.2" } }, "node_modules/lit-element": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.3.tgz", - "integrity": "sha512-2vhidmC7gGLfnVx41P8UZpzyS0Fb8wYhS5RCm16cMW3oERO0Khd3EsKwtRpOnttuByI5rURjT2dfoA7NlInCNw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.4.tgz", + "integrity": "sha512-98CvgulX6eCPs6TyAIQoJZBCQPo80rgXR+dVBs61cstJXqtI+USQZAbA4gFHh6L/mxBx9MrgPLHLsUgDUHAcCQ==", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.2", - "@lit/reactive-element": "^2.0.0", - "lit-html": "^3.1.0" + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.1.2" } }, "node_modules/lit-html": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.1.tgz", - "integrity": "sha512-x/EwfGk2D/f4odSFM40hcGumzqoKv0/SUh6fBO+1Ragez81APrcAMPo1jIrCDd9Sn+Z4CT867HWKViByvkDZUA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.2.tgz", + "integrity": "sha512-3OBZSUrPnAHoKJ9AMjRL/m01YJxQMf+TMHanNtTHG68ubjnZxK0RFl102DPzsw4mWnHibfZIBJm3LWCZ/LmMvg==", "dependencies": { "@types/trusted-types": "^2.0.2" } @@ -11311,6 +11392,12 @@ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", "dev": true }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11417,7 +11504,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -11453,9 +11539,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.6.tgz", + "integrity": "sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -12268,15 +12354,16 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz", + "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "array.prototype.filter": "^1.0.3", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0" } }, "node_modules/object.values": { @@ -12673,9 +12760,9 @@ } }, "node_modules/pino": { - "version": "8.17.2", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.17.2.tgz", - "integrity": "sha512-LA6qKgeDMLr2ux2y/YiUt47EfgQ+S9LznBWOJdN3q1dx2sv0ziDLUBeVpyVv17TEcGCBuWf0zNtg3M5m1NhhWQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.18.0.tgz", + "integrity": "sha512-Mz/gKiRyuXu4HnpHgi1YWdHQCoWMufapzooisvFn78zl4dZciAxS+YeRkUxXl1ee/SzU80YCz1zpECCh4oC6Aw==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", @@ -13305,9 +13392,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -13519,7 +13606,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13547,12 +13633,11 @@ "dev": true }, "node_modules/react-router": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz", - "integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==", - "dev": true, + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", + "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==", "dependencies": { - "@remix-run/router": "1.14.2" + "@remix-run/router": "1.15.0" }, "engines": { "node": ">=14.0.0" @@ -13562,13 +13647,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz", - "integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==", + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz", + "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==", "dev": true, "dependencies": { - "@remix-run/router": "1.14.2", - "react-router": "6.21.3" + "@remix-run/router": "1.15.0", + "react-router": "6.22.0" }, "engines": { "node": ">=14.0.0" @@ -14313,6 +14398,15 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -15626,16 +15720,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -16435,6 +16529,102 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/ts/hilla-file-router": { + "name": "@vaadin/hilla-file-router", + "version": "24.4.0-alpha1", + "license": "Apache-2.0", + "dependencies": { + "@vaadin/hilla-generator-utils": "^24.4.0-alpha1", + "react": "^18.2.0" + }, + "devDependencies": { + "@esm-bundle/chai": "^4.3.4-fix.0", + "@types/chai-like": "^1.1.3", + "@types/deep-equal-in-any-order": "^1.0.3", + "@types/mocha": "^10.0.6", + "@types/sinon": "^17.0.3", + "chai-like": "^1.1.1", + "deep-equal-in-any-order": "^2.0.6", + "mocha": "^10.2.0", + "rimraf": "^5.0.5", + "sinon": "^17.0.1", + "type-fest": "^4.9.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "react-router": "^6.21.1" + } + }, + "packages/ts/hilla-file-router/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "packages/ts/hilla-file-router/node_modules/@types/sinon": { + "version": "17.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "packages/ts/hilla-file-router/node_modules/diff": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "packages/ts/hilla-file-router/node_modules/rimraf": { + "version": "5.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/ts/hilla-file-router/node_modules/sinon": { + "version": "17.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "packages/ts/hilla-file-router/node_modules/typescript": { + "version": "5.3.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "packages/ts/lit-form": { "name": "@vaadin/hilla-lit-form", "version": "24.4.0-alpha2", From 45d41a3b8b505533d0339baf52684089d8a14f5f Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Mon, 5 Feb 2024 15:30:20 +0200 Subject: [PATCH 14/18] chore: update codecov-action --- .github/workflows/validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 308d8e934f..372110d6d6 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -144,7 +144,7 @@ jobs: echo "COVFILES=$COVFILES" >> $GITHUB_ENV - name: Send Coverage to Codecov if: ${{ env.COVFILES != '' }} - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ env.COVFILES }} From 9b41199900d802c8bfac09cac4e4a2dd761ff3bb Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Tue, 6 Feb 2024 18:40:11 +0200 Subject: [PATCH 15/18] chore(file-router): update dependencies Co-authored-by: Soroosh Taefi <taefi.soroosh@gmail.com> --- packages/ts/hilla-file-router/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ts/hilla-file-router/package.json b/packages/ts/hilla-file-router/package.json index dcdbc224a1..3038f83092 100644 --- a/packages/ts/hilla-file-router/package.json +++ b/packages/ts/hilla-file-router/package.json @@ -1,6 +1,6 @@ { "name": "@vaadin/hilla-file-router", - "version": "24.4.0-alpha1", + "version": "24.4.0-alpha2", "description": "Hilla file-based router", "main": "index.js", "module": "index.js", From 921dbfbdd985bb66626bb712088cd3549ca4a628 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Wed, 7 Feb 2024 05:23:51 +0200 Subject: [PATCH 16/18] refactor(file-router): address review comments * Improve namings * Separate runtime and compile-time code --- packages/ts/hilla-file-router/package.json | 4 ++-- .../src/{react.ts => runtime.ts} | 10 +++++++-- .../src/{ => runtime}/utils.ts | 13 ++--------- .../src/vite-plugin-file-router.ts | 22 +++++++++---------- .../collectRoutesFromFS.ts} | 4 ++-- .../createRoutesFromFS.ts} | 9 ++++---- .../createViewConfigJson.ts} | 16 +++++--------- .../src/vite-plugin/utils.ts | 11 ++++++++++ ...es.spec.ts => collectRoutesFromFS.spec.ts} | 4 ++-- ...tes.spec.ts => createRoutesFromFS.spec.ts} | 6 ++--- ...n.spec.ts => createViewConfigJson.spec.ts} | 6 ++--- .../test/{react.spec.tsx => runtime.spec.tsx} | 4 ++-- packages/ts/hilla-file-router/test/utils.ts | 2 +- packages/ts/hilla-file-router/tsconfig.json | 2 -- 14 files changed, 58 insertions(+), 55 deletions(-) rename packages/ts/hilla-file-router/src/{react.ts => runtime.ts} (82%) rename packages/ts/hilla-file-router/src/{ => runtime}/utils.ts (87%) rename packages/ts/hilla-file-router/src/{collectRoutes.ts => vite-plugin/collectRoutesFromFS.ts} (91%) rename packages/ts/hilla-file-router/src/{generateRoutes.ts => vite-plugin/createRoutesFromFS.ts} (88%) rename packages/ts/hilla-file-router/src/{generateJson.ts => vite-plugin/createViewConfigJson.ts} (81%) create mode 100644 packages/ts/hilla-file-router/src/vite-plugin/utils.ts rename packages/ts/hilla-file-router/test/{collectRoutes.spec.ts => collectRoutesFromFS.spec.ts} (89%) rename packages/ts/hilla-file-router/test/{generateRoutes.spec.ts => createRoutesFromFS.spec.ts} (91%) rename packages/ts/hilla-file-router/test/{generateJson.spec.ts => createViewConfigJson.spec.ts} (84%) rename packages/ts/hilla-file-router/test/{react.spec.tsx => runtime.spec.tsx} (95%) diff --git a/packages/ts/hilla-file-router/package.json b/packages/ts/hilla-file-router/package.json index 3038f83092..bf683f2ab5 100644 --- a/packages/ts/hilla-file-router/package.json +++ b/packages/ts/hilla-file-router/package.json @@ -31,8 +31,8 @@ "typecheck": "tsc --noEmit" }, "exports": { - "./react.js": { - "default": "./react.js" + "./runtime.js": { + "default": "./runtime.js" }, "./vite-plugin-file-router.js": { "default": "./vite-plugin-file-router.js" diff --git a/packages/ts/hilla-file-router/src/react.ts b/packages/ts/hilla-file-router/src/runtime.ts similarity index 82% rename from packages/ts/hilla-file-router/src/react.ts rename to packages/ts/hilla-file-router/src/runtime.ts index b8acab4986..b77e045603 100644 --- a/packages/ts/hilla-file-router/src/react.ts +++ b/packages/ts/hilla-file-router/src/runtime.ts @@ -1,7 +1,13 @@ import type { UIMatch } from '@remix-run/router'; import { type ComponentType, createElement } from 'react'; import { type RouteObject, useMatches } from 'react-router'; -import { type AgnosticRoute, transformRoute, prepareConfig, type ViewConfig, extractComponentName } from './utils.js'; +import { + type AgnosticRoute, + transformRoute, + adjustViewTitle, + type ViewConfig, + extractComponentName, +} from './runtime/utils.js'; export type RouteModule<P = object> = Readonly<{ default: ComponentType<P>; @@ -22,7 +28,7 @@ export function toReactRouter(routes: AgnosticRoute<RouteModule>): RouteObject { path, element: module?.default ? createElement(module.default) : undefined, children: children.length > 0 ? (children as RouteObject[]) : undefined, - handle: prepareConfig(module?.config, extractComponentName(module?.default)), + handle: adjustViewTitle(module?.config, extractComponentName(module?.default)), }) satisfies RouteObject, ); } diff --git a/packages/ts/hilla-file-router/src/utils.ts b/packages/ts/hilla-file-router/src/runtime/utils.ts similarity index 87% rename from packages/ts/hilla-file-router/src/utils.ts rename to packages/ts/hilla-file-router/src/runtime/utils.ts index d72ed922e5..0687f7c8b8 100644 --- a/packages/ts/hilla-file-router/src/utils.ts +++ b/packages/ts/hilla-file-router/src/runtime/utils.ts @@ -1,5 +1,3 @@ -import { Script } from 'node:vm'; - export type ViewConfig = Readonly<{ /** * View title used in the main layout header, as <title> and as the default @@ -15,7 +13,7 @@ export type ViewConfig = Readonly<{ /** * Allows overriding the route path configuration. Uses the same syntax as - * the path property with React Router.This can be used to define a route + * the path property with React Router. This can be used to define a route * that conflicts with the file name conventions, e.g. /foo/index */ route?: string; @@ -62,13 +60,6 @@ export type AgnosticRoute<T> = Readonly<{ children?: ReadonlyArray<AgnosticRoute<T>>; }>; -export function processPattern(blank: string): string { - return blank - .replaceAll(/\{\.{3}.+\}/gu, '*') - .replaceAll(/\{{2}(.+)\}{2}/gu, ':$1?') - .replaceAll(/\{(.+)\}/gu, ':$1'); -} - export function transformRoute<T, U>( route: T, getChildren: (route: T) => IterableIterator<T> | null | undefined, @@ -98,7 +89,7 @@ export function extractComponentName(component?: unknown): string | undefined { const viewPattern = /view/giu; const upperCaseSplitPattern = /(?=[A-Z])/gu; -export function prepareConfig(config?: ViewConfig, componentName?: string): ViewConfig | undefined { +export function adjustViewTitle(config?: ViewConfig, componentName?: string): ViewConfig | undefined { if (config?.title) { return config; } diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts index ecbbb98537..4e2745c458 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -1,9 +1,9 @@ import { writeFile } from 'node:fs/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin } from 'vite'; -import collectRoutes from './collectRoutes.js'; -import generateJson from './generateJson.js'; -import generateRoutes from './generateRoutes.js'; +import collectRoutesFromFS from './vite-plugin/collectRoutesFromFS.js'; +import createRoutesFromFS from './vite-plugin/createRoutesFromFS.js'; +import createViewConfigJson from './vite-plugin/createViewConfigJson.js'; export type PluginOptions = Readonly<{ /** @@ -35,27 +35,27 @@ export type PluginOptions = Readonly<{ configExportName?: string; }>; -type GeneratedUrls = Readonly<{ +type RuntimeFileUrls = Readonly<{ json: URL; code: URL; }>; -async function generate(code: string, json: string, urls: GeneratedUrls) { +async function generateRuntimeFiles(code: string, json: string, urls: RuntimeFileUrls) { await Promise.all([writeFile(urls.json, json, 'utf-8'), writeFile(urls.code, code, 'utf-8')]); } async function build( viewsDir: URL, outDir: URL, - generatedUrls: GeneratedUrls, + generatedUrls: RuntimeFileUrls, extensions: readonly string[], configExportName: string, ): Promise<void> { - const routeMeta = await collectRoutes(viewsDir, { extensions }); - const code = generateRoutes(routeMeta, outDir); - const json = await generateJson(routeMeta, configExportName); + const routeMeta = await collectRoutesFromFS(viewsDir, { extensions }); + const runtimeRoutesCode = createRoutesFromFS(routeMeta, outDir); + const viewConfigJson = await createViewConfigJson(routeMeta, configExportName); - await generate(code, json, generatedUrls); + await generateRuntimeFiles(runtimeRoutesCode, viewConfigJson, generatedUrls); } /** @@ -73,7 +73,7 @@ export default function vitePluginFileSystemRouter({ let _viewsDir: URL; let _generatedDir: URL; let _outDir: URL; - let generatedUrls: GeneratedUrls; + let generatedUrls: RuntimeFileUrls; return { name: 'vite-plugin-file-router', diff --git a/packages/ts/hilla-file-router/src/collectRoutes.ts b/packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts similarity index 91% rename from packages/ts/hilla-file-router/src/collectRoutes.ts rename to packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts index b93758e544..d119681063 100644 --- a/packages/ts/hilla-file-router/src/collectRoutes.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts @@ -23,7 +23,7 @@ function cleanUp(blank: string) { const collator = new Intl.Collator('en-US'); -export default async function collectRoutes( +export default async function collectRoutesFromFS( dir: URL, { extensions, parent = dir }: CollectRoutesOptions, ): Promise<RouteMeta> { @@ -33,7 +33,7 @@ export default async function collectRoutes( for await (const d of await opendir(dir)) { if (d.isDirectory()) { - children.push(await collectRoutes(new URL(`${d.name}/`, dir), { extensions, parent: dir })); + children.push(await collectRoutesFromFS(new URL(`${d.name}/`, dir), { extensions, parent: dir })); } else if (d.isFile() && extensions.includes(extname(d.name))) { const file = new URL(d.name, dir); const name = basename(d.name, extname(d.name)); diff --git a/packages/ts/hilla-file-router/src/generateRoutes.ts b/packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromFS.ts similarity index 88% rename from packages/ts/hilla-file-router/src/generateRoutes.ts rename to packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromFS.ts index 0a618a6de1..4442fee7b6 100644 --- a/packages/ts/hilla-file-router/src/generateRoutes.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromFS.ts @@ -8,8 +8,9 @@ import ts, { type StringLiteral, type VariableStatement, } from 'typescript'; -import type { RouteMeta } from './collectRoutes.js'; -import { processPattern, transformRoute } from './utils.js'; +import { transformRoute } from '../runtime/utils.js'; +import type { RouteMeta } from './collectRoutesFromFS.js'; +import { convertFSPatternToURLPatternString } from './utils.js'; const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); @@ -49,7 +50,7 @@ function createRouteData( ); } -export default function generateRoutes(views: RouteMeta, generatedDir: URL): string { +export default function createRoutesFromFS(views: RouteMeta, generatedDir: URL): string { const imports: ImportDeclaration[] = []; let id = 0; @@ -69,7 +70,7 @@ export default function generateRoutes(views: RouteMeta, generatedDir: URL): str imports.push(createImport(mod, relativize(layout, generatedDir))); } - return createRouteData(processPattern(path), mod, children); + return createRouteData(convertFSPatternToURLPatternString(path), mod, children); }, ); diff --git a/packages/ts/hilla-file-router/src/generateJson.ts b/packages/ts/hilla-file-router/src/vite-plugin/createViewConfigJson.ts similarity index 81% rename from packages/ts/hilla-file-router/src/generateJson.ts rename to packages/ts/hilla-file-router/src/vite-plugin/createViewConfigJson.ts index 695e6627e2..17b3c7f628 100644 --- a/packages/ts/hilla-file-router/src/generateJson.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin/createViewConfigJson.ts @@ -1,8 +1,9 @@ import { readFile } from 'node:fs/promises'; import { Script } from 'node:vm'; import ts, { type Node } from 'typescript'; -import type { RouteMeta } from './collectRoutes.js'; -import { prepareConfig, processPattern, type ViewConfig } from './utils.js'; +import { adjustViewTitle, type ViewConfig } from '../runtime/utils.js'; +import type { RouteMeta } from './collectRoutesFromFS.js'; +import { convertFSPatternToURLPatternString } from './utils.js'; function* traverse( views: RouteMeta, @@ -19,11 +20,6 @@ function* traverse( } } -type RouteModule = Readonly<{ - default: unknown; - config?: ViewConfig; -}>; - function* walkAST(node: Node): Generator<Node> { yield node; @@ -32,7 +28,7 @@ function* walkAST(node: Node): Generator<Node> { } } -export default async function generateJson(views: RouteMeta, configExportName: string): Promise<string> { +export default async function createViewConfigJson(views: RouteMeta, configExportName: string): Promise<string> { const res = await Promise.all( Array.from(traverse(views), async (branch) => { const configs = await Promise.all( @@ -59,11 +55,11 @@ export default async function generateJson(views: RouteMeta, configExportName: s } } - return prepareConfig(config, componentName); + return adjustViewTitle(config, componentName); }), ); - const key = branch.map(({ path }) => processPattern(path)).join('/'); + const key = branch.map(({ path }) => convertFSPatternToURLPatternString(path)).join('/'); const value = configs[configs.length - 1]; return [key, value] satisfies readonly [string, ViewConfig | undefined]; diff --git a/packages/ts/hilla-file-router/src/vite-plugin/utils.ts b/packages/ts/hilla-file-router/src/vite-plugin/utils.ts new file mode 100644 index 0000000000..0fc2b585f4 --- /dev/null +++ b/packages/ts/hilla-file-router/src/vite-plugin/utils.ts @@ -0,0 +1,11 @@ +export function convertFSPatternToURLPatternString(fsPattern: string): string { + return ( + fsPattern + // /url/{...rest}/page -> /url/*/page + .replaceAll(/\{\.{3}.+\}/gu, '*') + // /url/{{param}}/page -> /url/:param?/page + .replaceAll(/\{{2}(.+)\}{2}/gu, ':$1?') + // /url/{param}/page -> /url/:param/page + .replaceAll(/\{(.+)\}/gu, ':$1') + ); +} diff --git a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts b/packages/ts/hilla-file-router/test/collectRoutesFromFS.spec.ts similarity index 89% rename from packages/ts/hilla-file-router/test/collectRoutes.spec.ts rename to packages/ts/hilla-file-router/test/collectRoutesFromFS.spec.ts index 90376a4367..f79e19401a 100644 --- a/packages/ts/hilla-file-router/test/collectRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/collectRoutesFromFS.spec.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'; import { expect, use } from '@esm-bundle/chai'; import deepEqualInAnyOrder from 'deep-equal-in-any-order'; import { rimraf } from 'rimraf'; -import collectRoutes from '../src/collectRoutes.js'; +import collectRoutesFromFS from '../src/vite-plugin/collectRoutesFromFS.js'; import { createTestingRouteFiles, createTestingRouteMeta, createTmpDir } from './utils.js'; use(deepEqualInAnyOrder); @@ -36,7 +36,7 @@ describe('@vaadin/hilla-file-router', () => { // │ ├── index.tsx // │ └── layout.tsx // └── about.tsx - const result = await collectRoutes(tmp, { extensions }); + const result = await collectRoutesFromFS(tmp, { extensions }); expect(result).to.deep.equals(createTestingRouteMeta(tmp)); }); diff --git a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts b/packages/ts/hilla-file-router/test/createRoutesFromFS.spec.ts similarity index 91% rename from packages/ts/hilla-file-router/test/generateRoutes.spec.ts rename to packages/ts/hilla-file-router/test/createRoutesFromFS.spec.ts index 68e963e7ab..20e44d4e5b 100644 --- a/packages/ts/hilla-file-router/test/generateRoutes.spec.ts +++ b/packages/ts/hilla-file-router/test/createRoutesFromFS.spec.ts @@ -2,8 +2,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { expect } from '@esm-bundle/chai'; -import type { RouteMeta } from '../src/collectRoutes.js'; -import generateRoutes from '../src/generateRoutes.js'; +import type { RouteMeta } from '../src/vite-plugin/collectRoutesFromFS.js'; +import createRoutesFromFS from '../src/vite-plugin/createRoutesFromFS.js'; import { createTestingRouteMeta } from './utils.js'; describe('@vaadin/hilla-file-router', () => { @@ -17,7 +17,7 @@ describe('@vaadin/hilla-file-router', () => { }); it('should generate a framework-agnostic tree of routes', () => { - const generated = generateRoutes(meta, new URL('./out/', dir)); + const generated = createRoutesFromFS(meta, new URL('./out/', dir)); expect(generated).to.equal(`import * as Page0 from "../views/about.js"; import * as Page1 from "../views/profile/$index.js"; diff --git a/packages/ts/hilla-file-router/test/generateJson.spec.ts b/packages/ts/hilla-file-router/test/createViewConfigJson.spec.ts similarity index 84% rename from packages/ts/hilla-file-router/test/generateJson.spec.ts rename to packages/ts/hilla-file-router/test/createViewConfigJson.spec.ts index 001fbb3903..6a8115c072 100644 --- a/packages/ts/hilla-file-router/test/generateJson.spec.ts +++ b/packages/ts/hilla-file-router/test/createViewConfigJson.spec.ts @@ -3,8 +3,8 @@ import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { expect } from '@esm-bundle/chai'; import { rimraf } from 'rimraf'; -import type { RouteMeta } from '../src/collectRoutes.js'; -import generateJson from '../src/generateJson.js'; +import type { RouteMeta } from '../src/vite-plugin/collectRoutesFromFS.js'; +import createViewConfigJson from '../src/vite-plugin/createViewConfigJson.js'; import { createTestingRouteFiles, createTestingRouteMeta, createTmpDir } from './utils.js'; describe('@vaadin/hilla-file-router', () => { @@ -26,7 +26,7 @@ describe('@vaadin/hilla-file-router', () => { }); it('should generate a JSON representation of the route tree', async () => { - const generated = await generateJson(meta, 'config'); + const generated = await createViewConfigJson(meta, 'config'); expect(generated).to.equal( JSON.stringify({ diff --git a/packages/ts/hilla-file-router/test/react.spec.tsx b/packages/ts/hilla-file-router/test/runtime.spec.tsx similarity index 95% rename from packages/ts/hilla-file-router/test/react.spec.tsx rename to packages/ts/hilla-file-router/test/runtime.spec.tsx index 183ae716c6..c211935804 100644 --- a/packages/ts/hilla-file-router/test/react.spec.tsx +++ b/packages/ts/hilla-file-router/test/runtime.spec.tsx @@ -1,8 +1,8 @@ import { expect, use } from '@esm-bundle/chai'; import chaiLike from 'chai-like'; import type { JSX } from 'react'; -import { type RouteModule, toReactRouter } from '../src/react.js'; -import type { AgnosticRoute } from '../src/utils.js'; +import type { AgnosticRoute } from '../src/runtime/utils.js'; +import { type RouteModule, toReactRouter } from '../src/runtime.js'; use(chaiLike); diff --git a/packages/ts/hilla-file-router/test/utils.ts b/packages/ts/hilla-file-router/test/utils.ts index cd8044b591..0690bfe232 100644 --- a/packages/ts/hilla-file-router/test/utils.ts +++ b/packages/ts/hilla-file-router/test/utils.ts @@ -2,7 +2,7 @@ import { appendFile, mkdir, mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; -import type { RouteMeta } from '../src/collectRoutes.js'; +import type { RouteMeta } from '../src/vite-plugin/collectRoutesFromFS.js'; export async function createTmpDir(): Promise<URL> { return pathToFileURL(`${await mkdtemp(join(tmpdir(), 'hilla-file-router-'))}/`); diff --git a/packages/ts/hilla-file-router/tsconfig.json b/packages/ts/hilla-file-router/tsconfig.json index 440ed221a9..2845bdae52 100644 --- a/packages/ts/hilla-file-router/tsconfig.json +++ b/packages/ts/hilla-file-router/tsconfig.json @@ -2,8 +2,6 @@ "extends": "../../../tsconfig.json", "compilerOptions": { "jsx": "react-jsx", - "experimentalDecorators": true, - "target": "es2022" }, "include": ["src", "test"], "exclude": ["test/**/*.snap.ts"] From 1a48962181264a5d4286dea63eda91ccc8484bf0 Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Wed, 7 Feb 2024 05:25:05 +0200 Subject: [PATCH 17/18] refactor(file-router): rename priority -> order --- packages/ts/hilla-file-router/src/runtime/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ts/hilla-file-router/src/runtime/utils.ts b/packages/ts/hilla-file-router/src/runtime/utils.ts index 0687f7c8b8..f05893f045 100644 --- a/packages/ts/hilla-file-router/src/runtime/utils.ts +++ b/packages/ts/hilla-file-router/src/runtime/utils.ts @@ -45,7 +45,7 @@ export type ViewConfig = Readonly<{ * used title. Entries without explicitly defined ordering are put below * entries with an order. */ - priority?: number; + order?: number; /** * Set to true to explicitly exclude a view from the automatically * populated menu. From 81a5745d2f219893baab15fd4ec95392f138480e Mon Sep 17 00:00:00 2001 From: Vlad Rindevich <vladrin@vaadin.com> Date: Wed, 7 Feb 2024 06:12:08 +0200 Subject: [PATCH 18/18] refactor(file-router): address review comments --- .../src/vite-plugin-file-router.ts | 4 +-- .../src/vite-plugin/collectRoutesFromFS.ts | 8 +---- ...outesFromFS.ts => createRoutesFromMeta.ts} | 2 +- .../src/vite-plugin/utils.ts | 31 +++++++++++++++++-- ...S.spec.ts => createRoutesFromMeta.spec.ts} | 4 +-- 5 files changed, 34 insertions(+), 15 deletions(-) rename packages/ts/hilla-file-router/src/vite-plugin/{createRoutesFromFS.ts => createRoutesFromMeta.ts} (96%) rename packages/ts/hilla-file-router/test/{createRoutesFromFS.spec.ts => createRoutesFromMeta.spec.ts} (93%) diff --git a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts index 4e2745c458..a9155ff2a2 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin-file-router.ts @@ -2,7 +2,7 @@ import { writeFile } from 'node:fs/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; import type { Plugin } from 'vite'; import collectRoutesFromFS from './vite-plugin/collectRoutesFromFS.js'; -import createRoutesFromFS from './vite-plugin/createRoutesFromFS.js'; +import createRoutesFromMeta from './vite-plugin/createRoutesFromMeta.js'; import createViewConfigJson from './vite-plugin/createViewConfigJson.js'; export type PluginOptions = Readonly<{ @@ -52,7 +52,7 @@ async function build( configExportName: string, ): Promise<void> { const routeMeta = await collectRoutesFromFS(viewsDir, { extensions }); - const runtimeRoutesCode = createRoutesFromFS(routeMeta, outDir); + const runtimeRoutesCode = createRoutesFromMeta(routeMeta, outDir); const viewConfigJson = await createViewConfigJson(routeMeta, configExportName); await generateRuntimeFiles(runtimeRoutesCode, viewConfigJson, generatedUrls); diff --git a/packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts b/packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts index d119681063..0e5cf32b5e 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin/collectRoutesFromFS.ts @@ -1,6 +1,7 @@ import { opendir } from 'node:fs/promises'; import { basename, extname, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { cleanUp } from './utils.js'; export type RouteMeta = Readonly<{ path: string; @@ -14,13 +15,6 @@ export type CollectRoutesOptions = Readonly<{ parent?: URL; }>; -function cleanUp(blank: string) { - return blank - .replaceAll(/\{\.{3}(.+)\}/gu, '$1') - .replaceAll(/\{{2}(.+)\}{2}/gu, '$1') - .replaceAll(/\{(.+)\}/gu, '$1'); -} - const collator = new Intl.Collator('en-US'); export default async function collectRoutesFromFS( diff --git a/packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromFS.ts b/packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromMeta.ts similarity index 96% rename from packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromFS.ts rename to packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromMeta.ts index 4442fee7b6..6643a52aa6 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromFS.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin/createRoutesFromMeta.ts @@ -50,7 +50,7 @@ function createRouteData( ); } -export default function createRoutesFromFS(views: RouteMeta, generatedDir: URL): string { +export default function createRoutesFromMeta(views: RouteMeta, generatedDir: URL): string { const imports: ImportDeclaration[] = []; let id = 0; diff --git a/packages/ts/hilla-file-router/src/vite-plugin/utils.ts b/packages/ts/hilla-file-router/src/vite-plugin/utils.ts index 0fc2b585f4..b221cada98 100644 --- a/packages/ts/hilla-file-router/src/vite-plugin/utils.ts +++ b/packages/ts/hilla-file-router/src/vite-plugin/utils.ts @@ -1,11 +1,36 @@ +const restParamPattern = /\{\.{3}(.+)\}/gu; +const optionalParamPattern = /\{{2}(.+)\}{2}/gu; +const paramPattern = /\{(.+)\}/gu; + +/** + * Converts a file system pattern to a URL pattern string. + * + * @param fsPattern - a string representing a file system pattern: + * - `{param}` - for a required single parameter; + * - `{{param}}` - for an optional single parameter; + * - `{...rest}` - for multiple parameters, including none. + * + * @returns a string representing a URL pattern, respectively: + * - `:param`; + * - `:param?`; + * - `*`. + */ export function convertFSPatternToURLPatternString(fsPattern: string): string { return ( fsPattern // /url/{...rest}/page -> /url/*/page - .replaceAll(/\{\.{3}.+\}/gu, '*') + .replaceAll(restParamPattern, '*') // /url/{{param}}/page -> /url/:param?/page - .replaceAll(/\{{2}(.+)\}{2}/gu, ':$1?') + .replaceAll(optionalParamPattern, ':$1?') // /url/{param}/page -> /url/:param/page - .replaceAll(/\{(.+)\}/gu, ':$1') + .replaceAll(paramPattern, ':$1') ); } + +/** + * A small helper function that clears route path of the control characters in + * order to sort the routes alphabetically. + */ +export function cleanUp(path: string): string { + return path.replaceAll(restParamPattern, '$1').replaceAll(optionalParamPattern, '$1').replaceAll(paramPattern, '$1'); +} diff --git a/packages/ts/hilla-file-router/test/createRoutesFromFS.spec.ts b/packages/ts/hilla-file-router/test/createRoutesFromMeta.spec.ts similarity index 93% rename from packages/ts/hilla-file-router/test/createRoutesFromFS.spec.ts rename to packages/ts/hilla-file-router/test/createRoutesFromMeta.spec.ts index 20e44d4e5b..24e10ff939 100644 --- a/packages/ts/hilla-file-router/test/createRoutesFromFS.spec.ts +++ b/packages/ts/hilla-file-router/test/createRoutesFromMeta.spec.ts @@ -3,7 +3,7 @@ import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { expect } from '@esm-bundle/chai'; import type { RouteMeta } from '../src/vite-plugin/collectRoutesFromFS.js'; -import createRoutesFromFS from '../src/vite-plugin/createRoutesFromFS.js'; +import createRoutesFromMeta from '../src/vite-plugin/createRoutesFromMeta.js'; import { createTestingRouteMeta } from './utils.js'; describe('@vaadin/hilla-file-router', () => { @@ -17,7 +17,7 @@ describe('@vaadin/hilla-file-router', () => { }); it('should generate a framework-agnostic tree of routes', () => { - const generated = createRoutesFromFS(meta, new URL('./out/', dir)); + const generated = createRoutesFromMeta(meta, new URL('./out/', dir)); expect(generated).to.equal(`import * as Page0 from "../views/about.js"; import * as Page1 from "../views/profile/$index.js";