fix: modernize react router interface

This commit is contained in:
cha0s 2024-02-17 07:12:18 -06:00
parent d2fabd902d
commit 2f522e5152
44 changed files with 761 additions and 242 deletions

311
package-lock.json generated
View File

@ -3088,6 +3088,14 @@
}
}
},
"node_modules/@remix-run/router": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz",
"integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@sigstore/bundle": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz",
@ -4672,53 +4680,6 @@
"ieee754": "^1.1.13"
}
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/body-parser/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/bonjour-service": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz",
@ -12714,31 +12675,6 @@
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@ -12901,23 +12837,29 @@
}
},
"node_modules/react-router": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"version": "6.22.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz",
"integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==",
"dependencies": {
"history": "^5.2.0"
"@remix-run/router": "1.15.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"version": "6.22.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.1.tgz",
"integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==",
"dependencies": {
"history": "^5.2.0",
"react-router": "6.2.1"
"@remix-run/router": "1.15.1",
"react-router": "6.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
@ -16669,7 +16611,7 @@
},
"packages/build": {
"name": "@flecks/build",
"version": "4.1.0",
"version": "4.1.1",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.12.10",
@ -16680,7 +16622,7 @@
"@babel/preset-env": "^7.12.11",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"babel-loader": "^9.1.3",
"babel-merge": "^3.0.0",
"chai": "4.2.0",
@ -16738,7 +16680,7 @@
},
"packages/core": {
"name": "@flecks/core",
"version": "4.2.0",
"version": "4.2.1",
"license": "MIT",
"dependencies": {
"callsites": "^3.1.0",
@ -16794,10 +16736,10 @@
},
"packages/create-app": {
"name": "@flecks/create-app",
"version": "4.0.9",
"version": "4.0.10",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"commander": "11.1.0",
"validate-npm-package-name": "^3.0.0"
},
@ -16805,8 +16747,8 @@
"create-app": "build/cli.js"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/create-app/node_modules/builtins": {
@ -16824,45 +16766,45 @@
},
"packages/create-fleck": {
"name": "@flecks/create-fleck",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"commander": "11.1.0"
},
"bin": {
"create-fleck": "build/cli.js"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/db": {
"name": "@flecks/db",
"version": "4.1.0",
"version": "4.1.1",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"sequelize": "^6.3.5",
"sqlite3": "^5.0.2"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/docker": {
"name": "@flecks/docker",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"debug": "^4.3.3"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/docker/node_modules/debug": {
@ -16883,47 +16825,47 @@
},
"packages/dox": {
"name": "@flecks/dox",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.17.2",
"@babel/traverse": "^7.17.0",
"@babel/types": "^7.17.0",
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"comment-parser": "^1.3.0",
"rimraf": "^5.0.5"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/electron": {
"name": "@flecks/electron",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"electron": "^28.1.4",
"electron-devtools-installer": "^3.2.0"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/fleck": {
"name": "@flecks/fleck",
"version": "4.1.0",
"version": "4.1.1",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"babel-merge": "^3.0.0",
"debug": "^4.3.3",
"mocha": "^10.2.0"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/build": "^4.1.1",
"chai": "4.2.0"
}
},
@ -16944,86 +16886,88 @@
}
},
"packages/headless": {
"name": "@flecks/headless",
"version": "4.0.0",
"dependencies": {
"@flecks/core": "^4.0.0"
},
"devDependencies": {
"@flecks/build": "^4.0.0",
"@flecks/fleck": "^4.0.0"
"@flecks/fleck": "^4.0.0",
"puppeteer": "^22.0.0"
}
},
"packages/passport": {
"name": "@flecks/passport",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/db": "^4.1.0",
"@flecks/redux": "^4.0.8",
"@flecks/session": "^4.0.8",
"@flecks/core": "^4.2.1",
"@flecks/db": "^4.1.1",
"@flecks/redux": "^4.0.9",
"@flecks/session": "^4.0.9",
"passport": "^0.7.0"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/passport-local": {
"name": "@flecks/passport-local",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/passport": "^4.0.8",
"@flecks/core": "^4.2.1",
"@flecks/passport": "^4.0.9",
"bcrypt": "^5.1.1",
"passport-local": "^1.0.0"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/passport-local-react": {
"name": "@flecks/passport-local-react",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/passport-local": "^4.0.8",
"@flecks/passport-react": "^4.0.8",
"@flecks/react": "^4.0.8"
"@flecks/core": "^4.2.1",
"@flecks/passport-local": "^4.0.9",
"@flecks/passport-react": "^4.0.9",
"@flecks/react": "^4.0.9"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/passport-react": {
"name": "@flecks/passport-react",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/passport": "^4.0.8",
"@flecks/react": "^4.0.8",
"@flecks/react-redux": "^4.0.8",
"@flecks/web": "^4.1.0"
"@flecks/core": "^4.2.1",
"@flecks/passport": "^4.0.9",
"@flecks/react": "^4.0.9",
"@flecks/react-redux": "^4.0.9",
"@flecks/web": "^4.1.1"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/react": {
"name": "@flecks/react",
"version": "4.0.8",
"version": "4.0.9",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@babel/preset-react": "^7.23.3",
"@flecks/core": "^4.2.0",
"@flecks/web": "^4.1.0",
"@flecks/core": "^4.2.1",
"@flecks/web": "^4.1.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"babel-merge": "^3.0.0",
"classnames": "^2.3.1",
@ -17033,61 +16977,61 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-refresh": "^0.14.0",
"react-router": "6.2.1",
"react-router-dom": "6.2.1",
"react-router": "6.22.1",
"react-router-dom": "6.22.1",
"react-tabs": "^6.0.2",
"redux-first-history": "5.1.1"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/react-redux": {
"name": "@flecks/react-redux",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/react": "^4.0.8",
"@flecks/redux": "^4.0.8",
"@flecks/core": "^4.2.1",
"@flecks/react": "^4.0.9",
"@flecks/redux": "^4.0.9",
"react-redux": "^7.2.2"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/redis": {
"name": "@flecks/redis",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"@socket.io/redis-adapter": "7.1.0",
"connect-redis": "^5.0.0",
"express-session": "^1.17.1",
"redis": "4.0.3"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/redux": {
"name": "@flecks/redux",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"@reduxjs/toolkit": "^1.5.0",
"debug": "^4.3.3",
"lodash.throttle": "^4.1.1",
"reduce-reducers": "^1.0.4"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/redux/node_modules/debug": {
@ -17108,51 +17052,51 @@
},
"packages/repl": {
"name": "@flecks/repl",
"version": "4.1.0",
"version": "4.1.1",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"command-exists": "^1.2.9",
"debug": "4.3.1"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/server": {
"name": "@flecks/server",
"version": "4.1.0",
"version": "4.1.1",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0"
"@flecks/core": "^4.2.1"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/session": {
"name": "@flecks/session",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/core": "^4.2.1",
"express": "^4.18.2",
"express-session": "^1.17.3"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/socket": {
"name": "@flecks/socket",
"version": "4.0.8",
"version": "4.0.9",
"license": "MIT",
"dependencies": {
"@flecks/core": "^4.2.0",
"@flecks/react": "^4.0.8",
"@flecks/core": "^4.2.1",
"@flecks/react": "^4.0.9",
"msgpack-lite": "^0.1.26",
"proxy-addr": "^2.0.6",
"schemapack": "^1.4.2",
@ -17160,25 +17104,24 @@
"socket.io-client": "^4.1.2"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1"
}
},
"packages/web": {
"name": "@flecks/web",
"version": "4.1.0",
"version": "4.1.1",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.17.0",
"@babel/types": "^7.17.0",
"@flecks/core": "^4.2.0",
"@flecks/server": "^4.1.0",
"@flecks/core": "^4.2.1",
"@flecks/server": "^4.1.1",
"@webpack-cli/serve": "^2.0.5",
"add-asset-html-webpack-plugin": "^6.0.0",
"assert": "^2.1.0",
"autoprefixer": "^10.4.17",
"before-build-webpack": "^0.2.13",
"body-parser": "^1.20.2",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "4.0.0",
@ -17205,9 +17148,9 @@
"webpack-dev-server": "^4.15.1"
},
"devDependencies": {
"@flecks/build": "^4.1.0",
"@flecks/fleck": "^4.1.0",
"puppeteer": "^22.0.0"
"@flecks/build": "^4.1.1",
"@flecks/fleck": "^4.1.1",
"@flecks/headless": "^4.0.0"
}
}
}

