chore(deps-dev): update eslint to v9 (#147)

This commit is contained in:
Marco Pasqualetti
2024-12-01 10:07:35 +01:00
committed by GitHub
parent 5fc68a1926
commit 11d425faa9
42 changed files with 665 additions and 504 deletions
-106
View File
@@ -1,106 +0,0 @@
{
"root": true,
"reportUnusedDisableDirectives": true,
"ignorePatterns": ["**/build", "**/dist"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
],
"env": {
"browser": true,
"es2020": true,
},
"parserOptions": {
"tsconfigRootDir": ".",
"project": true,
"sourceType": "module",
"ecmaVersion": 2020,
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
"typescript": true,
},
"react": {
"version": "detect",
},
},
"rules": {
"@typescript-eslint/array-type": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{ "prefer": "type-imports" },
],
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/method-signature-style": ["error", "property"],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
"custom": {
"regex": "^(T|T[A-Z][A-Za-z]+)$",
"match": true,
},
},
],
"@typescript-eslint/no-deprecated": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-inferrable-types": [
"error",
{ "ignoreParameters": true },
],
"import/default": "error",
"import/export": "error",
"import/namespace": "error",
"import/newline-after-import": "error",
"import/no-cycle": "error",
"import/no-duplicates": "off",
"import/no-named-as-default-member": "error",
"import/no-unused-modules": "error",
"import/order": [
"off",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type",
],
},
],
"no-case-declarations": "error",
"no-empty": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-shadow": "error",
"no-undef": "off",
"sort-imports": "off",
},
}
+131
View File
@@ -0,0 +1,131 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
// @ts-expect-error no types are available for this plugin
import eslintPluginImport from 'eslint-plugin-import'
export default tseslint.config(
{
ignores: [
// #region shared
'**/build',
'**/dist',
'**/vite.config.ts.timestamp**',
// #endregion shared
// #region package-specific
'packages/fs-router-vite-plugin/tests/generator/**',
'packages/tuono/bin/**',
// #endregion package-specific
],
},
{
linterOptions: {
reportUnusedDisableDirectives: 'error',
},
},
eslint.configs.recommended,
eslintPluginImport.flatConfigs.recommended,
eslintPluginImport.flatConfigs.typescript,
tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: true,
},
react: {
version: 'detect',
},
},
rules: {
// #region @typescript-eslint
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/no-wrapper-object-types': 'error',
'@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' },
],
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/method-signature-style': ['error', 'property'],
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'typeParameter',
format: ['PascalCase'],
leadingUnderscore: 'forbid',
trailingUnderscore: 'forbid',
custom: {
regex: '^(T|T[A-Z][A-Za-z]+)$',
match: true,
},
},
],
'@typescript-eslint/no-deprecated': 'error',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-inferrable-types': [
'error',
{ ignoreParameters: true },
],
// #endregion @typescript-eslint
// #region import
'import/default': 'error',
'import/export': 'error',
'import/namespace': 'error',
'import/newline-after-import': 'error',
'import/no-cycle': 'error',
'import/no-duplicates': 'error',
'import/no-named-as-default-member': 'error',
'import/no-unused-modules': 'error',
'import/order': [
'error',
{
'newlines-between': 'always-and-inside-groups',
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
],
},
],
// #endregion import
// #region misc
/**
* @todo some of this might be removed.
* They are handled by other plugin and / or typescript
*/
'no-case-declarations': 'error',
'no-empty': 'error',
'no-prototype-builtins': 'error',
'no-redeclare': 'error',
'no-shadow': 'error',
'no-undef': 'off',
'sort-imports': 'off',
},
},
)
+8 -9
View File
@@ -1,5 +1,7 @@
{
"name": "workspace",
"private": true,
"type": "module",
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387",
"scripts": {
"dev": "turbo dev --filter=./packages/*",
@@ -21,24 +23,21 @@
"author": "Valerio Ageno",
"license": "MIT",
"devDependencies": {
"@eslint/js": "9.15.0",
"@tanstack/config": "^0.7.0",
"@types/node": "^22.0.0",
"@types/react": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint": "^9.15.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^5.0.0",
"prettier": "^3.2.4",
"typescript": "^5.4.5",
"turbo": "^2.2.3",
"typescript": "^5.6.3",
"typescript-eslint": "8.15.0",
"vite": "^5.2.11",
"vitest": "^2.0.0"
},
"dependencies": {
"turbo": "^2.2.3"
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"types": "tsc --noEmit",
@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest'
import { buildRouteConfig } from './build-route-config'
const routes = [
@@ -61,11 +62,11 @@ const routes = [
},
]
describe('buildRouteConfig works', async () => {
describe('buildRouteConfig works', () => {
it('Should build the correct config', () => {
const expectedConfig =
'PostsMyPostRoute,PostsIndexRoute,PostspostIndexRoute'
const config = buildRouteConfig(routes)
expect(config).toStrictEqual(expectedConfig)
expect(config).toStrictEqual(
'PostsMyPostRoute,PostsIndexRoute,PostspostIndexRoute',
)
})
})
@@ -1,8 +1,10 @@
import * as fsp from 'fs/promises'
import path from 'path'
import { format } from 'prettier'
import { buildRouteConfig } from './build-route-config'
import { hasParentRoute } from './has-parent-route'
import {
cleanPath,
determineNodePath,
@@ -19,7 +21,6 @@ import {
import type { Config, RouteNode } from './types'
import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
import { format } from 'prettier'
import { sortRouteNodes } from './sort-route-nodes'
import isDefaultExported from './utils/is-default-exported'
@@ -130,7 +131,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
// Loop over the flat list of routeNodes and
// build up a tree based on the routeNodes' routePath
const handleNode = async (node: RouteNode): Promise<void> => {
const handleNode = (node: RouteNode): void => {
const parentRoute = hasParentRoute(routeNodes, node, node.routePath)
if (parentRoute) node.parent = parentRoute
@@ -149,7 +150,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
}
for (const node of preRouteNodes) {
await handleNode(node)
handleNode(node)
}
const routeConfigChildrenText = buildRouteConfig(routeNodes)
@@ -237,7 +238,8 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
const routeTreeContent = await fsp
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
.catch((err: any) => {
.catch((e) => {
const err = e as Error & { code?: string }
if (err.code === 'ENOENT') {
return undefined
}
@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest'
import { hasParentRoute } from './has-parent-route'
const routes = [
@@ -70,7 +71,7 @@ const dynamicRoute = {
cleanedPath: '/posts/[post]',
}
describe('hasParentRoute works', async () => {
describe('hasParentRoute works', () => {
it('Should detect parent route', () => {
const parentRoute = hasParentRoute(routes, myPost, myPost.path)
expect(parentRoute).toStrictEqual(parent)
+2 -2
View File
@@ -1,9 +1,9 @@
import { routeGenerator } from './generator'
import { normalize } from 'path'
import type { Plugin } from 'vite'
import { routeGenerator } from './generator'
const ROUTES_DIRECTORY_PATH = './src/routes'
let lock = false
@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest'
import { sortRouteNodes } from './sort-route-nodes'
const routes = [
@@ -106,7 +107,7 @@ const expectedSorting = [
},
]
describe('sortRouteNodes works', async () => {
describe('sortRouteNodes works', () => {
it('Should correctly sort the nodes', () => {
const sorted = sortRouteNodes(routes)
expect(sorted).toStrictEqual(expectedSorting)
+6 -5
View File
@@ -35,7 +35,7 @@ export function routePathToVariable(routePath: string): string {
export function multiSortBy<T>(
arr: T[],
accessors: ((item: T) => any)[] = [(d): any => d],
accessors: ((item: T) => unknown)[] = [(d): unknown => d],
): T[] {
return arr
.map((d, i) => [d, i] as const)
@@ -55,7 +55,7 @@ export function multiSortBy<T>(
continue
}
return ao > bo ? 1 : -1
return (ao as number) > (bo as number) ? 1 : -1
}
return ai - bi
@@ -81,8 +81,8 @@ export function removeTrailingUnderscores(s?: string): string | undefined {
/**
* Removes all segments from a given path that start with an underscore ('_').
*
* @param {string} routePath - The path from which to remove segments. Defaults to '/'.
* @returns {string} The path with all underscore-prefixed segments removed.
* @param routePath - The path from which to remove segments. Defaults to '/'.
* @returns The path with all underscore-prefixed segments removed.
* @example
* removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
*/
@@ -101,8 +101,9 @@ export function trimPathLeft(pathToTrim: string): string {
}
export function removeLastSlash(str: string): string {
if (str.length > 1 && str.endsWith('/'))
if (str.length > 1 && str.endsWith('/')) {
return str.substring(0, str.length - 1)
}
return str
}
@@ -1,5 +1,7 @@
import fs from 'fs/promises'
import { describe, it, expect } from 'vitest'
import { routeGenerator } from '../src/generator'
describe('generator works', async () => {
+2 -1
View File
@@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "vite.config.ts"]
"include": ["src", "tests", "vite.config.ts"],
"exclude": ["tests/generator/**"]
}
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"types": "tsc --noEmit",
+9 -11
View File
@@ -1,16 +1,7 @@
import type { Plugin } from 'vite'
import * as babel from '@babel/core'
import type { PluginItem } from '@babel/core'
import { isTuonoDynamicFnImported } from './utils'
import {
TUONO_MAIN_PACKAGE,
TUONO_DYNAMIC_FN_ID,
TUONO_LAZY_FN_ID,
} from './constants'
import * as t from '@babel/types'
import type { Plugin } from 'vite'
import type { PluginItem } from '@babel/core'
import type {
Identifier,
CallExpression,
@@ -18,6 +9,13 @@ import type {
StringLiteral,
} from '@babel/types'
import {
TUONO_MAIN_PACKAGE,
TUONO_DYNAMIC_FN_ID,
TUONO_LAZY_FN_ID,
} from './constants'
import { isTuonoDynamicFnImported } from './utils'
/**
* [SERVER build]
* This plugin just removes the `dynamic` imported function from any tuono import
+7 -4
View File
@@ -1,10 +1,13 @@
import { TUONO_MAIN_PACKAGE, TUONO_DYNAMIC_FN_ID } from './constants'
import type * as t from '@babel/types'
import type {
Identifier,
ImportDeclaration,
ImportSpecifier,
} from '@babel/types'
import type { Identifier, ImportDeclaration } from '@babel/types'
import { TUONO_MAIN_PACKAGE, TUONO_DYNAMIC_FN_ID } from './constants'
export const isTuonoDynamicFnImported = (
path: babel.NodePath<t.ImportSpecifier>,
path: babel.NodePath<ImportSpecifier>,
): boolean => {
if ((path.node.imported as Identifier).name !== TUONO_DYNAMIC_FN_ID) {
return false
@@ -1,4 +1,6 @@
import { it, expect, describe } from 'vitest'
import type { Plugin } from 'vite'
import { LazyLoadingPlugin } from '../src'
const SOURCE_CODE = `
@@ -20,9 +22,21 @@ const PokemonspokemonImport = dynamic(
)
`
type ViteTransformHandler = Exclude<
Plugin['transform'],
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
Function | undefined
>['handler']
// Create a type-safe transform method
function getTransform(): (...args: Parameters<ViteTransformHandler>) => string {
return LazyLoadingPlugin().transform as never
}
describe('Transpile tuono source', () => {
it('Into the client bundle', () => {
const bundle = LazyLoadingPlugin().transform?.(SOURCE_CODE, 'id')
const pluginTransform = getTransform()
const bundle = pluginTransform(SOURCE_CODE, 'id')
expect(bundle)
.toBe(`import { createRoute, lazyLoadComponent as dynamic } from 'tuono';
const IndexImport = dynamic(() => import('./../src/routes/index'));
@@ -30,7 +44,8 @@ const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[po
})
it('Into the server bundle', () => {
const bundle = LazyLoadingPlugin().transform?.(SOURCE_CODE, 'id', {
const pluginTransform = getTransform()
const bundle = pluginTransform(SOURCE_CODE, 'id', {
ssr: true,
})
expect(bundle).toBe(`import { createRoute } from 'tuono';
@@ -41,7 +56,8 @@ import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]";`)
describe('Non tuono dynamic function', () => {
it('Into the client bundle', () => {
const bundle = LazyLoadingPlugin().transform?.(NON_DYNAMIC_SOURCE, 'id')
const pluginTransform = getTransform()
const bundle = pluginTransform(NON_DYNAMIC_SOURCE, 'id')
expect(bundle).toBe(`import { createRoute } from 'tuono';
import { dynamic } from 'external-lib';
const IndexImport = dynamic(() => import('./../src/routes/index'));
@@ -49,7 +65,8 @@ const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[po
})
it('Into the server bundle', () => {
const bundle = LazyLoadingPlugin().transform?.(NON_DYNAMIC_SOURCE, 'id', {
const pluginTransform = getTransform()
const bundle = pluginTransform(NON_DYNAMIC_SOURCE, 'id', {
ssr: true,
})
expect(bundle).toBe(`import { createRoute } from 'tuono';
+1 -1
View File
@@ -1,4 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "vite.config.ts"]
"include": ["src", "tests", "vite.config.ts"]
}
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"types": "tsc --noEmit",
+5 -4
View File
@@ -1,9 +1,10 @@
import * as React from 'react'
import { useRouter } from '../hooks/useRouter'
import type { AnchorHTMLAttributes, MouseEvent } from 'react'
import useRoute from '../hooks/useRoute'
import type { AnchorHTMLAttributes } from 'react'
import { useInView } from 'react-intersection-observer'
import { useRouter } from '../hooks/useRouter'
import useRoute from '../hooks/useRoute'
interface TuonoLinkProps {
/**
* If "true" the route gets loaded when the link enters the viewport. Default "true"
@@ -28,7 +29,7 @@ export default function Link(
triggerOnce: true,
})
const handleTransition = (e: MouseEvent<HTMLAnchorElement>): void => {
const handleTransition: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.preventDefault()
props.onClick?.(e)
+6 -4
View File
@@ -1,12 +1,14 @@
import * as React from 'react'
import { useRouterStore } from '../hooks/useRouterStore'
import { RouteMatch } from './RouteMatch'
import NotFound from './NotFound'
import useRoute from '../hooks/useRoute'
interface MatchesProps {
import { RouteMatch } from './RouteMatch'
import NotFound from './NotFound'
interface MatchesProps<TServerSideProps = unknown> {
// user defined props
serverSideProps: any
serverSideProps: TServerSideProps
}
export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
+3 -1
View File
@@ -1,7 +1,9 @@
import * as React from 'react'
import { useInternalRouter } from '../hooks/useInternalRouter'
import { RouteMatch } from './RouteMatch'
import Link from './Link'
import { useInternalRouter } from '../hooks/useInternalRouter'
export default function NotFound(): React.JSX.Element {
const router = useInternalRouter()
@@ -1,8 +1,11 @@
import * as React from 'react'
import { afterEach, describe, expect, test, vi } from 'vitest'
import { RouteMatch } from './RouteMatch'
import { cleanup, render, screen } from '@testing-library/react'
import type { Route } from '../route'
import { RouteMatch } from './RouteMatch'
import '@testing-library/jest-dom'
interface Props {
+13 -12
View File
@@ -1,11 +1,12 @@
import * as React from 'react'
import type { Route } from '../route'
import { useServerSideProps } from '../hooks/useServerSideProps'
interface MatchProps {
interface RouteMatchProps<TServerSideProps = unknown> {
route: Route
// User defined server side props
serverSideProps: any
serverSideProps: TServerSideProps
}
/**
@@ -16,7 +17,7 @@ interface MatchProps {
export const RouteMatch = ({
route,
serverSideProps,
}: MatchProps): React.JSX.Element => {
}: RouteMatchProps): React.JSX.Element => {
const { data, isLoading } = useServerSideProps(route, serverSideProps)
const routes = React.useMemo(() => loadParentComponents(route), [route.id])
@@ -28,20 +29,20 @@ export const RouteMatch = ({
)
}
interface TraverseRootComponentsProps {
interface ParentProps<TData = unknown> {
children: React.ReactNode
data: TData
isLoading: boolean
}
interface TraverseRootComponentsProps<TData = unknown> {
routes: Route[]
data: any
data: TData
isLoading: boolean
children?: React.ReactNode
index?: number
}
interface ParentProps {
children: React.ReactNode
data: any
isLoading: boolean
}
/*
* This component traverses and renders
* all the components that wraps the selected route (__root).
@@ -84,7 +85,7 @@ const TraverseRootComponents = React.memo(
)
const loadParentComponents = (route: Route, loader: Route[] = []): Route[] => {
const parentComponent = route.options?.getParentRoute?.()
const parentComponent = route.options.getParentRoute?.() as Route
loader.push(parentComponent)
@@ -1,21 +1,22 @@
import * as React from 'react'
import React from 'react'
import type { Router } from '../router'
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const routerContext = React.createContext<Router>(null!)
const TUONO_CONTEXT = '__TUONO_CONTEXT__'
const TUONO_CONTEXT_GLOBAL_NAME = '__TUONO_CONTEXT__'
export function getRouterContext(): any {
export function getRouterContext(): React.Context<Router> {
if (typeof document === 'undefined') {
return routerContext
}
if (window[TUONO_CONTEXT as any]) {
return window[TUONO_CONTEXT as any]
if (window[TUONO_CONTEXT_GLOBAL_NAME]) {
return window[TUONO_CONTEXT_GLOBAL_NAME]
}
window[TUONO_CONTEXT as any] = routerContext as any
window[TUONO_CONTEXT_GLOBAL_NAME] = routerContext
return routerContext
}
@@ -1,23 +1,19 @@
import React from 'react'
import type { ReactNode, JSX } from 'react'
import { useListenBrowserUrlUpdates } from '../hooks/useListenBrowserUrlUpdates'
import { initRouterStore } from '../hooks/useRouterStore'
import type { ServerProps } from '../types'
import type { Router } from '../router'
import { getRouterContext } from './RouterContext'
import { Matches } from './Matches'
type Router = any
interface RouterContextProviderProps {
router: Router
children: ReactNode
}
interface RouterProviderProps {
router: Router
serverProps?: ServerProps
}
function RouterContextProvider({
router,
children,
@@ -27,7 +23,11 @@ function RouterContextProvider({
router.update({
...router.options,
...rest,
// @ts-expect-error router options do not have context as property and I was unable to find any usage of it,
// so is this spread required?
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
context: {
// @ts-expect-error see previous
...router.options.context,
},
})
@@ -41,6 +41,11 @@ function RouterContextProvider({
)
}
interface RouterProviderProps {
router: Router
serverProps?: ServerProps
}
export function RouterProvider({
router,
serverProps,
+1
View File
@@ -1,5 +1,6 @@
import * as React from 'react'
import type { ReactElement, ComponentType } from 'react'
import type { RouteComponent } from './types'
type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
+14
View File
@@ -0,0 +1,14 @@
import type React from 'react'
import type { Router } from './router'
declare global {
interface Window {
__TUONO__ROUTER__: Router
__TUONO_SSR_PROPS__?: {
props?: unknown
}
__TUONO_CONTEXT__?: React.Context<Router>
}
}
@@ -1,4 +1,5 @@
import * as React from 'react'
import { getRouterContext } from '../components/RouterContext'
import type { Router } from '../router'
@@ -1,6 +1,7 @@
import { useRouterStore } from './useRouterStore'
import { useEffect } from 'react'
import { useRouterStore } from './useRouterStore'
/**
* This hook is meant to handle just browser related location updates
* like the back and forward buttons.
@@ -8,8 +9,9 @@ import { useEffect } from 'react'
export const useListenBrowserUrlUpdates = (): void => {
const updateLocation = useRouterStore((st) => st.updateLocation)
const updateLocationOnPopStateChange = ({ target }: any): void => {
const { pathname, hash, href, search } = target.location
const updateLocationOnPopStateChange = ({ target }: PopStateEvent): void => {
const { pathname, hash, href, search } = (target as typeof window).location
updateLocation({
pathname,
hash,
@@ -18,8 +20,10 @@ export const useListenBrowserUrlUpdates = (): void => {
search: Object.fromEntries(new URLSearchParams(search)),
})
}
useEffect(() => {
window.addEventListener('popstate', updateLocationOnPopStateChange)
return (): void => {
window.removeEventListener('popstate', updateLocationOnPopStateChange)
}
+3 -4
View File
@@ -1,11 +1,10 @@
import { afterEach, describe, expect, test, vi } from 'vitest'
import useRoute from './useRoute'
import { cleanup } from '@testing-library/react'
import useRoute from './useRoute'
describe('Test useRoute fn', () => {
afterEach(() => {
cleanup()
})
afterEach(cleanup)
test('match routes by ids', () => {
vi.mock('./useInternalRouter.tsx', () => ({
+1
View File
@@ -1,4 +1,5 @@
import type { Route } from '../route'
import { useInternalRouter } from './useInternalRouter'
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
@@ -1,21 +1,17 @@
import { useState, useEffect, useRef } from 'react'
import type { Route } from '../route'
import { useRouterStore } from './useRouterStore'
import { fromUrlToParsedLocation } from '../utils/from-url-to-parsed-location'
import { useRouterStore } from './useRouterStore'
const isServer = typeof document === 'undefined'
interface UseServerSidePropsReturn {
data: any
interface UseServerSidePropsReturn<TData> {
data: TData
isLoading: boolean
}
declare global {
interface Window {
__TUONO_SSR_PROPS__: any
}
}
interface TuonoApi {
data?: any
info: {
@@ -26,7 +22,7 @@ interface TuonoApi {
const fetchClientSideData = async (): Promise<TuonoApi> => {
const slash = location.pathname.endsWith('/') ? '' : '/'
const res = await fetch(`/__tuono/data${location.pathname}${slash}data.json`)
const data: TuonoApi = await res.json()
const data = (await res.json()) as TuonoApi
return data
}
@@ -40,7 +36,7 @@ export function useServerSideProps<T>(
route: Route,
// User defined props
serverSideProps: T,
): UseServerSidePropsReturn {
): UseServerSidePropsReturn<T> {
const isFirstRendering = useRef<boolean>(true)
const [location, updateLocation] = useRouterStore((st) => [
st.location,
@@ -48,15 +44,15 @@ export function useServerSideProps<T>(
])
const [isLoading, setIsLoading] = useState<boolean>(
// Force loading if has handler
route.options.hasHandler &&
!!route.options.hasHandler &&
// Avoid loading on the server
!isServer &&
// Avoid loading if first rendering
!isFirstRendering.current,
)
const [data, setData] = useState<any>(
serverSideProps ?? window.__TUONO_SSR_PROPS__?.props,
const [data, setData] = useState<T | undefined>(
(serverSideProps ?? window.__TUONO_SSR_PROPS__?.props) as T,
)
useEffect(() => {
@@ -68,6 +64,8 @@ export function useServerSideProps<T>(
}
// After client side routing load again the remote data
if (route.options.hasHandler) {
// The error management is already handled inside the IIFE
// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async (): Promise<void> => {
setIsLoading(true)
try {
@@ -86,7 +84,7 @@ export function useServerSideProps<T>(
updateLocation(parsedLocation)
return
}
setData(response.data)
setData(response.data as T)
} catch (error) {
throw Error('Failed loading Server Side Data', { cause: error })
} finally {
@@ -101,5 +99,5 @@ export function useServerSideProps<T>(
}
}, [location.pathname])
return { isLoading, data }
return { isLoading, data: data as T }
}
+15 -9
View File
@@ -3,35 +3,41 @@ import type { RouteComponent } from './types'
import { trimPathLeft, joinPaths } from './utils'
interface RouteOptions {
id?: string
isRoot?: boolean
getParentRoute?: () => Route
path?: string
component: RouteComponent
hasHandler?: boolean
}
export function createRoute(options: RouteOptions): Route {
return new Route(options)
}
export const rootRouteId = '__root__'
const rootRouteId = '__root__'
export class Route {
parentRoute!: any
options: RouteOptions
id?: string
fullPath!: string
isRoot: boolean
path?: string
options: any
fullPath!: string
children?: Route[]
parentRoute!: any
/** @todo check if this is required */
router: RouterType
isRoot: boolean
originalIndex?: number
component: RouteComponent
'$$typeof': symbol
constructor(options: RouteOptions) {
this.isRoot = options.isRoot ?? typeof options.getParentRoute !== 'function'
this.options = options
;(this as any).$$typeof = Symbol.for('react.memo')
this.$$typeof = Symbol.for('react.memo')
this.component = options.component
}
@@ -39,9 +45,9 @@ export class Route {
init = (originalIndex: number): void => {
this.originalIndex = originalIndex
const isRoot = !this.options?.path && !this.options?.id
const isRoot = !this.options.path && !this.options.id
this.parentRoute = this.options?.getParentRoute?.()
this.parentRoute = this.options.getParentRoute?.()
if (isRoot) {
this.path = rootRouteId
@@ -54,7 +60,7 @@ export class Route {
path = trimPathLeft(path)
}
const customId = this.options?.id || path
const customId = this.options.id || path
// Strip the parentId prefix from the first level of children
let id = isRoot ? rootRouteId : joinPaths([customId])
+19 -12
View File
@@ -3,15 +3,15 @@ import type { ComponentType as ReactComponentType } from 'react'
import { trimPath, trimPathRight } from './utils'
import type { Route } from './route'
type RouteTree = any
type RouteTree = Route
interface CreateRouter {
interface CreateRouterOptions {
routeTree: RouteTree
basePath?: string
options?: RouteOptions
options?: RouterOptions
}
interface RouteOptions {
interface RouterOptions {
component?: ReactComponentType
hasHandler?: boolean
routeTree?: RouteTree
@@ -19,32 +19,39 @@ interface RouteOptions {
export type RouterType = any
export function createRouter(options: CreateRouter): Router {
export function createRouter(options: CreateRouterOptions): Router {
return new Router(options)
}
export class Router {
options?: RouteOptions
options?: RouterOptions
basePath = '/'
routeTree: any
routeTree?: RouteTree
/** Not used */
history: any
isServer = typeof document === 'undefined'
routesById: Record<string, Route> = {}
// Not used
/** Not used */
routesByPath: Record<string, Route> = {}
/** Not used */
flatRoutes: any
constructor(options: CreateRouter) {
constructor(options: CreateRouterOptions) {
this.update({
...options,
})
if (!this.isServer) {
;(window as any).__TSR__ROUTER__ = this
window.__TUONO__ROUTER__ = this
}
}
update = (newOptions: CreateRouter): void => {
update = (newOptions: CreateRouterOptions): void => {
this.options = {
...this.options,
...newOptions,
@@ -82,7 +89,7 @@ export class Router {
})
}
recurseRoutes([this.routeTree])
recurseRoutes([this.routeTree as Route])
}
#updateBasePath = (basePath?: string): void => {
+4 -4
View File
@@ -5,17 +5,17 @@ export interface Segment {
value: string
}
export interface ServerProps {
export interface ServerProps<TProps = unknown> {
router: {
href: string
pathname: string
searchStr: string
}
props: any
props: TProps
}
export interface RouteProps {
data: any
export interface RouteProps<TData = unknown> {
data: TData
isLoading: boolean
}
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"types": "tsc --noEmit",
+2 -1
View File
@@ -1,9 +1,10 @@
import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'
import { HelmetProvider } from 'react-helmet-async'
type RouteTree = any
type RouteTree = ReturnType<typeof createRoute>
export function hydrate(routeTree: RouteTree): void {
// Create a new router instance
+10 -6
View File
@@ -4,8 +4,9 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import type { HelmetServerState } from 'react-helmet-async'
import { HelmetProvider } from 'react-helmet-async'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'
type RouteTree = any
type RouteTree = ReturnType<typeof createRoute>
type Mode = 'Dev' | 'Prod'
const TUONO_DEV_SERVER_PORT = 3000
@@ -37,17 +38,20 @@ function generateJsScripts(jsBundles: string[], mode: Mode): string {
export function serverSideRendering(routeTree: RouteTree) {
return function render(payload: string | undefined): string {
const props = payload ? JSON.parse(payload) : {}
const serverProps = (payload ? JSON.parse(payload) : {}) as Record<
string,
unknown
>
const mode = props.mode as Mode
const jsBundles = props.jsBundles as string[]
const cssBundles = props.cssBundles as string[]
const mode = serverProps.mode as Mode
const jsBundles = serverProps.jsBundles as string[]
const cssBundles = serverProps.cssBundles as string[]
const router = createRouter({ routeTree }) // Render the app
const helmetContext = {}
const app = renderToString(
<HelmetProvider context={helmetContext}>
<RouterProvider router={router} serverProps={props} />
<RouterProvider router={router} serverProps={serverProps as never} />
</HelmetProvider>,
)
+1 -2
View File
@@ -3,6 +3,5 @@
"compilerOptions": {
"jsx": "react"
},
"include": ["src", "tests", "vite.config.ts"],
"exclude": ["vite.config.ts"]
"include": ["src", "tests", "vite.config.ts"]
}
+284 -236
View File
File diff suppressed because it is too large Load Diff
+29 -18
View File
@@ -1,28 +1,39 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
// Typechecking
"strict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"checkJs": true,
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ES2022",
"moduleResolution": "Bundler",
"noEmit": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noUncheckedIndexedAccess": true,
// Modules
"module": "ES2022",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2020"
},
"include": [".eslintrc.cjs", "prettier.config.js"],
"exclude": ["node_modules"]
// Emit
"noEmit": true,
"declaration": true,
// JavaScript Support
"allowJs": true,
"checkJs": true,
// Interop Constraints
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
// Language and Environment
"target": "ES2020",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
// Completeness
"skipLibCheck": true
}
}
+1
View File
@@ -1,5 +1,6 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["eslint.config.js"],
"tasks": {
"lint": {
"dependsOn": ["^build"]