mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
chore(deps-dev): update eslint to v9 (#147)
This commit is contained in:
committed by
GitHub
parent
5fc68a1926
commit
11d425faa9
@@ -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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -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
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387",
|
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo dev --filter=./packages/*",
|
"dev": "turbo dev --filter=./packages/*",
|
||||||
@@ -21,24 +23,21 @@
|
|||||||
"author": "Valerio Ageno",
|
"author": "Valerio Ageno",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "9.15.0",
|
||||||
"@tanstack/config": "^0.7.0",
|
"@tanstack/config": "^0.7.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"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": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"prettier": "^3.2.4",
|
"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",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"turbo": "^2.2.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
"lint": "eslint .",
|
||||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||||
"types": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
import { buildRouteConfig } from './build-route-config'
|
import { buildRouteConfig } from './build-route-config'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -61,11 +62,11 @@ const routes = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('buildRouteConfig works', async () => {
|
describe('buildRouteConfig works', () => {
|
||||||
it('Should build the correct config', () => {
|
it('Should build the correct config', () => {
|
||||||
const expectedConfig =
|
|
||||||
'PostsMyPostRoute,PostsIndexRoute,PostspostIndexRoute'
|
|
||||||
const config = buildRouteConfig(routes)
|
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 * as fsp from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
import { format } from 'prettier'
|
||||||
|
|
||||||
import { buildRouteConfig } from './build-route-config'
|
import { buildRouteConfig } from './build-route-config'
|
||||||
import { hasParentRoute } from './has-parent-route'
|
import { hasParentRoute } from './has-parent-route'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cleanPath,
|
cleanPath,
|
||||||
determineNodePath,
|
determineNodePath,
|
||||||
@@ -19,7 +21,6 @@ import {
|
|||||||
import type { Config, RouteNode } from './types'
|
import type { Config, RouteNode } from './types'
|
||||||
import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
|
import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
|
||||||
|
|
||||||
import { format } from 'prettier'
|
|
||||||
import { sortRouteNodes } from './sort-route-nodes'
|
import { sortRouteNodes } from './sort-route-nodes'
|
||||||
import isDefaultExported from './utils/is-default-exported'
|
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
|
// Loop over the flat list of routeNodes and
|
||||||
// build up a tree based on the routeNodes' routePath
|
// 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)
|
const parentRoute = hasParentRoute(routeNodes, node, node.routePath)
|
||||||
|
|
||||||
if (parentRoute) node.parent = parentRoute
|
if (parentRoute) node.parent = parentRoute
|
||||||
@@ -149,7 +150,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const node of preRouteNodes) {
|
for (const node of preRouteNodes) {
|
||||||
await handleNode(node)
|
handleNode(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeConfigChildrenText = buildRouteConfig(routeNodes)
|
const routeConfigChildrenText = buildRouteConfig(routeNodes)
|
||||||
@@ -237,7 +238,8 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
|
|||||||
|
|
||||||
const routeTreeContent = await fsp
|
const routeTreeContent = await fsp
|
||||||
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
|
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
|
||||||
.catch((err: any) => {
|
.catch((e) => {
|
||||||
|
const err = e as Error & { code?: string }
|
||||||
if (err.code === 'ENOENT') {
|
if (err.code === 'ENOENT') {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
import { hasParentRoute } from './has-parent-route'
|
import { hasParentRoute } from './has-parent-route'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -70,7 +71,7 @@ const dynamicRoute = {
|
|||||||
cleanedPath: '/posts/[post]',
|
cleanedPath: '/posts/[post]',
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('hasParentRoute works', async () => {
|
describe('hasParentRoute works', () => {
|
||||||
it('Should detect parent route', () => {
|
it('Should detect parent route', () => {
|
||||||
const parentRoute = hasParentRoute(routes, myPost, myPost.path)
|
const parentRoute = hasParentRoute(routes, myPost, myPost.path)
|
||||||
expect(parentRoute).toStrictEqual(parent)
|
expect(parentRoute).toStrictEqual(parent)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { routeGenerator } from './generator'
|
|
||||||
|
|
||||||
import { normalize } from 'path'
|
import { normalize } from 'path'
|
||||||
|
|
||||||
import type { Plugin } from 'vite'
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
import { routeGenerator } from './generator'
|
||||||
|
|
||||||
const ROUTES_DIRECTORY_PATH = './src/routes'
|
const ROUTES_DIRECTORY_PATH = './src/routes'
|
||||||
|
|
||||||
let lock = false
|
let lock = false
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
import { sortRouteNodes } from './sort-route-nodes'
|
import { sortRouteNodes } from './sort-route-nodes'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -106,7 +107,7 @@ const expectedSorting = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('sortRouteNodes works', async () => {
|
describe('sortRouteNodes works', () => {
|
||||||
it('Should correctly sort the nodes', () => {
|
it('Should correctly sort the nodes', () => {
|
||||||
const sorted = sortRouteNodes(routes)
|
const sorted = sortRouteNodes(routes)
|
||||||
expect(sorted).toStrictEqual(expectedSorting)
|
expect(sorted).toStrictEqual(expectedSorting)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function routePathToVariable(routePath: string): string {
|
|||||||
|
|
||||||
export function multiSortBy<T>(
|
export function multiSortBy<T>(
|
||||||
arr: T[],
|
arr: T[],
|
||||||
accessors: ((item: T) => any)[] = [(d): any => d],
|
accessors: ((item: T) => unknown)[] = [(d): unknown => d],
|
||||||
): T[] {
|
): T[] {
|
||||||
return arr
|
return arr
|
||||||
.map((d, i) => [d, i] as const)
|
.map((d, i) => [d, i] as const)
|
||||||
@@ -55,7 +55,7 @@ export function multiSortBy<T>(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return ao > bo ? 1 : -1
|
return (ao as number) > (bo as number) ? 1 : -1
|
||||||
}
|
}
|
||||||
|
|
||||||
return ai - bi
|
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 ('_').
|
* Removes all segments from a given path that start with an underscore ('_').
|
||||||
*
|
*
|
||||||
* @param {string} routePath - The path from which to remove segments. Defaults to '/'.
|
* @param routePath - The path from which to remove segments. Defaults to '/'.
|
||||||
* @returns {string} The path with all underscore-prefixed segments removed.
|
* @returns The path with all underscore-prefixed segments removed.
|
||||||
* @example
|
* @example
|
||||||
* removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
|
* removeLayoutSegments('/workspace/_auth/foo') // '/workspace/foo'
|
||||||
*/
|
*/
|
||||||
@@ -101,8 +101,9 @@ export function trimPathLeft(pathToTrim: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeLastSlash(str: 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.substring(0, str.length - 1)
|
||||||
|
}
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
import { routeGenerator } from '../src/generator'
|
import { routeGenerator } from '../src/generator'
|
||||||
|
|
||||||
describe('generator works', async () => {
|
describe('generator works', async () => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"include": ["src", "vite.config.ts"]
|
"include": ["src", "tests", "vite.config.ts"],
|
||||||
|
"exclude": ["tests/generator/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
"lint": "eslint .",
|
||||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||||
"types": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import type { Plugin } from 'vite'
|
|
||||||
import * as babel from '@babel/core'
|
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 * as t from '@babel/types'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import type { PluginItem } from '@babel/core'
|
||||||
import type {
|
import type {
|
||||||
Identifier,
|
Identifier,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
@@ -18,6 +9,13 @@ import type {
|
|||||||
StringLiteral,
|
StringLiteral,
|
||||||
} from '@babel/types'
|
} from '@babel/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
TUONO_MAIN_PACKAGE,
|
||||||
|
TUONO_DYNAMIC_FN_ID,
|
||||||
|
TUONO_LAZY_FN_ID,
|
||||||
|
} from './constants'
|
||||||
|
import { isTuonoDynamicFnImported } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [SERVER build]
|
* [SERVER build]
|
||||||
* This plugin just removes the `dynamic` imported function from any tuono import
|
* This plugin just removes the `dynamic` imported function from any tuono import
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { TUONO_MAIN_PACKAGE, TUONO_DYNAMIC_FN_ID } from './constants'
|
import type {
|
||||||
import type * as t from '@babel/types'
|
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 = (
|
export const isTuonoDynamicFnImported = (
|
||||||
path: babel.NodePath<t.ImportSpecifier>,
|
path: babel.NodePath<ImportSpecifier>,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if ((path.node.imported as Identifier).name !== TUONO_DYNAMIC_FN_ID) {
|
if ((path.node.imported as Identifier).name !== TUONO_DYNAMIC_FN_ID) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
+21
-4
@@ -1,4 +1,6 @@
|
|||||||
import { it, expect, describe } from 'vitest'
|
import { it, expect, describe } from 'vitest'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
import { LazyLoadingPlugin } from '../src'
|
import { LazyLoadingPlugin } from '../src'
|
||||||
|
|
||||||
const SOURCE_CODE = `
|
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', () => {
|
describe('Transpile tuono source', () => {
|
||||||
it('Into the client bundle', () => {
|
it('Into the client bundle', () => {
|
||||||
const bundle = LazyLoadingPlugin().transform?.(SOURCE_CODE, 'id')
|
const pluginTransform = getTransform()
|
||||||
|
const bundle = pluginTransform(SOURCE_CODE, 'id')
|
||||||
expect(bundle)
|
expect(bundle)
|
||||||
.toBe(`import { createRoute, lazyLoadComponent as dynamic } from 'tuono';
|
.toBe(`import { createRoute, lazyLoadComponent as dynamic } from 'tuono';
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
||||||
@@ -30,7 +44,8 @@ const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[po
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Into the server bundle', () => {
|
it('Into the server bundle', () => {
|
||||||
const bundle = LazyLoadingPlugin().transform?.(SOURCE_CODE, 'id', {
|
const pluginTransform = getTransform()
|
||||||
|
const bundle = pluginTransform(SOURCE_CODE, 'id', {
|
||||||
ssr: true,
|
ssr: true,
|
||||||
})
|
})
|
||||||
expect(bundle).toBe(`import { createRoute } from 'tuono';
|
expect(bundle).toBe(`import { createRoute } from 'tuono';
|
||||||
@@ -41,7 +56,8 @@ import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]";`)
|
|||||||
|
|
||||||
describe('Non tuono dynamic function', () => {
|
describe('Non tuono dynamic function', () => {
|
||||||
it('Into the client bundle', () => {
|
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';
|
expect(bundle).toBe(`import { createRoute } from 'tuono';
|
||||||
import { dynamic } from 'external-lib';
|
import { dynamic } from 'external-lib';
|
||||||
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
||||||
@@ -49,7 +65,8 @@ const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[po
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Into the server bundle', () => {
|
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,
|
ssr: true,
|
||||||
})
|
})
|
||||||
expect(bundle).toBe(`import { createRoute } from 'tuono';
|
expect(bundle).toBe(`import { createRoute } from 'tuono';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"include": ["src", "vite.config.ts"]
|
"include": ["src", "tests", "vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
"lint": "eslint .",
|
||||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||||
"types": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useRouter } from '../hooks/useRouter'
|
import type { AnchorHTMLAttributes } from 'react'
|
||||||
import type { AnchorHTMLAttributes, MouseEvent } from 'react'
|
|
||||||
import useRoute from '../hooks/useRoute'
|
|
||||||
import { useInView } from 'react-intersection-observer'
|
import { useInView } from 'react-intersection-observer'
|
||||||
|
|
||||||
|
import { useRouter } from '../hooks/useRouter'
|
||||||
|
import useRoute from '../hooks/useRoute'
|
||||||
|
|
||||||
interface TuonoLinkProps {
|
interface TuonoLinkProps {
|
||||||
/**
|
/**
|
||||||
* If "true" the route gets loaded when the link enters the viewport. Default "true"
|
* If "true" the route gets loaded when the link enters the viewport. Default "true"
|
||||||
@@ -28,7 +29,7 @@ export default function Link(
|
|||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleTransition = (e: MouseEvent<HTMLAnchorElement>): void => {
|
const handleTransition: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
props.onClick?.(e)
|
props.onClick?.(e)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import { useRouterStore } from '../hooks/useRouterStore'
|
import { useRouterStore } from '../hooks/useRouterStore'
|
||||||
import { RouteMatch } from './RouteMatch'
|
|
||||||
import NotFound from './NotFound'
|
|
||||||
import useRoute from '../hooks/useRoute'
|
import useRoute from '../hooks/useRoute'
|
||||||
|
|
||||||
interface MatchesProps {
|
import { RouteMatch } from './RouteMatch'
|
||||||
|
import NotFound from './NotFound'
|
||||||
|
|
||||||
|
interface MatchesProps<TServerSideProps = unknown> {
|
||||||
// user defined props
|
// user defined props
|
||||||
serverSideProps: any
|
serverSideProps: TServerSideProps
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
|
export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { useInternalRouter } from '../hooks/useInternalRouter'
|
||||||
|
|
||||||
import { RouteMatch } from './RouteMatch'
|
import { RouteMatch } from './RouteMatch'
|
||||||
import Link from './Link'
|
import Link from './Link'
|
||||||
import { useInternalRouter } from '../hooks/useInternalRouter'
|
|
||||||
|
|
||||||
export default function NotFound(): React.JSX.Element {
|
export default function NotFound(): React.JSX.Element {
|
||||||
const router = useInternalRouter()
|
const router = useInternalRouter()
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { afterEach, describe, expect, test, vi } from 'vitest'
|
import { afterEach, describe, expect, test, vi } from 'vitest'
|
||||||
import { RouteMatch } from './RouteMatch'
|
|
||||||
import { cleanup, render, screen } from '@testing-library/react'
|
import { cleanup, render, screen } from '@testing-library/react'
|
||||||
|
|
||||||
import type { Route } from '../route'
|
import type { Route } from '../route'
|
||||||
|
|
||||||
|
import { RouteMatch } from './RouteMatch'
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
import type { Route } from '../route'
|
import type { Route } from '../route'
|
||||||
import { useServerSideProps } from '../hooks/useServerSideProps'
|
import { useServerSideProps } from '../hooks/useServerSideProps'
|
||||||
|
|
||||||
interface MatchProps {
|
interface RouteMatchProps<TServerSideProps = unknown> {
|
||||||
route: Route
|
route: Route
|
||||||
// User defined server side props
|
// User defined server side props
|
||||||
serverSideProps: any
|
serverSideProps: TServerSideProps
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +17,7 @@ interface MatchProps {
|
|||||||
export const RouteMatch = ({
|
export const RouteMatch = ({
|
||||||
route,
|
route,
|
||||||
serverSideProps,
|
serverSideProps,
|
||||||
}: MatchProps): React.JSX.Element => {
|
}: RouteMatchProps): React.JSX.Element => {
|
||||||
const { data, isLoading } = useServerSideProps(route, serverSideProps)
|
const { data, isLoading } = useServerSideProps(route, serverSideProps)
|
||||||
|
|
||||||
const routes = React.useMemo(() => loadParentComponents(route), [route.id])
|
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[]
|
routes: Route[]
|
||||||
data: any
|
data: TData
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
index?: number
|
index?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParentProps {
|
|
||||||
children: React.ReactNode
|
|
||||||
data: any
|
|
||||||
isLoading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This component traverses and renders
|
* This component traverses and renders
|
||||||
* all the components that wraps the selected route (__root).
|
* 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 loadParentComponents = (route: Route, loader: Route[] = []): Route[] => {
|
||||||
const parentComponent = route.options?.getParentRoute?.()
|
const parentComponent = route.options.getParentRoute?.() as Route
|
||||||
|
|
||||||
loader.push(parentComponent)
|
loader.push(parentComponent)
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import * as React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { Router } from '../router'
|
import type { Router } from '../router'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const routerContext = React.createContext<Router>(null!)
|
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') {
|
if (typeof document === 'undefined') {
|
||||||
return routerContext
|
return routerContext
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window[TUONO_CONTEXT as any]) {
|
if (window[TUONO_CONTEXT_GLOBAL_NAME]) {
|
||||||
return window[TUONO_CONTEXT as any]
|
return window[TUONO_CONTEXT_GLOBAL_NAME]
|
||||||
}
|
}
|
||||||
|
|
||||||
window[TUONO_CONTEXT as any] = routerContext as any
|
window[TUONO_CONTEXT_GLOBAL_NAME] = routerContext
|
||||||
|
|
||||||
return routerContext
|
return routerContext
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { ReactNode, JSX } from 'react'
|
import type { ReactNode, JSX } from 'react'
|
||||||
|
|
||||||
import { useListenBrowserUrlUpdates } from '../hooks/useListenBrowserUrlUpdates'
|
import { useListenBrowserUrlUpdates } from '../hooks/useListenBrowserUrlUpdates'
|
||||||
import { initRouterStore } from '../hooks/useRouterStore'
|
import { initRouterStore } from '../hooks/useRouterStore'
|
||||||
import type { ServerProps } from '../types'
|
import type { ServerProps } from '../types'
|
||||||
|
import type { Router } from '../router'
|
||||||
|
|
||||||
import { getRouterContext } from './RouterContext'
|
import { getRouterContext } from './RouterContext'
|
||||||
import { Matches } from './Matches'
|
import { Matches } from './Matches'
|
||||||
|
|
||||||
type Router = any
|
|
||||||
|
|
||||||
interface RouterContextProviderProps {
|
interface RouterContextProviderProps {
|
||||||
router: Router
|
router: Router
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RouterProviderProps {
|
|
||||||
router: Router
|
|
||||||
serverProps?: ServerProps
|
|
||||||
}
|
|
||||||
|
|
||||||
function RouterContextProvider({
|
function RouterContextProvider({
|
||||||
router,
|
router,
|
||||||
children,
|
children,
|
||||||
@@ -27,7 +23,11 @@ function RouterContextProvider({
|
|||||||
router.update({
|
router.update({
|
||||||
...router.options,
|
...router.options,
|
||||||
...rest,
|
...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: {
|
context: {
|
||||||
|
// @ts-expect-error see previous
|
||||||
...router.options.context,
|
...router.options.context,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -41,6 +41,11 @@ function RouterContextProvider({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RouterProviderProps {
|
||||||
|
router: Router
|
||||||
|
serverProps?: ServerProps
|
||||||
|
}
|
||||||
|
|
||||||
export function RouterProvider({
|
export function RouterProvider({
|
||||||
router,
|
router,
|
||||||
serverProps,
|
serverProps,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { ReactElement, ComponentType } from 'react'
|
import type { ReactElement, ComponentType } from 'react'
|
||||||
|
|
||||||
import type { RouteComponent } from './types'
|
import type { RouteComponent } from './types'
|
||||||
|
|
||||||
type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
|
type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
|
||||||
|
|||||||
@@ -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 * as React from 'react'
|
||||||
|
|
||||||
import { getRouterContext } from '../components/RouterContext'
|
import { getRouterContext } from '../components/RouterContext'
|
||||||
import type { Router } from '../router'
|
import type { Router } from '../router'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useRouterStore } from './useRouterStore'
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { useRouterStore } from './useRouterStore'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook is meant to handle just browser related location updates
|
* This hook is meant to handle just browser related location updates
|
||||||
* like the back and forward buttons.
|
* like the back and forward buttons.
|
||||||
@@ -8,8 +9,9 @@ import { useEffect } from 'react'
|
|||||||
export const useListenBrowserUrlUpdates = (): void => {
|
export const useListenBrowserUrlUpdates = (): void => {
|
||||||
const updateLocation = useRouterStore((st) => st.updateLocation)
|
const updateLocation = useRouterStore((st) => st.updateLocation)
|
||||||
|
|
||||||
const updateLocationOnPopStateChange = ({ target }: any): void => {
|
const updateLocationOnPopStateChange = ({ target }: PopStateEvent): void => {
|
||||||
const { pathname, hash, href, search } = target.location
|
const { pathname, hash, href, search } = (target as typeof window).location
|
||||||
|
|
||||||
updateLocation({
|
updateLocation({
|
||||||
pathname,
|
pathname,
|
||||||
hash,
|
hash,
|
||||||
@@ -18,8 +20,10 @@ export const useListenBrowserUrlUpdates = (): void => {
|
|||||||
search: Object.fromEntries(new URLSearchParams(search)),
|
search: Object.fromEntries(new URLSearchParams(search)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('popstate', updateLocationOnPopStateChange)
|
window.addEventListener('popstate', updateLocationOnPopStateChange)
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
window.removeEventListener('popstate', updateLocationOnPopStateChange)
|
window.removeEventListener('popstate', updateLocationOnPopStateChange)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { afterEach, describe, expect, test, vi } from 'vitest'
|
import { afterEach, describe, expect, test, vi } from 'vitest'
|
||||||
import useRoute from './useRoute'
|
|
||||||
import { cleanup } from '@testing-library/react'
|
import { cleanup } from '@testing-library/react'
|
||||||
|
|
||||||
|
import useRoute from './useRoute'
|
||||||
|
|
||||||
describe('Test useRoute fn', () => {
|
describe('Test useRoute fn', () => {
|
||||||
afterEach(() => {
|
afterEach(cleanup)
|
||||||
cleanup()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('match routes by ids', () => {
|
test('match routes by ids', () => {
|
||||||
vi.mock('./useInternalRouter.tsx', () => ({
|
vi.mock('./useInternalRouter.tsx', () => ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Route } from '../route'
|
import type { Route } from '../route'
|
||||||
|
|
||||||
import { useInternalRouter } from './useInternalRouter'
|
import { useInternalRouter } from './useInternalRouter'
|
||||||
|
|
||||||
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
|
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import type { Route } from '../route'
|
import type { Route } from '../route'
|
||||||
import { useRouterStore } from './useRouterStore'
|
|
||||||
import { fromUrlToParsedLocation } from '../utils/from-url-to-parsed-location'
|
import { fromUrlToParsedLocation } from '../utils/from-url-to-parsed-location'
|
||||||
|
|
||||||
|
import { useRouterStore } from './useRouterStore'
|
||||||
|
|
||||||
const isServer = typeof document === 'undefined'
|
const isServer = typeof document === 'undefined'
|
||||||
|
|
||||||
interface UseServerSidePropsReturn {
|
interface UseServerSidePropsReturn<TData> {
|
||||||
data: any
|
data: TData
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__TUONO_SSR_PROPS__: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TuonoApi {
|
interface TuonoApi {
|
||||||
data?: any
|
data?: any
|
||||||
info: {
|
info: {
|
||||||
@@ -26,7 +22,7 @@ interface TuonoApi {
|
|||||||
const fetchClientSideData = async (): Promise<TuonoApi> => {
|
const fetchClientSideData = async (): Promise<TuonoApi> => {
|
||||||
const slash = location.pathname.endsWith('/') ? '' : '/'
|
const slash = location.pathname.endsWith('/') ? '' : '/'
|
||||||
const res = await fetch(`/__tuono/data${location.pathname}${slash}data.json`)
|
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
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ export function useServerSideProps<T>(
|
|||||||
route: Route,
|
route: Route,
|
||||||
// User defined props
|
// User defined props
|
||||||
serverSideProps: T,
|
serverSideProps: T,
|
||||||
): UseServerSidePropsReturn {
|
): UseServerSidePropsReturn<T> {
|
||||||
const isFirstRendering = useRef<boolean>(true)
|
const isFirstRendering = useRef<boolean>(true)
|
||||||
const [location, updateLocation] = useRouterStore((st) => [
|
const [location, updateLocation] = useRouterStore((st) => [
|
||||||
st.location,
|
st.location,
|
||||||
@@ -48,15 +44,15 @@ export function useServerSideProps<T>(
|
|||||||
])
|
])
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(
|
const [isLoading, setIsLoading] = useState<boolean>(
|
||||||
// Force loading if has handler
|
// Force loading if has handler
|
||||||
route.options.hasHandler &&
|
!!route.options.hasHandler &&
|
||||||
// Avoid loading on the server
|
// Avoid loading on the server
|
||||||
!isServer &&
|
!isServer &&
|
||||||
// Avoid loading if first rendering
|
// Avoid loading if first rendering
|
||||||
!isFirstRendering.current,
|
!isFirstRendering.current,
|
||||||
)
|
)
|
||||||
|
|
||||||
const [data, setData] = useState<any>(
|
const [data, setData] = useState<T | undefined>(
|
||||||
serverSideProps ?? window.__TUONO_SSR_PROPS__?.props,
|
(serverSideProps ?? window.__TUONO_SSR_PROPS__?.props) as T,
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,6 +64,8 @@ export function useServerSideProps<T>(
|
|||||||
}
|
}
|
||||||
// After client side routing load again the remote data
|
// After client side routing load again the remote data
|
||||||
if (route.options.hasHandler) {
|
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> => {
|
;(async (): Promise<void> => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -86,7 +84,7 @@ export function useServerSideProps<T>(
|
|||||||
updateLocation(parsedLocation)
|
updateLocation(parsedLocation)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setData(response.data)
|
setData(response.data as T)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error('Failed loading Server Side Data', { cause: error })
|
throw Error('Failed loading Server Side Data', { cause: error })
|
||||||
} finally {
|
} finally {
|
||||||
@@ -101,5 +99,5 @@ export function useServerSideProps<T>(
|
|||||||
}
|
}
|
||||||
}, [location.pathname])
|
}, [location.pathname])
|
||||||
|
|
||||||
return { isLoading, data }
|
return { isLoading, data: data as T }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,35 +3,41 @@ import type { RouteComponent } from './types'
|
|||||||
import { trimPathLeft, joinPaths } from './utils'
|
import { trimPathLeft, joinPaths } from './utils'
|
||||||
|
|
||||||
interface RouteOptions {
|
interface RouteOptions {
|
||||||
|
id?: string
|
||||||
isRoot?: boolean
|
isRoot?: boolean
|
||||||
getParentRoute?: () => Route
|
getParentRoute?: () => Route
|
||||||
path?: string
|
path?: string
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
|
hasHandler?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRoute(options: RouteOptions): Route {
|
export function createRoute(options: RouteOptions): Route {
|
||||||
return new Route(options)
|
return new Route(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rootRouteId = '__root__'
|
const rootRouteId = '__root__'
|
||||||
|
|
||||||
export class Route {
|
export class Route {
|
||||||
parentRoute!: any
|
options: RouteOptions
|
||||||
|
|
||||||
id?: string
|
id?: string
|
||||||
fullPath!: string
|
isRoot: boolean
|
||||||
path?: string
|
path?: string
|
||||||
options: any
|
fullPath!: string
|
||||||
|
|
||||||
children?: Route[]
|
children?: Route[]
|
||||||
|
parentRoute!: any
|
||||||
|
/** @todo check if this is required */
|
||||||
router: RouterType
|
router: RouterType
|
||||||
isRoot: boolean
|
|
||||||
originalIndex?: number
|
originalIndex?: number
|
||||||
component: RouteComponent
|
component: RouteComponent
|
||||||
|
|
||||||
|
'$$typeof': symbol
|
||||||
|
|
||||||
constructor(options: RouteOptions) {
|
constructor(options: RouteOptions) {
|
||||||
this.isRoot = options.isRoot ?? typeof options.getParentRoute !== 'function'
|
this.isRoot = options.isRoot ?? typeof options.getParentRoute !== 'function'
|
||||||
this.options = options
|
this.options = options
|
||||||
;(this as any).$$typeof = Symbol.for('react.memo')
|
this.$$typeof = Symbol.for('react.memo')
|
||||||
|
|
||||||
this.component = options.component
|
this.component = options.component
|
||||||
}
|
}
|
||||||
@@ -39,9 +45,9 @@ export class Route {
|
|||||||
init = (originalIndex: number): void => {
|
init = (originalIndex: number): void => {
|
||||||
this.originalIndex = originalIndex
|
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) {
|
if (isRoot) {
|
||||||
this.path = rootRouteId
|
this.path = rootRouteId
|
||||||
@@ -54,7 +60,7 @@ export class Route {
|
|||||||
path = trimPathLeft(path)
|
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
|
// Strip the parentId prefix from the first level of children
|
||||||
let id = isRoot ? rootRouteId : joinPaths([customId])
|
let id = isRoot ? rootRouteId : joinPaths([customId])
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { ComponentType as ReactComponentType } from 'react'
|
|||||||
import { trimPath, trimPathRight } from './utils'
|
import { trimPath, trimPathRight } from './utils'
|
||||||
import type { Route } from './route'
|
import type { Route } from './route'
|
||||||
|
|
||||||
type RouteTree = any
|
type RouteTree = Route
|
||||||
|
|
||||||
interface CreateRouter {
|
interface CreateRouterOptions {
|
||||||
routeTree: RouteTree
|
routeTree: RouteTree
|
||||||
basePath?: string
|
basePath?: string
|
||||||
options?: RouteOptions
|
options?: RouterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RouteOptions {
|
interface RouterOptions {
|
||||||
component?: ReactComponentType
|
component?: ReactComponentType
|
||||||
hasHandler?: boolean
|
hasHandler?: boolean
|
||||||
routeTree?: RouteTree
|
routeTree?: RouteTree
|
||||||
@@ -19,32 +19,39 @@ interface RouteOptions {
|
|||||||
|
|
||||||
export type RouterType = any
|
export type RouterType = any
|
||||||
|
|
||||||
export function createRouter(options: CreateRouter): Router {
|
export function createRouter(options: CreateRouterOptions): Router {
|
||||||
return new Router(options)
|
return new Router(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
options?: RouteOptions
|
options?: RouterOptions
|
||||||
basePath = '/'
|
basePath = '/'
|
||||||
routeTree: any
|
routeTree?: RouteTree
|
||||||
|
|
||||||
|
/** Not used */
|
||||||
history: any
|
history: any
|
||||||
|
|
||||||
isServer = typeof document === 'undefined'
|
isServer = typeof document === 'undefined'
|
||||||
|
|
||||||
routesById: Record<string, Route> = {}
|
routesById: Record<string, Route> = {}
|
||||||
// Not used
|
|
||||||
|
/** Not used */
|
||||||
routesByPath: Record<string, Route> = {}
|
routesByPath: Record<string, Route> = {}
|
||||||
|
|
||||||
|
/** Not used */
|
||||||
flatRoutes: any
|
flatRoutes: any
|
||||||
|
|
||||||
constructor(options: CreateRouter) {
|
constructor(options: CreateRouterOptions) {
|
||||||
this.update({
|
this.update({
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!this.isServer) {
|
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 = {
|
||||||
...this.options,
|
...this.options,
|
||||||
...newOptions,
|
...newOptions,
|
||||||
@@ -82,7 +89,7 @@ export class Router {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
recurseRoutes([this.routeTree])
|
recurseRoutes([this.routeTree as Route])
|
||||||
}
|
}
|
||||||
|
|
||||||
#updateBasePath = (basePath?: string): void => {
|
#updateBasePath = (basePath?: string): void => {
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ export interface Segment {
|
|||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerProps {
|
export interface ServerProps<TProps = unknown> {
|
||||||
router: {
|
router: {
|
||||||
href: string
|
href: string
|
||||||
pathname: string
|
pathname: string
|
||||||
searchStr: string
|
searchStr: string
|
||||||
}
|
}
|
||||||
props: any
|
props: TProps
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RouteProps {
|
export interface RouteProps<TData = unknown> {
|
||||||
data: any
|
data: TData
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
"lint": "eslint .",
|
||||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||||
"types": "tsc --noEmit",
|
"types": "tsc --noEmit",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { hydrateRoot } from 'react-dom/client'
|
import { hydrateRoot } from 'react-dom/client'
|
||||||
import { RouterProvider, createRouter } from 'tuono-router'
|
import { RouterProvider, createRouter } from 'tuono-router'
|
||||||
|
import type { createRoute } from 'tuono-router'
|
||||||
import { HelmetProvider } from 'react-helmet-async'
|
import { HelmetProvider } from 'react-helmet-async'
|
||||||
|
|
||||||
type RouteTree = any
|
type RouteTree = ReturnType<typeof createRoute>
|
||||||
|
|
||||||
export function hydrate(routeTree: RouteTree): void {
|
export function hydrate(routeTree: RouteTree): void {
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { renderToString, renderToStaticMarkup } from 'react-dom/server'
|
|||||||
import type { HelmetServerState } from 'react-helmet-async'
|
import type { HelmetServerState } from 'react-helmet-async'
|
||||||
import { HelmetProvider } from 'react-helmet-async'
|
import { HelmetProvider } from 'react-helmet-async'
|
||||||
import { RouterProvider, createRouter } from 'tuono-router'
|
import { RouterProvider, createRouter } from 'tuono-router'
|
||||||
|
import type { createRoute } from 'tuono-router'
|
||||||
|
|
||||||
type RouteTree = any
|
type RouteTree = ReturnType<typeof createRoute>
|
||||||
type Mode = 'Dev' | 'Prod'
|
type Mode = 'Dev' | 'Prod'
|
||||||
|
|
||||||
const TUONO_DEV_SERVER_PORT = 3000
|
const TUONO_DEV_SERVER_PORT = 3000
|
||||||
@@ -37,17 +38,20 @@ function generateJsScripts(jsBundles: string[], mode: Mode): string {
|
|||||||
|
|
||||||
export function serverSideRendering(routeTree: RouteTree) {
|
export function serverSideRendering(routeTree: RouteTree) {
|
||||||
return function render(payload: string | undefined): string {
|
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 mode = serverProps.mode as Mode
|
||||||
const jsBundles = props.jsBundles as string[]
|
const jsBundles = serverProps.jsBundles as string[]
|
||||||
const cssBundles = props.cssBundles as string[]
|
const cssBundles = serverProps.cssBundles as string[]
|
||||||
const router = createRouter({ routeTree }) // Render the app
|
const router = createRouter({ routeTree }) // Render the app
|
||||||
|
|
||||||
const helmetContext = {}
|
const helmetContext = {}
|
||||||
const app = renderToString(
|
const app = renderToString(
|
||||||
<HelmetProvider context={helmetContext}>
|
<HelmetProvider context={helmetContext}>
|
||||||
<RouterProvider router={router} serverProps={props} />
|
<RouterProvider router={router} serverProps={serverProps as never} />
|
||||||
</HelmetProvider>,
|
</HelmetProvider>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react"
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"include": ["src", "tests", "vite.config.ts"],
|
"include": ["src", "tests", "vite.config.ts"]
|
||||||
"exclude": ["vite.config.ts"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+284
-236
File diff suppressed because it is too large
Load Diff
+29
-18
@@ -1,28 +1,39 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
// Typechecking
|
||||||
"allowSyntheticDefaultImports": true,
|
"strict": true,
|
||||||
"allowUnreachableCode": false,
|
"allowUnreachableCode": false,
|
||||||
"allowUnusedLabels": 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,
|
"noImplicitReturns": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
// Emit
|
||||||
"target": "ES2020"
|
"noEmit": true,
|
||||||
},
|
"declaration": true,
|
||||||
"include": [".eslintrc.cjs", "prettier.config.js"],
|
|
||||||
"exclude": ["node_modules"]
|
// 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,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": ["eslint.config.js"],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"lint": {
|
"lint": {
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
|
|||||||
Reference in New Issue
Block a user