View File

@ -33,5 +33,59 @@ export const hooks = {
// You can also just:
return Component;
},
/**
* Provide routes for React Router.
*
* You can also build routes from a file structure using e.g.:
*
* ```js
* import {createRoutesFromContext} from '@flecks/react/router';
*
* export const hooks = {
* '@flecks/react/router.routes': () => (
* createRoutesFromContext(require.context('./routes'))
* ),
* };
* ```
*
* See [the documentation page on routing](../react#routing) for more details.
*
* @returns {RouteObject[]} An array of React Router route objects.
*/
'@flecks/react/router.routes': () => {
// You can also just return routes how React Router expects:
return [
{
path: '/',
Component: function Component() {
return <p>This is the root route</p>;
},
},
{
path: '/team',
children: [
{
path: ':teamId',
Component: function Component() {
const {teamId} = useParams();
return <p>This is team {teamId}.</p>;
},
},
{
index: true,
Component: function Component() {
return <p>This is the team overview.</p>;
},
},
],
},
{
path: '/about',
Component: function Component() {
return <p>This is the about page</p>;
},
},
];
}
};

View File

@ -1,12 +1,28 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import {createReduxHistory, history} from '@flecks/react/router/context';
import {unstable_HistoryRouter as HistoryRouter} from 'react-router-dom';
import {HistoryRouter as ReduxHistoryRouter} from './history-router';
import {
createBrowserRouter,
matchRoutes,
RouterProvider,
} from 'react-router-dom';
export const hooks = {
'@flecks/react.providers': (req, flecks) => (
flecks.fleck('@flecks/redux')
? [ReduxHistoryRouter, {history: createReduxHistory(flecks.redux)}]
: [HistoryRouter, {history}]
),
'@flecks/react.roots': async (req, res, flecks) => {
const {routes} = flecks.reactRouter;
// Determine if any of the initial routes are lazy
const lazyMatches = matchRoutes(routes, window.location)?.filter(({route}) => route.lazy);
// Load the lazy matches and update the routes before creating the router
// so we can hydrate the SSR-rendered content synchronously.
if (lazyMatches && lazyMatches?.length > 0) {
await Promise.all(
lazyMatches.map(async (m) => {
Object.entries(await m.route.lazy())
.forEach(([name, value]) => {
m.route[name] = value;
});
delete m.route.lazy;
}),
);
}
const router = createBrowserRouter(routes);
return [RouterProvider, {router}];
},
};

