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", "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"
} }
} }
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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)
+6 -5
View File
@@ -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 () => {
+2 -1
View File
@@ -1,4 +1,5 @@
{ {
"extends": "../../tsconfig.json", "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": { "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",
+9 -11
View File
@@ -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
+7 -4
View File
@@ -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
@@ -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 -1
View File
@@ -1,4 +1,4 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"include": ["src", "vite.config.ts"] "include": ["src", "tests", "vite.config.ts"]
} }
+1 -1
View File
@@ -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",
+5 -4
View File
@@ -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)
+6 -4
View File
@@ -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 {
+3 -1
View File
@@ -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 {
+13 -12
View File
@@ -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
View File
@@ -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> }>
+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 * 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)
} }
+3 -4
View File
@@ -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
View File
@@ -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 }
} }
+15 -9
View File
@@ -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])
+19 -12
View File
@@ -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 => {
+4 -4
View File
@@ -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
} }
+1 -1
View File
@@ -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",
+2 -1
View File
@@ -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
+10 -6
View File
@@ -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>,
) )
+1 -2
View File
@@ -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"]
} }
+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", "$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
View File
@@ -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"]