From c3ca61f9a49d7f72b1f0813ac49c7b3c2a47a9e5 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:28:59 +0900 Subject: [PATCH 01/12] fix(link): inject URL resolver via context (FEP-2677) --- .changeset/fep-2677-link-url-resolver.md | 5 + .pnp.cjs | 150 +++++++----------- extensions/link/README.md | 27 +++- extensions/link/package.json | 30 +++- extensions/link/src/Link.spec.tsx | 74 +++++++++ extensions/link/src/Link.tsx | 34 +--- .../link/src/LinkUrlResolverContext.tsx | 30 ++++ extensions/link/src/index.ts | 1 + yarn.lock | 11 +- 9 files changed, 228 insertions(+), 134 deletions(-) create mode 100644 .changeset/fep-2677-link-url-resolver.md create mode 100644 extensions/link/src/Link.spec.tsx create mode 100644 extensions/link/src/LinkUrlResolverContext.tsx diff --git a/.changeset/fep-2677-link-url-resolver.md b/.changeset/fep-2677-link-url-resolver.md new file mode 100644 index 000000000..b5fe9bf7d --- /dev/null +++ b/.changeset/fep-2677-link-url-resolver.md @@ -0,0 +1,5 @@ +--- +"@stackflow/link": patch +--- + +Inject a URL resolver into Link so route selection, encoding, and URL pattern options stay consistent with the routing plugin. diff --git a/.pnp.cjs b/.pnp.cjs index bda09f835..719f1ce47 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -115,7 +115,7 @@ const RAW_RUNTIME_STATE = ["@stackflow/plugin-blocker", ["workspace:extensions/plugin-blocker"]],\ ["@stackflow/plugin-devtools", ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-devtools", "workspace:extensions/plugin-devtools"]],\ ["@stackflow/plugin-google-analytics-4", ["workspace:extensions/plugin-google-analytics-4"]],\ - ["@stackflow/plugin-history-sync", ["virtual:0892efb9b4f386c24dba440a9d2a0ff9dd8aabcfd8ef5403c4c5f10c0d1c23fd4bad505b8db6b1207f29fb736fee165114aaa48c8dc6b509afc29633adbfcc46#workspace:extensions/plugin-history-sync", "workspace:extensions/plugin-history-sync"]],\ + ["@stackflow/plugin-history-sync", ["workspace:extensions/plugin-history-sync"]],\ ["@stackflow/plugin-lifecycle", ["workspace:extensions/plugin-lifecycle"]],\ ["@stackflow/plugin-renderer-basic", ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-renderer-basic", "workspace:extensions/plugin-renderer-basic"]],\ ["@stackflow/plugin-renderer-web", ["workspace:extensions/plugin-renderer-web"]],\ @@ -4082,11 +4082,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:9.0.5", {\ - "packageLocation": "./.yarn/__virtual__/@graphql-tools-merge-virtual-07b692da0d/0/cache/@graphql-tools-merge-npm-9.0.5-a514a8ad9b-ec98638e78.zip/node_modules/@graphql-tools/merge/",\ + ["virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:9.0.5", {\ + "packageLocation": "./.yarn/__virtual__/@graphql-tools-merge-virtual-8a80188199/0/cache/@graphql-tools-merge-npm-9.0.5-a514a8ad9b-ec98638e78.zip/node_modules/@graphql-tools/merge/",\ "packageDependencies": [\ - ["@graphql-tools/merge", "virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:9.0.5"],\ - ["@graphql-tools/utils", "virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:10.5.2"],\ + ["@graphql-tools/merge", "virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:9.0.5"],\ + ["@graphql-tools/utils", "virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:10.5.2"],\ ["@types/graphql", null],\ ["graphql", "npm:16.9.0"],\ ["tslib", "npm:2.5.0"]\ @@ -4106,12 +4106,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:10.0.5", {\ - "packageLocation": "./.yarn/__virtual__/@graphql-tools-schema-virtual-4f4a2ef6c5/0/cache/@graphql-tools-schema-npm-10.0.5-ec976a54e7-f5c275a580.zip/node_modules/@graphql-tools/schema/",\ + ["virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:10.0.5", {\ + "packageLocation": "./.yarn/__virtual__/@graphql-tools-schema-virtual-e2c394a98e/0/cache/@graphql-tools-schema-npm-10.0.5-ec976a54e7-f5c275a580.zip/node_modules/@graphql-tools/schema/",\ "packageDependencies": [\ - ["@graphql-tools/schema", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:10.0.5"],\ - ["@graphql-tools/merge", "virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:9.0.5"],\ - ["@graphql-tools/utils", "virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:10.5.2"],\ + ["@graphql-tools/schema", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:10.0.5"],\ + ["@graphql-tools/merge", "virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:9.0.5"],\ + ["@graphql-tools/utils", "virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:10.5.2"],\ ["@types/graphql", null],\ ["graphql", "npm:16.9.0"],\ ["tslib", "npm:2.5.0"],\ @@ -4132,11 +4132,11 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:10.5.2", {\ - "packageLocation": "./.yarn/__virtual__/@graphql-tools-utils-virtual-00e3608fca/0/cache/@graphql-tools-utils-npm-10.5.2-b443d9e793-f400ef22ac.zip/node_modules/@graphql-tools/utils/",\ + ["virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:10.5.2", {\ + "packageLocation": "./.yarn/__virtual__/@graphql-tools-utils-virtual-19c2de4320/0/cache/@graphql-tools-utils-npm-10.5.2-b443d9e793-f400ef22ac.zip/node_modules/@graphql-tools/utils/",\ "packageDependencies": [\ - ["@graphql-tools/utils", "virtual:4f4a2ef6c5b21698ba5dede146cd1400d3b0066e665e90fde63114d6566072a2d8c630254158bf570aad90e7d5f8a18dee47d459302f95a950cc6965a62453fb#npm:10.5.2"],\ - ["@graphql-typed-document-node/core", "virtual:00e3608fcaa16b6ddd7a1963290d75adc04a8ab1e919013ad25c0f94907a08a0cdb703b417f7959a271d9fe2fd00278106b57ee76bd3ce93cd528ec56b309020#npm:3.2.0"],\ + ["@graphql-tools/utils", "virtual:e2c394a98ed6ed4ff5ab160f49cdc9ed949fe3a74c1065765c6992089552f8a4fb261c1937424b73b9937ac4a40d41a4620ffea8c1f1e37ac44df594052b2658#npm:10.5.2"],\ + ["@graphql-typed-document-node/core", "virtual:19c2de4320dd9f62c472a686692d634e8bf4cb35b61a8f822042f0e0a50906330e2fa4a0595186a3f6be61fbc9663328313d2d1123a2c876325ac36ad3a66603#npm:3.2.0"],\ ["@types/graphql", null],\ ["cross-inspect", "npm:1.0.1"],\ ["dset", "npm:3.1.3"],\ @@ -4158,10 +4158,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:00e3608fcaa16b6ddd7a1963290d75adc04a8ab1e919013ad25c0f94907a08a0cdb703b417f7959a271d9fe2fd00278106b57ee76bd3ce93cd528ec56b309020#npm:3.2.0", {\ - "packageLocation": "./.yarn/__virtual__/@graphql-typed-document-node-core-virtual-e83525f87c/0/cache/@graphql-typed-document-node-core-npm-3.2.0-505adb1e90-fa44443acc.zip/node_modules/@graphql-typed-document-node/core/",\ + ["virtual:19c2de4320dd9f62c472a686692d634e8bf4cb35b61a8f822042f0e0a50906330e2fa4a0595186a3f6be61fbc9663328313d2d1123a2c876325ac36ad3a66603#npm:3.2.0", {\ + "packageLocation": "./.yarn/__virtual__/@graphql-typed-document-node-core-virtual-9011abb4fb/0/cache/@graphql-typed-document-node-core-npm-3.2.0-505adb1e90-fa44443acc.zip/node_modules/@graphql-typed-document-node/core/",\ "packageDependencies": [\ - ["@graphql-typed-document-node/core", "virtual:00e3608fcaa16b6ddd7a1963290d75adc04a8ab1e919013ad25c0f94907a08a0cdb703b417f7959a271d9fe2fd00278106b57ee76bd3ce93cd528ec56b309020#npm:3.2.0"],\ + ["@graphql-typed-document-node/core", "virtual:19c2de4320dd9f62c472a686692d634e8bf4cb35b61a8f822042f0e0a50906330e2fa4a0595186a3f6be61fbc9663328313d2d1123a2c876325ac36ad3a66603#npm:3.2.0"],\ ["@types/graphql", null],\ ["graphql", "npm:16.9.0"]\ ],\ @@ -6663,25 +6663,28 @@ const RAW_RUNTIME_STATE = ["@stackflow/config", "workspace:config"],\ ["@stackflow/core", "workspace:core"],\ ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ - ["@stackflow/plugin-history-sync", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:1.12.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40stackflow%2Fplugin-history-sync%2F-%2Fplugin-history-sync-1.12.0.tgz"],\ ["@stackflow/react", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:integrations/react"],\ + ["@swc/core", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:1.6.6"],\ + ["@swc/jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:0.2.36"],\ + ["@types/jest", "npm:29.5.12"],\ ["@types/react", "npm:18.3.3"],\ + ["@types/react-dom", "npm:18.3.0"],\ ["@types/stackflow__core", null],\ - ["@types/stackflow__plugin-history-sync", null],\ ["@types/stackflow__react", null],\ ["esbuild", "npm:0.23.0"],\ ["esbuild-plugin-file-path-extensions", "npm:2.1.3"],\ + ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ + ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:3.0.2"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"]\ ],\ "packagePeers": [\ "@stackflow/core",\ - "@stackflow/plugin-history-sync",\ "@stackflow/react",\ "@types/react",\ "@types/stackflow__core",\ - "@types/stackflow__plugin-history-sync",\ "@types/stackflow__react",\ "react"\ ],\ @@ -6694,12 +6697,18 @@ const RAW_RUNTIME_STATE = ["@stackflow/config", "workspace:config"],\ ["@stackflow/core", "workspace:core"],\ ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ - ["@stackflow/plugin-history-sync", "virtual:0892efb9b4f386c24dba440a9d2a0ff9dd8aabcfd8ef5403c4c5f10c0d1c23fd4bad505b8db6b1207f29fb736fee165114aaa48c8dc6b509afc29633adbfcc46#workspace:extensions/plugin-history-sync"],\ ["@stackflow/react", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:integrations/react"],\ + ["@swc/core", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:1.6.6"],\ + ["@swc/jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:0.2.36"],\ + ["@types/jest", "npm:29.5.12"],\ ["@types/react", "npm:18.3.3"],\ + ["@types/react-dom", "npm:18.3.0"],\ ["esbuild", "npm:0.23.0"],\ ["esbuild-plugin-file-path-extensions", "npm:2.1.3"],\ + ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ + ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:3.0.2"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"]\ ],\ @@ -6810,7 +6819,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:18.3.3"],\ ["esbuild", "npm:0.27.3"],\ ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:6.1.3"],\ @@ -6907,57 +6916,6 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:0892efb9b4f386c24dba440a9d2a0ff9dd8aabcfd8ef5403c4c5f10c0d1c23fd4bad505b8db6b1207f29fb736fee165114aaa48c8dc6b509afc29633adbfcc46#workspace:extensions/plugin-history-sync", {\ - "packageLocation": "./.yarn/__virtual__/@stackflow-plugin-history-sync-virtual-0b16c4696c/1/extensions/plugin-history-sync/",\ - "packageDependencies": [\ - ["@stackflow/plugin-history-sync", "virtual:0892efb9b4f386c24dba440a9d2a0ff9dd8aabcfd8ef5403c4c5f10c0d1c23fd4bad505b8db6b1207f29fb736fee165114aaa48c8dc6b509afc29633adbfcc46#workspace:extensions/plugin-history-sync"],\ - ["@graphql-tools/schema", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:10.0.5"],\ - ["@stackflow/config", "workspace:config"],\ - ["@stackflow/core", "workspace:core"],\ - ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ - ["@stackflow/plugin-renderer-basic", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:extensions/plugin-renderer-basic"],\ - ["@stackflow/react", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:integrations/react"],\ - ["@swc/core", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:1.6.6"],\ - ["@swc/jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:0.2.36"],\ - ["@testing-library/dom", "npm:10.4.1"],\ - ["@testing-library/react", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:16.3.2"],\ - ["@types/jest", "npm:29.5.12"],\ - ["@types/node", "npm:20.14.9"],\ - ["@types/react", "npm:18.3.3"],\ - ["@types/react-dom", "npm:18.3.0"],\ - ["@types/react-relay", "npm:16.0.6"],\ - ["@types/relay-runtime", "npm:17.0.4"],\ - ["@types/stackflow__config", null],\ - ["@types/stackflow__core", null],\ - ["@types/stackflow__react", null],\ - ["esbuild", "npm:0.23.0"],\ - ["flatted", "npm:3.3.1"],\ - ["graphql", "npm:16.9.0"],\ - ["history", "npm:5.3.0"],\ - ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:29.7.0"],\ - ["react", "npm:18.3.1"],\ - ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ - ["react-relay", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:17.0.0"],\ - ["react18-use", "virtual:0f555d13a58e9dd5c800ba6c444965d36246460fac7ea23a2ab4acc22d433bd134e1beca6f312395a6e7138233daf2b253710629ef0b429f21772e5419e43990#npm:0.4.1"],\ - ["relay-compiler", "npm:17.0.0"],\ - ["relay-runtime", "npm:17.0.0"],\ - ["rimraf", "npm:3.0.2"],\ - ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"],\ - ["url-pattern", "npm:1.0.3"]\ - ],\ - "packagePeers": [\ - "@stackflow/config",\ - "@stackflow/core",\ - "@stackflow/react",\ - "@types/react",\ - "@types/stackflow__config",\ - "@types/stackflow__core",\ - "@types/stackflow__react",\ - "react"\ - ],\ - "linkType": "SOFT"\ - }],\ ["virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:1.12.0::__archiveUrl=https%3A%2F%2Fregistry.npmjs.org%2F%40stackflow%2Fplugin-history-sync%2F-%2Fplugin-history-sync-1.12.0.tgz", {\ "packageLocation": "./.yarn/__virtual__/@stackflow-plugin-history-sync-virtual-0f555d13a5/0/cache/@stackflow-plugin-history-sync-npm-1.12.0-f5ab13d9ae-b1d1f5a380.zip/node_modules/@stackflow/plugin-history-sync/",\ "packageDependencies": [\ @@ -6991,7 +6949,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./extensions/plugin-history-sync/",\ "packageDependencies": [\ ["@stackflow/plugin-history-sync", "workspace:extensions/plugin-history-sync"],\ - ["@graphql-tools/schema", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:10.0.5"],\ + ["@graphql-tools/schema", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:10.0.5"],\ ["@stackflow/config", "workspace:config"],\ ["@stackflow/core", "workspace:core"],\ ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ @@ -7000,7 +6958,7 @@ const RAW_RUNTIME_STATE = ["@swc/core", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:1.6.6"],\ ["@swc/jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:0.2.36"],\ ["@testing-library/dom", "npm:10.4.1"],\ - ["@testing-library/react", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:16.3.2"],\ + ["@testing-library/react", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:16.3.2"],\ ["@types/jest", "npm:29.5.12"],\ ["@types/node", "npm:20.14.9"],\ ["@types/react", "npm:18.3.3"],\ @@ -7012,10 +6970,10 @@ const RAW_RUNTIME_STATE = ["graphql", "npm:16.9.0"],\ ["history", "npm:5.3.0"],\ ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ - ["react-relay", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:17.0.0"],\ + ["react-relay", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:17.0.0"],\ ["react18-use", "virtual:0f555d13a58e9dd5c800ba6c444965d36246460fac7ea23a2ab4acc22d433bd134e1beca6f312395a6e7138233daf2b253710629ef0b429f21772e5419e43990#npm:0.4.1"],\ ["relay-compiler", "npm:17.0.0"],\ ["relay-runtime", "npm:17.0.0"],\ @@ -7044,7 +7002,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:18.3.3"],\ ["esbuild", "npm:0.27.3"],\ ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:6.1.3"],\ @@ -7503,10 +7461,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:16.3.2", {\ - "packageLocation": "./.yarn/__virtual__/@testing-library-react-virtual-4882a36d99/0/cache/@testing-library-react-npm-16.3.2-67b0b894c8-0ca88c6f67.zip/node_modules/@testing-library/react/",\ + ["virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:16.3.2", {\ + "packageLocation": "./.yarn/__virtual__/@testing-library-react-virtual-1101e0d7a1/0/cache/@testing-library-react-npm-16.3.2-67b0b894c8-0ca88c6f67.zip/node_modules/@testing-library/react/",\ "packageDependencies": [\ - ["@testing-library/react", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:16.3.2"],\ + ["@testing-library/react", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:16.3.2"],\ ["@babel/runtime", "npm:7.25.0"],\ ["@testing-library/dom", "npm:10.4.1"],\ ["@types/react", "npm:18.3.3"],\ @@ -13045,10 +13003,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:29.7.0", {\ - "packageLocation": "./.yarn/__virtual__/jest-environment-jsdom-virtual-a089821e60/0/cache/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b-23bbfc9bca.zip/node_modules/jest-environment-jsdom/",\ + ["virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0", {\ + "packageLocation": "./.yarn/__virtual__/jest-environment-jsdom-virtual-3860b907a3/0/cache/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b-23bbfc9bca.zip/node_modules/jest-environment-jsdom/",\ "packageDependencies": [\ - ["jest-environment-jsdom", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["@jest/environment", "npm:29.7.0"],\ ["@jest/fake-timers", "npm:29.7.0"],\ ["@jest/types", "npm:29.6.3"],\ @@ -13058,7 +13016,7 @@ const RAW_RUNTIME_STATE = ["canvas", null],\ ["jest-mock", "npm:29.7.0"],\ ["jest-util", "npm:29.7.0"],\ - ["jsdom", "virtual:a089821e60430e98a5b3ef663a5eeb2414d40cb3eed0eb44d0cf9f0936e27fda9c9045d546e1a91634c47e938bf0f44cfcf4df7f4d7fae586f4dcdce3aab0194#npm:20.0.3"]\ + ["jsdom", "virtual:3860b907a31300af5097c87a5e1d3ae77507975becba94f9f62bcd6fb844cd3cb254d19fb73e2724318151b22430040481b6f441a90049b6b343a6dc3c32a958#npm:20.0.3"]\ ],\ "packagePeers": [\ "@types/canvas",\ @@ -13421,10 +13379,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:a089821e60430e98a5b3ef663a5eeb2414d40cb3eed0eb44d0cf9f0936e27fda9c9045d546e1a91634c47e938bf0f44cfcf4df7f4d7fae586f4dcdce3aab0194#npm:20.0.3", {\ - "packageLocation": "./.yarn/__virtual__/jsdom-virtual-0ddf92ef70/0/cache/jsdom-npm-20.0.3-906a2f7005-a4cdcff5b0.zip/node_modules/jsdom/",\ + ["virtual:3860b907a31300af5097c87a5e1d3ae77507975becba94f9f62bcd6fb844cd3cb254d19fb73e2724318151b22430040481b6f441a90049b6b343a6dc3c32a958#npm:20.0.3", {\ + "packageLocation": "./.yarn/__virtual__/jsdom-virtual-4200d7e3d6/0/cache/jsdom-npm-20.0.3-906a2f7005-a4cdcff5b0.zip/node_modules/jsdom/",\ "packageDependencies": [\ - ["jsdom", "virtual:a089821e60430e98a5b3ef663a5eeb2414d40cb3eed0eb44d0cf9f0936e27fda9c9045d546e1a91634c47e938bf0f44cfcf4df7f4d7fae586f4dcdce3aab0194#npm:20.0.3"],\ + ["jsdom", "virtual:3860b907a31300af5097c87a5e1d3ae77507975becba94f9f62bcd6fb844cd3cb254d19fb73e2724318151b22430040481b6f441a90049b6b343a6dc3c32a958#npm:20.0.3"],\ ["@types/canvas", null],\ ["abab", "npm:2.0.6"],\ ["acorn", "npm:8.16.0"],\ @@ -13451,7 +13409,7 @@ const RAW_RUNTIME_STATE = ["whatwg-encoding", "npm:2.0.0"],\ ["whatwg-mimetype", "npm:3.0.0"],\ ["whatwg-url", "npm:11.0.0"],\ - ["ws", "virtual:0ddf92ef7004d8a162ebc1a217af0f907c2ccde8ba64ef771dcb5bc0c694507e9eadb73ca24a6db674131ba0253634cb4670815bec7ff50c6ab6b975735689b4#npm:8.19.0"],\ + ["ws", "virtual:4200d7e3d6e47908b64a1319efd062b6c54854a7c3a03fee52630c2066bd5478e36e3d8d67e316b068ca4f93e9becf4be2b544db8e35178e926a0760a3392568#npm:8.19.0"],\ ["xml-name-validator", "npm:4.0.0"]\ ],\ "packagePeers": [\ @@ -16940,10 +16898,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:17.0.0", {\ - "packageLocation": "./.yarn/__virtual__/react-relay-virtual-32d5466f1a/0/cache/react-relay-npm-17.0.0-b76694d525-5f4c3f1e99.zip/node_modules/react-relay/",\ + ["virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:17.0.0", {\ + "packageLocation": "./.yarn/__virtual__/react-relay-virtual-e541417c2f/0/cache/react-relay-npm-17.0.0-b76694d525-5f4c3f1e99.zip/node_modules/react-relay/",\ "packageDependencies": [\ - ["react-relay", "virtual:0b16c4696c71f8b9ab6ff13965cb6891a42897b4bb0994338440e95ea94562b0b68d2e71acdf31f08648da7d5a35d8b342325f7cb96e5b84f9e77bc4d98f8564#npm:17.0.0"],\ + ["react-relay", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:17.0.0"],\ ["@babel/runtime", "npm:7.25.0"],\ ["@types/react", "npm:18.3.3"],\ ["fbjs", "npm:3.0.5"],\ @@ -19472,10 +19430,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0ddf92ef7004d8a162ebc1a217af0f907c2ccde8ba64ef771dcb5bc0c694507e9eadb73ca24a6db674131ba0253634cb4670815bec7ff50c6ab6b975735689b4#npm:8.19.0", {\ - "packageLocation": "./.yarn/__virtual__/ws-virtual-2228229947/0/cache/ws-npm-8.19.0-c967c046a5-26e4901e93.zip/node_modules/ws/",\ + ["virtual:4200d7e3d6e47908b64a1319efd062b6c54854a7c3a03fee52630c2066bd5478e36e3d8d67e316b068ca4f93e9becf4be2b544db8e35178e926a0760a3392568#npm:8.19.0", {\ + "packageLocation": "./.yarn/__virtual__/ws-virtual-0cfadb17bf/0/cache/ws-npm-8.19.0-c967c046a5-26e4901e93.zip/node_modules/ws/",\ "packageDependencies": [\ - ["ws", "virtual:0ddf92ef7004d8a162ebc1a217af0f907c2ccde8ba64ef771dcb5bc0c694507e9eadb73ca24a6db674131ba0253634cb4670815bec7ff50c6ab6b975735689b4#npm:8.19.0"],\ + ["ws", "virtual:4200d7e3d6e47908b64a1319efd062b6c54854a7c3a03fee52630c2066bd5478e36e3d8d67e316b068ca4f93e9becf4be2b544db8e35178e926a0760a3392568#npm:8.19.0"],\ ["@types/bufferutil", null],\ ["@types/utf-8-validate", null],\ ["bufferutil", null],\ diff --git a/extensions/link/README.md b/extensions/link/README.md index 4210f1dea..77c4a0e1d 100644 --- a/extensions/link/README.md +++ b/extensions/link/README.md @@ -4,9 +4,8 @@ It mimics the `` component behavior provided by Gatsby or Next.js. ## Dependencies -It can be used only when `@stackflow/plugin-history-sync` is set. - -- `@stackflow/plugin-history-sync` +Provide a URL resolver with `LinkUrlResolverProvider`. The resolver can come +from `@stackflow/plugin-history-sync` or another routing plugin. ## Usage @@ -38,21 +37,35 @@ import { historySyncPlugin } from "@stackflow/plugin-history-sync"; import { config } from "./stackflow.config"; import { MyActivity } from "./MyActivity"; +const historySync = historySyncPlugin({ + config, + fallbackActivity: () => "MyActivity", +}); + const { Stack } = stackflow({ config, components: { MyActivity, }, plugins: [ - historySyncPlugin({ - config, - fallbackActivity: () => "MyActivity", - }), + historySync, // ... ], }); ``` +Wrap `Stack` with the resolver from the routing plugin. + +```tsx +import { LinkUrlResolverProvider } from "@stackflow/link"; + +const App = () => ( + + + +); +``` + ```tsx /** * MyComponent.ts diff --git a/extensions/link/package.json b/extensions/link/package.json index 860c3b4bb..56fdfe2e1 100644 --- a/extensions/link/package.json +++ b/extensions/link/package.json @@ -28,24 +28,50 @@ "build:js": "node ./esbuild.config.js", "clean": "rimraf dist", "dev": "yarn build:js --watch && yarn build:dts --watch", + "test": "yarn jest", "typecheck": "tsc --noEmit" }, + "jest": { + "testEnvironment": "jsdom", + "testPathIgnorePatterns": [ + "/dist/" + ], + "transform": { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + "jsc": { + "transform": { + "react": { + "runtime": "automatic" + } + } + } + } + ] + } + }, "devDependencies": { "@stackflow/config": "^2.0.0", "@stackflow/core": "^3.0.0", "@stackflow/esbuild-config": "^1.0.3", - "@stackflow/plugin-history-sync": "^2.1.0", "@stackflow/react": "^2.1.2", + "@swc/core": "^1.6.6", + "@swc/jest": "^0.2.36", + "@types/jest": "^29.5.12", "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "esbuild": "^0.23.0", "esbuild-plugin-file-path-extensions": "^2.1.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "react": "^18.3.1", + "react-dom": "^18.3.1", "rimraf": "^3.0.2", "typescript": "^5.5.3" }, "peerDependencies": { "@stackflow/core": "^2.0.0 || ^3.0.0", - "@stackflow/plugin-history-sync": "^1.6.4-canary.0 || ^2.0.0", "@stackflow/react": "^2.0.0", "@types/react": ">=16.8.0", "react": ">=16.8.0" diff --git a/extensions/link/src/Link.spec.tsx b/extensions/link/src/Link.spec.tsx new file mode 100644 index 000000000..f58521d28 --- /dev/null +++ b/extensions/link/src/Link.spec.tsx @@ -0,0 +1,74 @@ +/** @jest-environment node */ + +import { defineConfig } from "@stackflow/config"; +import type { StackflowReactPlugin } from "@stackflow/react"; +import { stackflow } from "@stackflow/react"; +import { renderToString } from "react-dom/server"; +import { Link } from "./Link"; +import { LinkUrlResolverProvider } from "./LinkUrlResolverContext"; + +declare module "@stackflow/config" { + interface Register { + Home: {}; + Article: { + articleId: number; + source: string; + }; + } +} + +function Home() { + return ( + + Article + + ); +} + +function Article() { + return
Article
; +} + +const rendererPlugin: StackflowReactPlugin = () => ({ + key: "test-renderer", + render({ stack }) { + return ( + <> + {stack.render().activities.map((activity) => ( +
{activity.render()}
+ ))} + + ); + }, +}); + +test("Link uses an injected resolver for its href", () => { + const config = defineConfig({ + initialActivity: () => "Home", + transitionDuration: 0, + activities: [{ name: "Home" }, { name: "Article" }], + }); + + const { Stack } = stackflow({ + config, + components: { Home, Article }, + plugins: [rendererPlugin], + }); + + const html = renderToString( + + + , + ); + + expect(html).toContain('href="/articles/42/?source=feed"'); +}); diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index 9a0619ad2..9a5049a69 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -1,18 +1,12 @@ -/// - import type { InferActivityParams, RegisteredActivityName, } from "@stackflow/config"; -import type { Route } from "@stackflow/plugin-history-sync"; -import { useConfig, useFlow } from "@stackflow/react"; +import { useFlow } from "@stackflow/react"; import { useMemo } from "react"; +import { useLinkUrlResolver } from "./LinkUrlResolverContext"; import { omit } from "./omit"; -function toRoute(route: string | Route): Route { - return typeof route === "string" ? { path: route, decode: undefined } : route; -} - type AnchorProps = Omit< React.DetailedHTMLProps< React.AnchorHTMLAttributes, @@ -31,26 +25,14 @@ export interface LinkProps } export function Link(props: LinkProps) { - const config = useConfig(); + const urlResolver = useLinkUrlResolver(); const { push, replace } = useFlow(); - const href = useMemo(() => { - const match = config.activities.find((r) => r.name === props.activityName); - - if (!match || !match.route || !config.historySync) { - return undefined; - } - - const { path, decode } = Array.isArray(match.route) - ? toRoute(match.route[0]) - : toRoute(match.route); - - const { makeTemplate, urlPatternOptions } = config.historySync; - - const template = makeTemplate({ path, decode }, urlPatternOptions); - - return template.fill(props.activityParams); - }, [config, props.activityName, props.activityParams]); + const href = useMemo( + () => + urlResolver?.makeActivityUrl(props.activityName, props.activityParams), + [urlResolver, props.activityName, props.activityParams], + ); const anchorProps = omit(props, [ // Custom Props diff --git a/extensions/link/src/LinkUrlResolverContext.tsx b/extensions/link/src/LinkUrlResolverContext.tsx new file mode 100644 index 000000000..12baaf78e --- /dev/null +++ b/extensions/link/src/LinkUrlResolverContext.tsx @@ -0,0 +1,30 @@ +import { createContext, type ReactNode, useContext } from "react"; + +export interface LinkUrlResolver { + readonly makeActivityUrl: ( + activityName: string, + activityParams: Record, + ) => string; +} + +export const LinkUrlResolverContext = createContext( + null, +); + +export function LinkUrlResolverProvider({ + resolver, + children, +}: { + resolver: LinkUrlResolver; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function useLinkUrlResolver() { + return useContext(LinkUrlResolverContext); +} diff --git a/extensions/link/src/index.ts b/extensions/link/src/index.ts index 3b40a46d8..899f349df 100644 --- a/extensions/link/src/index.ts +++ b/extensions/link/src/index.ts @@ -1 +1,2 @@ export * from "./Link"; +export * from "./LinkUrlResolverContext"; diff --git a/yarn.lock b/yarn.lock index eda3670a3..eb126d563 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5728,17 +5728,22 @@ __metadata: "@stackflow/config": "npm:^2.0.0" "@stackflow/core": "npm:^3.0.0" "@stackflow/esbuild-config": "npm:^1.0.3" - "@stackflow/plugin-history-sync": "npm:^2.1.0" "@stackflow/react": "npm:^2.1.2" + "@swc/core": "npm:^1.6.6" + "@swc/jest": "npm:^0.2.36" + "@types/jest": "npm:^29.5.12" "@types/react": "npm:^18.3.3" + "@types/react-dom": "npm:^18.3.0" esbuild: "npm:^0.23.0" esbuild-plugin-file-path-extensions: "npm:^2.1.3" + jest: "npm:^29.7.0" + jest-environment-jsdom: "npm:^29.7.0" react: "npm:^18.3.1" + react-dom: "npm:^18.3.1" rimraf: "npm:^3.0.2" typescript: "npm:^5.5.3" peerDependencies: "@stackflow/core": ^2.0.0 || ^3.0.0 - "@stackflow/plugin-history-sync": ^1.6.4-canary.0 || ^2.0.0 "@stackflow/react": ^2.0.0 "@types/react": ">=16.8.0" react: ">=16.8.0" @@ -5890,7 +5895,7 @@ __metadata: languageName: node linkType: hard -"@stackflow/plugin-history-sync@npm:^2.1.0, @stackflow/plugin-history-sync@workspace:extensions/plugin-history-sync": +"@stackflow/plugin-history-sync@workspace:extensions/plugin-history-sync": version: 0.0.0-use.local resolution: "@stackflow/plugin-history-sync@workspace:extensions/plugin-history-sync" dependencies: From 758790e0d4b54c2c3ed368807289901441b93e92 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:35:53 +0900 Subject: [PATCH 02/12] chore(link): remove package-local Jest setup --- .pnp.cjs | 42 ++++++------------ extensions/link/package.json | 28 ------------ extensions/link/src/Link.spec.tsx | 74 ------------------------------- yarn.lock | 7 --- 4 files changed, 14 insertions(+), 137 deletions(-) delete mode 100644 extensions/link/src/Link.spec.tsx diff --git a/.pnp.cjs b/.pnp.cjs index 719f1ce47..e5db87c0d 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -6664,19 +6664,12 @@ const RAW_RUNTIME_STATE = ["@stackflow/core", "workspace:core"],\ ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ ["@stackflow/react", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:integrations/react"],\ - ["@swc/core", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:1.6.6"],\ - ["@swc/jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:0.2.36"],\ - ["@types/jest", "npm:29.5.12"],\ ["@types/react", "npm:18.3.3"],\ - ["@types/react-dom", "npm:18.3.0"],\ ["@types/stackflow__core", null],\ ["@types/stackflow__react", null],\ ["esbuild", "npm:0.23.0"],\ ["esbuild-plugin-file-path-extensions", "npm:2.1.3"],\ - ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ - ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:3.0.2"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"]\ ],\ @@ -6698,17 +6691,10 @@ const RAW_RUNTIME_STATE = ["@stackflow/core", "workspace:core"],\ ["@stackflow/esbuild-config", "workspace:packages/esbuild-config"],\ ["@stackflow/react", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#workspace:integrations/react"],\ - ["@swc/core", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:1.6.6"],\ - ["@swc/jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:0.2.36"],\ - ["@types/jest", "npm:29.5.12"],\ ["@types/react", "npm:18.3.3"],\ - ["@types/react-dom", "npm:18.3.0"],\ ["esbuild", "npm:0.23.0"],\ ["esbuild-plugin-file-path-extensions", "npm:2.1.3"],\ - ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ - ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:3.0.2"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"]\ ],\ @@ -6819,7 +6805,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:18.3.3"],\ ["esbuild", "npm:0.27.3"],\ ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:983596cc6314880cdf5646ccae28a297f9a9d9cc50891bcdd6486e5d19a65321933850dc7adb791ac89d6716f7185d6397520da0ee1852df1d3f86cb026a38fc#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:6.1.3"],\ @@ -6970,7 +6956,7 @@ const RAW_RUNTIME_STATE = ["graphql", "npm:16.9.0"],\ ["history", "npm:5.3.0"],\ ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:983596cc6314880cdf5646ccae28a297f9a9d9cc50891bcdd6486e5d19a65321933850dc7adb791ac89d6716f7185d6397520da0ee1852df1d3f86cb026a38fc#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["react-relay", "virtual:038d976d2b7f3e8fd148f18e9a5ef6bd00ac410ac8a72b75f7f5638a0435d7823d8b7e12953476b405049282091613331adeb4b267577f96604772f6b6322006#npm:17.0.0"],\ @@ -7002,7 +6988,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:18.3.3"],\ ["esbuild", "npm:0.27.3"],\ ["jest", "virtual:b327d7e228fba669b88a8bb23bcf526374e46fa67e617b1e6848e8a205357fee5ce94b47c49b5a570fd9e8a44fa218a13cd00e2eca327c99114cbd21d72ecf9c#npm:29.7.0"],\ - ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:983596cc6314880cdf5646ccae28a297f9a9d9cc50891bcdd6486e5d19a65321933850dc7adb791ac89d6716f7185d6397520da0ee1852df1d3f86cb026a38fc#npm:29.7.0"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:413bca98ff76262f6f1f73762ccc4b7edee04a5da42f3d6b9ed2cb2d6dbc397b2094da59b50f6e828091c88e7b5f86990feff596c43f0eb50a58fc42aae64a20#npm:18.3.1"],\ ["rimraf", "npm:6.1.3"],\ @@ -13003,10 +12989,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0", {\ - "packageLocation": "./.yarn/__virtual__/jest-environment-jsdom-virtual-3860b907a3/0/cache/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b-23bbfc9bca.zip/node_modules/jest-environment-jsdom/",\ + ["virtual:983596cc6314880cdf5646ccae28a297f9a9d9cc50891bcdd6486e5d19a65321933850dc7adb791ac89d6716f7185d6397520da0ee1852df1d3f86cb026a38fc#npm:29.7.0", {\ + "packageLocation": "./.yarn/__virtual__/jest-environment-jsdom-virtual-03ba513b4a/0/cache/jest-environment-jsdom-npm-29.7.0-0b72dd0e0b-23bbfc9bca.zip/node_modules/jest-environment-jsdom/",\ "packageDependencies": [\ - ["jest-environment-jsdom", "virtual:9ebc34fcb6f35d6a144c2c0e54526b8c8bc3fb282b451cd82514ec7c8eaa6e61fde3c84d5c25e8add468cb1f45a42f8fe2d8b659771afa10fe0f729c49f5cb3f#npm:29.7.0"],\ + ["jest-environment-jsdom", "virtual:983596cc6314880cdf5646ccae28a297f9a9d9cc50891bcdd6486e5d19a65321933850dc7adb791ac89d6716f7185d6397520da0ee1852df1d3f86cb026a38fc#npm:29.7.0"],\ ["@jest/environment", "npm:29.7.0"],\ ["@jest/fake-timers", "npm:29.7.0"],\ ["@jest/types", "npm:29.6.3"],\ @@ -13016,7 +13002,7 @@ const RAW_RUNTIME_STATE = ["canvas", null],\ ["jest-mock", "npm:29.7.0"],\ ["jest-util", "npm:29.7.0"],\ - ["jsdom", "virtual:3860b907a31300af5097c87a5e1d3ae77507975becba94f9f62bcd6fb844cd3cb254d19fb73e2724318151b22430040481b6f441a90049b6b343a6dc3c32a958#npm:20.0.3"]\ + ["jsdom", "virtual:03ba513b4a4f2f49a0ee779e0b1da3ef4f41cbf0cff4a27f151a6c11d5162aae67852dc5c3f387d71c020640c3547cdf783b461f72a6ebbd7907fd3300ce6913#npm:20.0.3"]\ ],\ "packagePeers": [\ "@types/canvas",\ @@ -13379,10 +13365,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:3860b907a31300af5097c87a5e1d3ae77507975becba94f9f62bcd6fb844cd3cb254d19fb73e2724318151b22430040481b6f441a90049b6b343a6dc3c32a958#npm:20.0.3", {\ - "packageLocation": "./.yarn/__virtual__/jsdom-virtual-4200d7e3d6/0/cache/jsdom-npm-20.0.3-906a2f7005-a4cdcff5b0.zip/node_modules/jsdom/",\ + ["virtual:03ba513b4a4f2f49a0ee779e0b1da3ef4f41cbf0cff4a27f151a6c11d5162aae67852dc5c3f387d71c020640c3547cdf783b461f72a6ebbd7907fd3300ce6913#npm:20.0.3", {\ + "packageLocation": "./.yarn/__virtual__/jsdom-virtual-09fbede01d/0/cache/jsdom-npm-20.0.3-906a2f7005-a4cdcff5b0.zip/node_modules/jsdom/",\ "packageDependencies": [\ - ["jsdom", "virtual:3860b907a31300af5097c87a5e1d3ae77507975becba94f9f62bcd6fb844cd3cb254d19fb73e2724318151b22430040481b6f441a90049b6b343a6dc3c32a958#npm:20.0.3"],\ + ["jsdom", "virtual:03ba513b4a4f2f49a0ee779e0b1da3ef4f41cbf0cff4a27f151a6c11d5162aae67852dc5c3f387d71c020640c3547cdf783b461f72a6ebbd7907fd3300ce6913#npm:20.0.3"],\ ["@types/canvas", null],\ ["abab", "npm:2.0.6"],\ ["acorn", "npm:8.16.0"],\ @@ -13409,7 +13395,7 @@ const RAW_RUNTIME_STATE = ["whatwg-encoding", "npm:2.0.0"],\ ["whatwg-mimetype", "npm:3.0.0"],\ ["whatwg-url", "npm:11.0.0"],\ - ["ws", "virtual:4200d7e3d6e47908b64a1319efd062b6c54854a7c3a03fee52630c2066bd5478e36e3d8d67e316b068ca4f93e9becf4be2b544db8e35178e926a0760a3392568#npm:8.19.0"],\ + ["ws", "virtual:09fbede01d752e610be1714c18909368fe4fa709b16e76a5ed8cde05b6dbb3342f037902ae401113b5bbbb44b9753fbd2ba83c3277f1f798491ade558971e25f#npm:8.19.0"],\ ["xml-name-validator", "npm:4.0.0"]\ ],\ "packagePeers": [\ @@ -19430,10 +19416,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:4200d7e3d6e47908b64a1319efd062b6c54854a7c3a03fee52630c2066bd5478e36e3d8d67e316b068ca4f93e9becf4be2b544db8e35178e926a0760a3392568#npm:8.19.0", {\ - "packageLocation": "./.yarn/__virtual__/ws-virtual-0cfadb17bf/0/cache/ws-npm-8.19.0-c967c046a5-26e4901e93.zip/node_modules/ws/",\ + ["virtual:09fbede01d752e610be1714c18909368fe4fa709b16e76a5ed8cde05b6dbb3342f037902ae401113b5bbbb44b9753fbd2ba83c3277f1f798491ade558971e25f#npm:8.19.0", {\ + "packageLocation": "./.yarn/__virtual__/ws-virtual-99b0ff26e3/0/cache/ws-npm-8.19.0-c967c046a5-26e4901e93.zip/node_modules/ws/",\ "packageDependencies": [\ - ["ws", "virtual:4200d7e3d6e47908b64a1319efd062b6c54854a7c3a03fee52630c2066bd5478e36e3d8d67e316b068ca4f93e9becf4be2b544db8e35178e926a0760a3392568#npm:8.19.0"],\ + ["ws", "virtual:09fbede01d752e610be1714c18909368fe4fa709b16e76a5ed8cde05b6dbb3342f037902ae401113b5bbbb44b9753fbd2ba83c3277f1f798491ade558971e25f#npm:8.19.0"],\ ["@types/bufferutil", null],\ ["@types/utf-8-validate", null],\ ["bufferutil", null],\ diff --git a/extensions/link/package.json b/extensions/link/package.json index 56fdfe2e1..c05525596 100644 --- a/extensions/link/package.json +++ b/extensions/link/package.json @@ -28,45 +28,17 @@ "build:js": "node ./esbuild.config.js", "clean": "rimraf dist", "dev": "yarn build:js --watch && yarn build:dts --watch", - "test": "yarn jest", "typecheck": "tsc --noEmit" }, - "jest": { - "testEnvironment": "jsdom", - "testPathIgnorePatterns": [ - "/dist/" - ], - "transform": { - "^.+\\.(t|j)sx?$": [ - "@swc/jest", - { - "jsc": { - "transform": { - "react": { - "runtime": "automatic" - } - } - } - } - ] - } - }, "devDependencies": { "@stackflow/config": "^2.0.0", "@stackflow/core": "^3.0.0", "@stackflow/esbuild-config": "^1.0.3", "@stackflow/react": "^2.1.2", - "@swc/core": "^1.6.6", - "@swc/jest": "^0.2.36", - "@types/jest": "^29.5.12", "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", "esbuild": "^0.23.0", "esbuild-plugin-file-path-extensions": "^2.1.3", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", "react": "^18.3.1", - "react-dom": "^18.3.1", "rimraf": "^3.0.2", "typescript": "^5.5.3" }, diff --git a/extensions/link/src/Link.spec.tsx b/extensions/link/src/Link.spec.tsx deleted file mode 100644 index f58521d28..000000000 --- a/extensions/link/src/Link.spec.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** @jest-environment node */ - -import { defineConfig } from "@stackflow/config"; -import type { StackflowReactPlugin } from "@stackflow/react"; -import { stackflow } from "@stackflow/react"; -import { renderToString } from "react-dom/server"; -import { Link } from "./Link"; -import { LinkUrlResolverProvider } from "./LinkUrlResolverContext"; - -declare module "@stackflow/config" { - interface Register { - Home: {}; - Article: { - articleId: number; - source: string; - }; - } -} - -function Home() { - return ( - - Article - - ); -} - -function Article() { - return
Article
; -} - -const rendererPlugin: StackflowReactPlugin = () => ({ - key: "test-renderer", - render({ stack }) { - return ( - <> - {stack.render().activities.map((activity) => ( -
{activity.render()}
- ))} - - ); - }, -}); - -test("Link uses an injected resolver for its href", () => { - const config = defineConfig({ - initialActivity: () => "Home", - transitionDuration: 0, - activities: [{ name: "Home" }, { name: "Article" }], - }); - - const { Stack } = stackflow({ - config, - components: { Home, Article }, - plugins: [rendererPlugin], - }); - - const html = renderToString( - - - , - ); - - expect(html).toContain('href="/articles/42/?source=feed"'); -}); diff --git a/yarn.lock b/yarn.lock index eb126d563..7d743fa87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5729,17 +5729,10 @@ __metadata: "@stackflow/core": "npm:^3.0.0" "@stackflow/esbuild-config": "npm:^1.0.3" "@stackflow/react": "npm:^2.1.2" - "@swc/core": "npm:^1.6.6" - "@swc/jest": "npm:^0.2.36" - "@types/jest": "npm:^29.5.12" "@types/react": "npm:^18.3.3" - "@types/react-dom": "npm:^18.3.0" esbuild: "npm:^0.23.0" esbuild-plugin-file-path-extensions: "npm:^2.1.3" - jest: "npm:^29.7.0" - jest-environment-jsdom: "npm:^29.7.0" react: "npm:^18.3.1" - react-dom: "npm:^18.3.1" rimraf: "npm:^3.0.2" typescript: "npm:^5.5.3" peerDependencies: From 59456705cbb142252cc870e3eecd7292851eb530 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:38:55 +0900 Subject: [PATCH 03/12] chore(link): classify resolver injection as breaking --- .changeset/fep-2677-link-url-resolver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fep-2677-link-url-resolver.md b/.changeset/fep-2677-link-url-resolver.md index b5fe9bf7d..9e9c4230b 100644 --- a/.changeset/fep-2677-link-url-resolver.md +++ b/.changeset/fep-2677-link-url-resolver.md @@ -1,5 +1,5 @@ --- -"@stackflow/link": patch +"@stackflow/link": major --- Inject a URL resolver into Link so route selection, encoding, and URL pattern options stay consistent with the routing plugin. From c455811e31455e7644a42836f766623a4f292b39 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:42:22 +0900 Subject: [PATCH 04/12] refactor(link): expose resolver context only --- extensions/link/README.md | 8 ++--- extensions/link/src/Link.tsx | 6 ++-- extensions/link/src/LinkUrlResolverContext.ts | 12 ++++++++ .../link/src/LinkUrlResolverContext.tsx | 30 ------------------- extensions/link/src/index.ts | 3 +- 5 files changed, 21 insertions(+), 38 deletions(-) create mode 100644 extensions/link/src/LinkUrlResolverContext.ts delete mode 100644 extensions/link/src/LinkUrlResolverContext.tsx diff --git a/extensions/link/README.md b/extensions/link/README.md index 77c4a0e1d..d7ab6263e 100644 --- a/extensions/link/README.md +++ b/extensions/link/README.md @@ -4,7 +4,7 @@ It mimics the `` component behavior provided by Gatsby or Next.js. ## Dependencies -Provide a URL resolver with `LinkUrlResolverProvider`. The resolver can come +Provide a URL resolver with `LinkUrlResolverContext`. The resolver can come from `@stackflow/plugin-history-sync` or another routing plugin. ## Usage @@ -57,12 +57,12 @@ const { Stack } = stackflow({ Wrap `Stack` with the resolver from the routing plugin. ```tsx -import { LinkUrlResolverProvider } from "@stackflow/link"; +import { LinkUrlResolverContext } from "@stackflow/link"; const App = () => ( - + - + ); ``` diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index 9a5049a69..77adba410 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -3,8 +3,8 @@ import type { RegisteredActivityName, } from "@stackflow/config"; import { useFlow } from "@stackflow/react"; -import { useMemo } from "react"; -import { useLinkUrlResolver } from "./LinkUrlResolverContext"; +import { useContext, useMemo } from "react"; +import { LinkUrlResolverContext } from "./LinkUrlResolverContext"; import { omit } from "./omit"; type AnchorProps = Omit< @@ -25,7 +25,7 @@ export interface LinkProps } export function Link(props: LinkProps) { - const urlResolver = useLinkUrlResolver(); + const urlResolver = useContext(LinkUrlResolverContext); const { push, replace } = useFlow(); const href = useMemo( diff --git a/extensions/link/src/LinkUrlResolverContext.ts b/extensions/link/src/LinkUrlResolverContext.ts new file mode 100644 index 000000000..d64a1235a --- /dev/null +++ b/extensions/link/src/LinkUrlResolverContext.ts @@ -0,0 +1,12 @@ +import { createContext } from "react"; + +export interface LinkUrlResolver { + readonly makeActivityUrl: ( + activityName: string, + activityParams: Record, + ) => string; +} + +export const LinkUrlResolverContext = createContext( + null, +); diff --git a/extensions/link/src/LinkUrlResolverContext.tsx b/extensions/link/src/LinkUrlResolverContext.tsx deleted file mode 100644 index 12baaf78e..000000000 --- a/extensions/link/src/LinkUrlResolverContext.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createContext, type ReactNode, useContext } from "react"; - -export interface LinkUrlResolver { - readonly makeActivityUrl: ( - activityName: string, - activityParams: Record, - ) => string; -} - -export const LinkUrlResolverContext = createContext( - null, -); - -export function LinkUrlResolverProvider({ - resolver, - children, -}: { - resolver: LinkUrlResolver; - children: ReactNode; -}) { - return ( - - {children} - - ); -} - -export function useLinkUrlResolver() { - return useContext(LinkUrlResolverContext); -} diff --git a/extensions/link/src/index.ts b/extensions/link/src/index.ts index 899f349df..3ba5257e6 100644 --- a/extensions/link/src/index.ts +++ b/extensions/link/src/index.ts @@ -1,2 +1,3 @@ export * from "./Link"; -export * from "./LinkUrlResolverContext"; +export type { LinkUrlResolver } from "./LinkUrlResolverContext"; +export { LinkUrlResolverContext } from "./LinkUrlResolverContext"; From 3d568891faaf56ff0b4ca533c441c1cd411658ca Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:48:09 +0900 Subject: [PATCH 05/12] fix(link): fail when resolver context is missing --- extensions/link/src/Link.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index 77adba410..036f5b76e 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -28,11 +28,22 @@ export function Link(props: LinkProps) { const urlResolver = useContext(LinkUrlResolverContext); const { push, replace } = useFlow(); - const href = useMemo( - () => - urlResolver?.makeActivityUrl(props.activityName, props.activityParams), - [urlResolver, props.activityName, props.activityParams], - ); + const href = useMemo(() => { + if (urlResolver === null) { + return undefined; + } + + return urlResolver.makeActivityUrl( + props.activityName, + props.activityParams, + ); + }, [urlResolver, props.activityName, props.activityParams]); + + if (urlResolver === null) { + throw new Error( + "Link must be rendered inside a LinkUrlResolverContext.Provider.", + ); + } const anchorProps = omit(props, [ // Custom Props From cd3cea7c5d1e7cde5ff65f1f377e55ab94d6352b Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:49:18 +0900 Subject: [PATCH 06/12] refactor(link): guard resolver before URL memoization --- extensions/link/src/Link.tsx | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index 036f5b76e..95012c99c 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -7,6 +7,18 @@ import { useContext, useMemo } from "react"; import { LinkUrlResolverContext } from "./LinkUrlResolverContext"; import { omit } from "./omit"; +function useRequiredLinkUrlResolver() { + const urlResolver = useContext(LinkUrlResolverContext); + + if (urlResolver === null) { + throw new Error( + "Link must be rendered inside a LinkUrlResolverContext.Provider.", + ); + } + + return urlResolver; +} + type AnchorProps = Omit< React.DetailedHTMLProps< React.AnchorHTMLAttributes, @@ -25,25 +37,13 @@ export interface LinkProps } export function Link(props: LinkProps) { - const urlResolver = useContext(LinkUrlResolverContext); + const urlResolver = useRequiredLinkUrlResolver(); const { push, replace } = useFlow(); - const href = useMemo(() => { - if (urlResolver === null) { - return undefined; - } - - return urlResolver.makeActivityUrl( - props.activityName, - props.activityParams, - ); - }, [urlResolver, props.activityName, props.activityParams]); - - if (urlResolver === null) { - throw new Error( - "Link must be rendered inside a LinkUrlResolverContext.Provider.", - ); - } + const href = useMemo( + () => urlResolver.makeActivityUrl(props.activityName, props.activityParams), + [urlResolver, props.activityName, props.activityParams], + ); const anchorProps = omit(props, [ // Custom Props From cc380bc82be8b96048f47653c3da394f8dbd3d54 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:53:03 +0900 Subject: [PATCH 07/12] refactor(link): colocate resolver context APIs --- extensions/link/README.md | 8 ++-- extensions/link/src/Link.tsx | 18 ++------- extensions/link/src/LinkUrlResolverContext.ts | 12 ------ .../link/src/LinkUrlResolverContext.tsx | 38 +++++++++++++++++++ extensions/link/src/index.ts | 3 +- 5 files changed, 46 insertions(+), 33 deletions(-) delete mode 100644 extensions/link/src/LinkUrlResolverContext.ts create mode 100644 extensions/link/src/LinkUrlResolverContext.tsx diff --git a/extensions/link/README.md b/extensions/link/README.md index d7ab6263e..77c4a0e1d 100644 --- a/extensions/link/README.md +++ b/extensions/link/README.md @@ -4,7 +4,7 @@ It mimics the `` component behavior provided by Gatsby or Next.js. ## Dependencies -Provide a URL resolver with `LinkUrlResolverContext`. The resolver can come +Provide a URL resolver with `LinkUrlResolverProvider`. The resolver can come from `@stackflow/plugin-history-sync` or another routing plugin. ## Usage @@ -57,12 +57,12 @@ const { Stack } = stackflow({ Wrap `Stack` with the resolver from the routing plugin. ```tsx -import { LinkUrlResolverContext } from "@stackflow/link"; +import { LinkUrlResolverProvider } from "@stackflow/link"; const App = () => ( - + - + ); ``` diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index 95012c99c..e9a297269 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -3,22 +3,10 @@ import type { RegisteredActivityName, } from "@stackflow/config"; import { useFlow } from "@stackflow/react"; -import { useContext, useMemo } from "react"; -import { LinkUrlResolverContext } from "./LinkUrlResolverContext"; +import { useMemo } from "react"; +import { useLinkUrlResolver } from "./LinkUrlResolverContext"; import { omit } from "./omit"; -function useRequiredLinkUrlResolver() { - const urlResolver = useContext(LinkUrlResolverContext); - - if (urlResolver === null) { - throw new Error( - "Link must be rendered inside a LinkUrlResolverContext.Provider.", - ); - } - - return urlResolver; -} - type AnchorProps = Omit< React.DetailedHTMLProps< React.AnchorHTMLAttributes, @@ -37,7 +25,7 @@ export interface LinkProps } export function Link(props: LinkProps) { - const urlResolver = useRequiredLinkUrlResolver(); + const urlResolver = useLinkUrlResolver(); const { push, replace } = useFlow(); const href = useMemo( diff --git a/extensions/link/src/LinkUrlResolverContext.ts b/extensions/link/src/LinkUrlResolverContext.ts deleted file mode 100644 index d64a1235a..000000000 --- a/extensions/link/src/LinkUrlResolverContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createContext } from "react"; - -export interface LinkUrlResolver { - readonly makeActivityUrl: ( - activityName: string, - activityParams: Record, - ) => string; -} - -export const LinkUrlResolverContext = createContext( - null, -); diff --git a/extensions/link/src/LinkUrlResolverContext.tsx b/extensions/link/src/LinkUrlResolverContext.tsx new file mode 100644 index 000000000..8a3048507 --- /dev/null +++ b/extensions/link/src/LinkUrlResolverContext.tsx @@ -0,0 +1,38 @@ +import { createContext, type ReactNode, useContext } from "react"; + +export interface LinkUrlResolver { + readonly makeActivityUrl: ( + activityName: string, + activityParams: Record, + ) => string; +} + +export const LinkUrlResolverContext = createContext( + null, +); + +export function LinkUrlResolverProvider({ + resolver, + children, +}: { + resolver: LinkUrlResolver; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function useLinkUrlResolver() { + const urlResolver = useContext(LinkUrlResolverContext); + + if (urlResolver === null) { + throw new Error( + "Link must be rendered inside a LinkUrlResolverContext.Provider.", + ); + } + + return urlResolver; +} diff --git a/extensions/link/src/index.ts b/extensions/link/src/index.ts index 3ba5257e6..899f349df 100644 --- a/extensions/link/src/index.ts +++ b/extensions/link/src/index.ts @@ -1,3 +1,2 @@ export * from "./Link"; -export type { LinkUrlResolver } from "./LinkUrlResolverContext"; -export { LinkUrlResolverContext } from "./LinkUrlResolverContext"; +export * from "./LinkUrlResolverContext"; From b51dfd6b2dff91656ee5f427a73a482e904f063d Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 12:58:26 +0900 Subject: [PATCH 08/12] fix(link): generalize resolver context error --- extensions/link/src/LinkUrlResolverContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/link/src/LinkUrlResolverContext.tsx b/extensions/link/src/LinkUrlResolverContext.tsx index 8a3048507..264e8d82f 100644 --- a/extensions/link/src/LinkUrlResolverContext.tsx +++ b/extensions/link/src/LinkUrlResolverContext.tsx @@ -30,7 +30,7 @@ export function useLinkUrlResolver() { if (urlResolver === null) { throw new Error( - "Link must be rendered inside a LinkUrlResolverContext.Provider.", + "No LinkUrlResolver was found in context. Wrap the component tree with LinkUrlResolverProvider.", ); } From 18d72b39008f58d19d97ef8ab25f06067a95454a Mon Sep 17 00:00:00 2001 From: Jaewon Seo Date: Mon, 10 Aug 2026 13:02:34 +0900 Subject: [PATCH 09/12] Update Link.tsx --- extensions/link/src/Link.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/link/src/Link.tsx b/extensions/link/src/Link.tsx index e9a297269..db8fea073 100644 --- a/extensions/link/src/Link.tsx +++ b/extensions/link/src/Link.tsx @@ -30,7 +30,7 @@ export function Link(props: LinkProps) { const href = useMemo( () => urlResolver.makeActivityUrl(props.activityName, props.activityParams), - [urlResolver, props.activityName, props.activityParams], + [urlResolver.makeActivityUrl, props.activityName, props.activityParams], ); const anchorProps = omit(props, [ From c44c6884e39b38929df8e5b0df144f19fa291c38 Mon Sep 17 00:00:00 2001 From: Jaewon Seo Date: Mon, 10 Aug 2026 13:03:45 +0900 Subject: [PATCH 10/12] Refactor exports in index.ts for LinkUrlResolver --- extensions/link/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/link/src/index.ts b/extensions/link/src/index.ts index 899f349df..d356ca166 100644 --- a/extensions/link/src/index.ts +++ b/extensions/link/src/index.ts @@ -1,2 +1,2 @@ export * from "./Link"; -export * from "./LinkUrlResolverContext"; +export { type LinkUrlResolver, LinkUrlResolverProvider } from "./LinkUrlResolverContext"; From d97ec962ece6a3eb6cb678a96218ff64f74547fa Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 14:34:44 +0900 Subject: [PATCH 11/12] docs(link): refine resolver changeset --- .changeset/fep-2677-link-url-resolver.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.changeset/fep-2677-link-url-resolver.md b/.changeset/fep-2677-link-url-resolver.md index 9e9c4230b..ddaa62a0c 100644 --- a/.changeset/fep-2677-link-url-resolver.md +++ b/.changeset/fep-2677-link-url-resolver.md @@ -2,4 +2,7 @@ "@stackflow/link": major --- -Inject a URL resolver into Link so route selection, encoding, and URL pattern options stay consistent with the routing plugin. +Require Link consumers to provide a URL resolver through +`LinkUrlResolverProvider`. This removes Link's direct dependency on +`@stackflow/plugin-history-sync` and keeps generated URLs consistent with the +configured routing plugin. From 5b41f8bfc3a53766542f7c023b4dceb206adf1d8 Mon Sep 17 00:00:00 2001 From: ENvironmentSet Date: Mon, 10 Aug 2026 14:37:04 +0900 Subject: [PATCH 12/12] style(link): format resolver exports --- extensions/link/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/link/src/index.ts b/extensions/link/src/index.ts index d356ca166..4f1b4792f 100644 --- a/extensions/link/src/index.ts +++ b/extensions/link/src/index.ts @@ -1,2 +1,5 @@ export * from "./Link"; -export { type LinkUrlResolver, LinkUrlResolverProvider } from "./LinkUrlResolverContext"; +export { + type LinkUrlResolver, + LinkUrlResolverProvider, +} from "./LinkUrlResolverContext";