View File

@ -1,15 +0,0 @@
import {createReduxHistoryContext} from 'redux-first-history';
import {createBrowserHistory, createMemoryHistory} from 'history';
export const history = 'undefined' === typeof window
? createMemoryHistory()
: createBrowserHistory();
export const {
createReduxHistory,
routerMiddleware,
routerReducer,
} = createReduxHistoryContext({
history,
reduxTravelling: true,
});

View File

@ -0,0 +1,138 @@
import {resolve} from 'path';
function filePathToSegmentPath(path) {
let localPath = path;
let suffix = '';
// `-path` -> `path?`
if (path.startsWith('-')) {
localPath = path.slice(1);
suffix = '?';
}
// `[...path]` -> `*`
if (localPath.match(/^\[\.\.\..*\]$/)) {
return '*';
}
// `[path]` -> `:path`
if (localPath.match(/^\[.*\]$/)) {
return `:${localPath.slice(1, -1)}${suffix}`;
}
return `${localPath}${suffix}`;
}
export async function createRoutesFromFiletree({importer, paths, resolver}) {
const children = [{path: '/'}];
const elements = [];
const fix = [];
const moduleMap = {};
paths.forEach((path) => {
const resolved = resolver(path);
if (!moduleMap[resolved]) {
moduleMap[resolved] = [];
}
moduleMap[resolved].push(path);
});
// Build initial router object.
Object.entries(moduleMap)
.map(([, paths]) => (
// Second-longest: path without extension.
paths
.sort((l, r) => (l.length < r.length ? -1 : 1))
.slice(0, -1)
.pop()
))
.forEach((path) => {
let walk = children;
const parts = ['/', ...resolve('/', path).split('/').slice(1)];
const segments = parts.map(filePathToSegmentPath);
for (let i = 0; i < segments.length; ++i) {
const segment = segments[i];
const end = parts.length - 1;
// Colocation: bail.
if (segment.startsWith('_')) {
break;
}
// Create a new node if necessary.
let route = walk.find(({path}) => segment === path);
if (!route) {
route = {path: segment};
walk.push(route);
}
// Walking...
if (i < end - 1) {
if (!route.children) {
route.children = [];
fix.push(route.children);
}
route.children.route = route;
route.children.parent = walk;
walk = route.children;
}
// About to end, check last.
if (i === end - 1) {
const last = parts[i + 1];
// Non-index is a sibling, create if necessary.
if ('index' !== last) {
const offset = '/' === parts[i] ? 1 : 0;
const nestedPath = segments.slice(i + offset).join('/');
route = walk.find(({path}) => nestedPath === path);
if (!route) {
route = {path: nestedPath};
walk.push(route);
}
}
elements.push([walk, route, path]);
break;
}
}
});
// Mix imported modules into router object.
await Promise.all(
elements.map(async ([children, node, path]) => {
const route = await importer(path);
let {hoist} = route;
if (hoist > 0) {
const parts = node.path.split('/');
children.splice(children.indexOf(node), 1);
let walk = children;
while (hoist--) {
parts.unshift('/' !== walk.route.path ? walk.route.path : '');
walk = walk.parent;
}
node.path = parts.join('/');
walk.push(node);
}
Object.entries(route)
.forEach(([key, value]) => {
switch (key) {
case 'children':
// ???
return;
case 'hoist':
return;
case 'index':
delete node.path;
break;
default:
}
node[key] = value;
});
}),
);
// Clean up the build junk.
fix.forEach((children) => {
if (0 === children.length) {
delete children.route.children;
}
delete children.route;
delete children.parent;
});
return children;
}
export async function createRoutesFromContext(context) {
return createRoutesFromFiletree({
paths: context.keys(),
importer: context,
resolver: context.resolve,
});
}

