From 5952af9722a87affed5a75a959a6bf4009bde3bf Mon Sep 17 00:00:00 2001 From: Leonardo Rick Date: Fri, 11 Jul 2025 14:10:54 +0100 Subject: [PATCH] feat(useNavigate): improve useNavigate to check if route exists in ember --- .gitignore | 1 + pnpm-lock.yaml | 42 +++++++++++++++++-- react-migration-toolkit/package.json | 5 ++- .../src/react/hooks/index.ts | 1 + .../src/react/hooks/use-is-ember-route.ts | 41 ++++++++++++++++++ .../src/react/hooks/use-navigate.ts | 31 ++++++++++++-- react-migration-toolkit/tsconfig.json | 1 + 7 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 react-migration-toolkit/src/react/hooks/use-is-ember-route.ts diff --git a/.gitignore b/.gitignore index 3e58c96..fd13e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ node_modules/ coverage/ npm-debug.log* yarn-error.log +.yalc # ember-try /.node_modules.ember-try/ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd45bed..71b6d50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,7 +60,7 @@ importers: version: 2.2.2(@babel/core@7.24.6) ember-element-helper: specifier: ^0.8.6 - version: 0.8.6(@glint/environment-ember-loose@1.4.0(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(@types/ember__array@4.0.10(@babel/core@7.24.6))(@types/ember__component@4.0.22(@babel/core@7.24.6))(@types/ember__controller@4.0.12(@babel/core@7.24.6))(@types/ember__object@4.0.12(@babel/core@7.24.6))(@types/ember__routing@4.0.22(@babel/core@7.24.6))(ember-cli-htmlbars@6.3.0)(ember-modifier@4.1.0(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5))))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)) + version: 0.8.6(@glint/environment-ember-loose@1.4.0(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(@types/ember__array@4.0.10(@babel/core@7.24.6))(@types/ember__component@4.0.22(@babel/core@7.24.6))(@types/ember__controller@4.0.12(@babel/core@7.24.6))(@types/ember__object@4.0.12(@babel/core@7.24.6))(@types/ember__routing@4.0.22(@babel/core@7.24.6))(ember-cli-htmlbars@6.3.0)(ember-modifier@4.1.0(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1))))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1)) ember-intl: specifier: ^7.0.0 && <7.0.8 version: 7.0.6(@ember/test-helpers@3.3.0(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1))(webpack@5.97.1))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(typescript@5.4.2)(webpack@5.97.1) @@ -79,6 +79,9 @@ importers: react-intl: specifier: ^6.6.8 version: 6.6.8(react@18.3.1)(typescript@5.4.2) + react-router: + specifier: 7.6.3 + version: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@babel/core': specifier: 7.24.6 @@ -4160,6 +4163,10 @@ packages: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + copy-dereference@1.0.0: resolution: {integrity: sha512-40TSLuhhbiKeszZhK9LfNdazC67Ue4kq/gGwN5sdxEUWPXTIMmKmGmgD9mPfNKVAeecEW+NfEIpBaZoACCQLLw==} @@ -6508,6 +6515,7 @@ packages: lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} @@ -6535,6 +6543,7 @@ packages: lodash.omit@4.5.0: resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} + deprecated: This package is deprecated. Use destructuring assignment syntax instead. lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -6544,6 +6553,7 @@ packages: lodash.template@4.5.0: resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} + deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. lodash.templatesettings@4.2.0: resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} @@ -6919,6 +6929,7 @@ packages: node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -7636,6 +7647,16 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-router@7.6.3: + resolution: {integrity: sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -7876,7 +7897,7 @@ packages: right-pad@1.0.1: resolution: {integrity: sha512-bYBjgxmkvTAfgIYy328fmkwhp39v8lwVgWhhrzxPV3yHtcSqyYKe9/XOhvW48UFjATg3VuJbpsp5822ACNvkmw==} engines: {node: '>= 0.10'} - deprecated: Please use String.prototype.padEnd() over this package. + deprecated: Use String.prototype.padEnd() instead rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} @@ -8058,6 +8079,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -15000,6 +15024,8 @@ snapshots: cookie@0.5.0: {} + cookie@1.0.2: {} + copy-dereference@1.0.0: {} copy-descriptor@0.1.1: {} @@ -15928,7 +15954,7 @@ snapshots: - '@babel/core' - supports-color - ember-element-helper@0.8.6(@glint/environment-ember-loose@1.4.0(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(@types/ember__array@4.0.10(@babel/core@7.24.6))(@types/ember__component@4.0.22(@babel/core@7.24.6))(@types/ember__controller@4.0.12(@babel/core@7.24.6))(@types/ember__object@4.0.12(@babel/core@7.24.6))(@types/ember__routing@4.0.22(@babel/core@7.24.6))(ember-cli-htmlbars@6.3.0)(ember-modifier@4.1.0(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5))))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)): + ember-element-helper@0.8.6(@glint/environment-ember-loose@1.4.0(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(@types/ember__array@4.0.10(@babel/core@7.24.6))(@types/ember__component@4.0.22(@babel/core@7.24.6))(@types/ember__controller@4.0.12(@babel/core@7.24.6))(@types/ember__object@4.0.12(@babel/core@7.24.6))(@types/ember__routing@4.0.22(@babel/core@7.24.6))(ember-cli-htmlbars@6.3.0)(ember-modifier@4.1.0(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1))))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1)): dependencies: '@embroider/addon-shim': 1.8.7 '@embroider/util': 1.13.0(@glint/environment-ember-loose@1.4.0(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(@types/ember__array@4.0.10(@babel/core@7.24.6))(@types/ember__component@4.0.22(@babel/core@7.24.6))(@types/ember__controller@4.0.12(@babel/core@7.24.6))(@types/ember__object@4.0.12(@babel/core@7.24.6))(@types/ember__routing@4.0.22(@babel/core@7.24.6))(ember-cli-htmlbars@6.3.0)(ember-modifier@4.1.0(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1))))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(ember-source@5.8.0(@babel/core@7.24.6)(@glimmer/component@1.1.2(@babel/core@7.24.6))(@glint/template@1.4.0(patch_hash=ygxmz5m3qdq3xzop3qfpvutpia))(rsvp@4.8.5)(webpack@5.97.1)) @@ -19489,6 +19515,14 @@ snapshots: react-is@16.13.1: {} + react-router@7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -20015,6 +20049,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 diff --git a/react-migration-toolkit/package.json b/react-migration-toolkit/package.json index 5fe5148..3876225 100644 --- a/react-migration-toolkit/package.json +++ b/react-migration-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@qonto/react-migration-toolkit", - "version": "3.0.0", + "version": "3.0.1", "description": "A toolkit to help migrate Ember components to React", "keywords": [ "ember-addon" @@ -137,9 +137,10 @@ } }, "peerDependencies": { + "ember-intl": "^7.0.0 && <7.0.8", "ember-source": "^3.28.0 || ^4.0.0 || ^5.0.0", "react-intl": "^6.6.8", - "ember-intl": "^7.0.0 && <7.0.8" + "react-router": "7.6.3" }, "peerDependenciesMeta": { "react-intl": { diff --git a/react-migration-toolkit/src/react/hooks/index.ts b/react-migration-toolkit/src/react/hooks/index.ts index 7b249a3..5387316 100644 --- a/react-migration-toolkit/src/react/hooks/index.ts +++ b/react-migration-toolkit/src/react/hooks/index.ts @@ -1,4 +1,5 @@ export { useEmberService } from './use-ember-service'; export { useApplicationInstance } from './use-application-instance'; export { useNavigate } from './use-navigate'; +export { useIsEmberRoute } from './use-is-ember-route'; export { useRouter } from './use-router'; diff --git a/react-migration-toolkit/src/react/hooks/use-is-ember-route.ts b/react-migration-toolkit/src/react/hooks/use-is-ember-route.ts new file mode 100644 index 0000000..e817cbc --- /dev/null +++ b/react-migration-toolkit/src/react/hooks/use-is-ember-route.ts @@ -0,0 +1,41 @@ +import { useLocation, useInRouterContext } from 'react-router'; +import type { UrlObject } from '../types/router'; +import { useEmberService } from './use-ember-service'; +import { useCallback } from 'react'; +/** + * Determines whether the current route is handled by the Ember router. + * + * This hook inspects the application's router to check if the current path + * matches any Ember route handler, excluding those that contain "react" in their name. + * We need to apply a regex to explicitly exclude 'react' routes because, + * if not, 'react-fallback' and similar named routes would match would match. + * + * @returns true if the current route is an Ember route (not a React route), otherwise false. + * + * @remarks + * This is useful for hybrid applications where both Ember and React routes may coexist, + * and you need to conditionally render or handle logic based on the routing system in use. + */ +export const useIsEmberRoute = (): (( + pathname?: string | UrlObject, +) => boolean) => { + // avoids Uncaught Error: useLocation() may be used only in the context of a component + if (!useInRouterContext()) { + return () => true; + } + const location = useLocation(); + const emberRouter = useEmberService('router'); + + return useCallback( + (path?: string | UrlObject) => { + // if inspection isn't possible, we assume Ember for backward compatibility + if (!emberRouter?.recognize) return true; + + const targetPath = + typeof path === 'string' ? path : path?.pathname || location.pathname; + const routeName = emberRouter.recognize(targetPath).name; + return Boolean(routeName) && !/react/.exec(routeName); + }, + [emberRouter, location.pathname], + ); +}; diff --git a/react-migration-toolkit/src/react/hooks/use-navigate.ts b/react-migration-toolkit/src/react/hooks/use-navigate.ts index b29f011..9832f21 100644 --- a/react-migration-toolkit/src/react/hooks/use-navigate.ts +++ b/react-migration-toolkit/src/react/hooks/use-navigate.ts @@ -3,13 +3,36 @@ import { PolymorphicNavigateContext, type PolymorphicNavigate, } from '../contexts/polymorphic-navigate-context'; +import type { NavigateOptions } from 'react-router'; +import { + useInRouterContext, + useNavigate as useReactRouterNavigate, +} from 'react-router'; +import { useIsEmberRoute } from './use-is-ember-route'; +import type { UrlObject } from '../types/router'; export const useNavigate = (): PolymorphicNavigate => { - const navigate = useContext(PolymorphicNavigateContext); - if (!navigate) { + const contextNavigate = useContext(PolymorphicNavigateContext); + if (!contextNavigate) { throw new Error( - 'this hook can only be used with PolymorphicNavigateProvider', + 'useNavigate hook can only be used with PolymorphicNavigateProvider', ); } - return navigate; + + // avoids Uncaught Error: useNavigate() may be used only in the context of a component + if (!useInRouterContext()) { + return (to: string | UrlObject, options?: NavigateOptions) => { + return contextNavigate(to, options); + }; + } + + const isEmberRoute = useIsEmberRoute(); + const reactRouterNavigate = useReactRouterNavigate(); + + return (to: string | UrlObject, options?: NavigateOptions) => { + if (isEmberRoute(to)) { + return contextNavigate(to, options); + } + return reactRouterNavigate(to, options); + }; }; diff --git a/react-migration-toolkit/tsconfig.json b/react-migration-toolkit/tsconfig.json index 0f84559..02bb943 100644 --- a/react-migration-toolkit/tsconfig.json +++ b/react-migration-toolkit/tsconfig.json @@ -8,6 +8,7 @@ "jsx": "react-jsx", "allowJs": true, "declarationDir": "declarations", + "lib": ["dom", "dom.iterable", "esnext"], /** https://www.typescriptlang.org/tsconfig#noEmit