View File

@ -1,24 +0,0 @@
/* eslint-disable react/prop-types */
// redux-first-router/rr6 doesn't honor hoisting.
const {createElement, useLayoutEffect, useState} = require('react');
const {Router} = require('react-router');
exports.HistoryRouter = function HistoryRouter({basename, children, history}) {
const [state, setState] = useState({
action: history.action,
location: history.location,
});
useLayoutEffect(() => {
history.listen(setState);
}, [history]);
// eslint-disable-next-line react/no-children-prop
return createElement(Router, {
basename,
children,
location: state.location,
navigationType: state.action,
navigator: history,
});
};

View File

@ -1,14 +1,32 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import {routerMiddleware, routerReducer} from '@flecks/react/router/context';
import {createRoutesFromFiletree, createRoutesFromContext} from './filetree-router';
export * from 'react-router-dom';
export * from 'redux-first-history';
export {createRoutesFromFiletree, createRoutesFromContext};
export const hooks = {
'@flecks/redux.slices': () => ({
router: routerReducer,
'@flecks/react/router.routes': () => createRoutesFromContext(require.context('./routes')),
'@flecks/core.config': () => ({
/**
* Select the root routes. Implement `@flecks/react/router.routes` in e.g. `@your/fleck` and
* set `root` to `@your/fleck` to render your routes.
*/
root: '@flecks/react/router',
}),
'@flecks/redux.store': (options) => {
options.middleware.push(routerMiddleware);
'@flecks/core.starting': async (flecks) => {
const {root} = flecks.get('@flecks/react/router');
flecks.reactRouter.routes = await flecks.invokeFleck('@flecks/react/router.routes', root);
},
'@flecks/web.config': (req, flecks) => ({
root: flecks.get('@flecks/react/router').root,
}),
};
export const mixin = (Flecks) => class FlecksWithReactRouterServer extends Flecks {
constructor(runtime) {
super(runtime);
this.reactRouter = {handler: undefined, routes: undefined};
}
};

View File

@ -0,0 +1,26 @@
// Adapted from https://reactrouter.com/en/main/guides/ssr
export function createFetchRequest(req, res) {
// Build and propagate request parameters.
const headers = new Headers();
Object.entries(req.headers).forEach(([key, values = []]) => {
(Array.isArray(values) ? values : [values]).forEach((value) => {
headers.append(key, value);
});
});
const init = {headers, method: req.method};
if (req.method !== 'GET' && req.method !== 'HEAD') {
const urlSearchParams = new URLSearchParams();
Object.entries(req.body)
.forEach(([name, value]) => {
urlSearchParams.append(name, value);
});
init.body = urlSearchParams;
}
// Build the request URL.
const origin = `${req.protocol}://${req.get('host')}`;
const {href} = new URL(req.originalUrl || req.url, origin);
// Allow aborting the request.
const controller = new AbortController();
res.on('close', () => controller.abort());
return new Request(href, {...init, signal: controller.signal});
}

View File

@ -0,0 +1,5 @@
import React from 'react';
export function Component() {
return <p className="flecks-router-root">No routes.</p>;
}

View File

@ -1,7 +1,37 @@
import {StaticRouter} from 'react-router-dom/server';
import {Flecks} from '@flecks/core';
import {
createStaticHandler,
createStaticRouter,
StaticRouterProvider,
} from 'react-router-dom/server';
import {createFetchRequest} from './request';
export const hooks = {
'@flecks/react.providers': (req, flecks) => (
flecks.get('@flecks/react.ssr') ? [StaticRouter, {location: req.url}] : []
'@flecks/web/server.request.socket': (flecks) => async (req, res, next) => {
const {handler} = flecks.reactRouter;
const context = await handler.query(createFetchRequest(req, res));
if (context instanceof Response && [301, 302, 303, 307, 308].includes(context.status)) {
res.redirect(context.status, context.headers.get('Location'));
return;
}
if ([404].includes(context.statusCode)) {
res.status(context.statusCode);
next();
return;
}
next();
},
'@flecks/react.roots': async (req, res, flecks) => {
const {handler} = flecks.reactRouter;
const context = await handler.query(createFetchRequest(req, res));
const router = createStaticRouter(handler.dataRoutes, context);
return [StaticRouterProvider, {context, router}];
},
'@flecks/server.up': Flecks.priority(
async (flecks) => {
flecks.reactRouter.handler = createStaticHandler(flecks.reactRouter.routes);
},
{before: '@flecks/web/server'},
),
};

View File

@ -0,0 +1,96 @@
import {expect} from 'chai';
import {createRoutesFromContext} from '@flecks/react/router';
it('does not nest siblings', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/siblings'));
expect(routes)
.to.deep.equal([{path: '/', name: 'index'}, {path: 'test', name: 'sibling'}]);
});
it('does nest siblings under parent', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/siblings-parent'));
expect(routes)
.to.deep.equal([
{
path: '/',
name: 'index',
children: [
{path: ':test', name: 'test'},
{path: ':test/nest', name: 'nest'},
],
},
]);
});
it('does nest children', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/children'));
expect(routes)
.to.deep.equal([
{
path: '/',
name: 'parent',
children: [
{path: 'test', name: 'child'},
],
},
]);
});
it('finds dynamic segments', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/dynamic-segments'));
expect(routes)
.to.deep.equal([
{
path: '/',
name: 'index',
children: [
{path: ':otherTest?', name: 'otherTest'},
{path: ':test', name: 'test'},
],
},
]);
});
it('finds splats', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/splats'));
expect(routes)
.to.deep.equal([
{
path: '/',
name: 'index',
children: [
{path: '*', name: 'splat'},
],
},
]);
});
it('allows colocated sources', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/colocated'));
expect(routes)
.to.deep.equal([{path: '/', name: 'index'}]);
});
it('promotes module with index export', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/index'));
expect(routes)
.to.deep.equal([
{
path: '/',
name: 'index',
children: [
{index: true, name: 'test'},
],
},
]);
});
it('hoists', async () => {
const routes = await createRoutesFromContext(require.context('./filetree/hoist'));
expect(routes)
.to.deep.equal([
{path: '/', name: 'index'},
{path: '/test', name: 'test'},
]);
});

View File

@ -0,0 +1 @@
export const name = 'parent';

View File

@ -0,0 +1 @@
export const name = 'child';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1 @@
export const name = 'otherTest';

View File

@ -0,0 +1 @@
export const name = 'test';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1,3 @@
export const hoist = 1;
export const name = 'test';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1,2 @@
export const index = true;
export const name = 'test';

View File

@ -0,0 +1 @@
export const name = 'test';

View File

@ -0,0 +1 @@
export const name = 'nest';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1 @@
export const name = 'sibling';

View File

@ -0,0 +1 @@
export const name = 'splat';

View File

@ -0,0 +1 @@
export const name = 'index';

View File

@ -0,0 +1,18 @@
import {expect} from 'chai';
import {withWeb} from '@flecks/headless/test/helpers/with-web';
it('allows custom routes', withWeb(
async ({page, response}) => {
expect(response)
.to.not.be.null;
expect(response.ok())
.to.be.true;
const output = await page.waitForSelector('.custom-route');
expect(output)
.to.not.be.undefined;
},
{
template: 'templates/router-custom',
},
));

View File

@ -0,0 +1,52 @@
import {expect} from 'chai';
import {withWeb} from '@flecks/headless/test/helpers/with-web';
it('implements data router', withWeb(
async ({page, response}) => {
expect(response)
.to.not.be.null;
expect(response.ok())
.to.be.true;
const output = await page.waitForSelector('.test-contact');
expect(await output?.evaluate((el) => el.textContent))
.to.equal('2');
await page.goto(new URL('/contact/2/edit', await page.url()));
expect(await page.waitForSelector('#test-contact-form'))
.to.not.be.undefined;
await page.type('[name="id"]', '5');
await page.click('[type="submit"]');
const {pathname} = new URL(await page.url());
expect(pathname)
.to.equal('/contact/52');
},
{
pagePath: '/contact/2',
template: 'templates/router-custom',
},
));
it('implements data router with ssr', withWeb(
async ({page, response}) => {
expect(response)
.to.not.be.null;
expect(response.ok())
.to.be.true;
expect(await page.waitForSelector('#test-contact-form'))
.to.not.be.undefined;
await page.type('[name="id"]', '5');
const navigation = page.waitForNavigation();
await page.click('[type="submit"]');
await navigation;
const {pathname} = new URL(await page.url());
expect(pathname)
.to.equal('/contact/52');
},
{
beforePage: async ({page}) => {
await page.setJavaScriptEnabled(false);
},
pagePath: '/contact/2/edit',
template: 'templates/router-custom',
},
));

View File

@ -0,0 +1,36 @@
import {expect} from 'chai';
import {withWeb} from '@flecks/headless/test/helpers/with-web';
it('provides default routes', withWeb(
async ({page, response}) => {
expect(response)
.to.not.be.null;
expect(response.ok())
.to.be.true;
const output = await page.waitForSelector('.flecks-router-root');
expect(output)
.to.not.be.undefined;
},
{
template: 'templates/router-default',
},
));
it('provides default routes with ssr', withWeb(
async ({page, response}) => {
expect(response)
.to.not.be.null;
expect(response.ok())
.to.be.true;
const output = await page.waitForSelector('.flecks-router-root');
expect(output)
.to.not.be.undefined;
},
{
beforePage: async ({page}) => {
await page.setJavaScriptEnabled(false);
},
template: 'templates/router-default',
},
));

View File

@ -0,0 +1,19 @@
import {expect} from 'chai';
import {withWeb} from '@flecks/headless/test/helpers/with-web';
it('allows lazy routes', withWeb(
async ({page, response}) => {
expect(response)
.to.not.be.null;
expect(response.ok())
.to.be.true;
const output = await page.waitForSelector('.lazy');
expect(output)
.to.not.be.undefined;
},
{
pagePath: '/lazy',
template: 'templates/router-custom',
},
));

View File

@ -0,0 +1,7 @@
'@flecks/core': {}
'@flecks/server': {}
'@flecks/react': {}
'@flecks/react/router': {
root: 'test'
}
'test:./test': {}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,5 @@
{
"dependencies": {
"@flecks/react": "*"
}
}

View File

@ -0,0 +1,5 @@
import {createRoutesFromContext} from '@flecks/react/router';
export const hooks = {
'@flecks/react/router.routes': () => createRoutesFromContext(require.context('./routes')),
};

View File

@ -0,0 +1,30 @@
import {React} from '@flecks/react';
import {
Form,
useLoaderData,
redirect,
} from '@flecks/react/router';
export function Component() {
const {contact} = useLoaderData();
return (
<Form method="post" id="test-contact-form">
<input
aria-label="Contact ID"
type="text"
name="id"
defaultValue={contact}
/>
<button type="submit">Submit</button>
</Form>
);
}
export async function action({request}) {
const formData = await request.formData();
return redirect(`/contact/${formData.get('id')}`);
}
export async function loader({params}) {
return {contact: params.contactId};
}

View File

@ -0,0 +1,11 @@
import {React} from '@flecks/react';
import {useParams} from '@flecks/react/router';
export function Component() {
const params = useParams();
return (
<div className="test-contact">
{params.contactId}
</div>
);
}

View File

@ -0,0 +1,8 @@
import {React} from '@flecks/react';
import {Outlet} from '@flecks/react/router';
export const hoist = 1;
export function Component() {
return <div className="test-contacts"><Outlet /></div>;
}

View File

@ -0,0 +1,6 @@
import {React} from '@flecks/react';
import {Outlet} from '@flecks/react/router';
export function Component() {
return <div className="custom-route"><Outlet /></div>;
}

View File

@ -0,0 +1,9 @@
import {React} from '@flecks/react';
export async function lazy() {
return {
Component() {
return <p className="lazy" />;
},
};
}

View File

@ -0,0 +1,4 @@
'@flecks/core': {}
'@flecks/server': {}
'@flecks/react': {}
'@flecks/react/router': {}

View File

@ -0,0 +1 @@
{}