From 3330c73065a3a5a51c2c4722b6f3824a1b0d98e1 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 6 Sep 2022 18:22:12 +0100 Subject: [PATCH 01/19] Move route parser to inside rwjsRouter --- .../templates/web-routerRoutes.d.ts.template | 45 +------------------ packages/router/src/index.ts | 2 + packages/router/src/routeParserTypes.ts | 44 ++++++++++++++++++ 3 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 packages/router/src/routeParserTypes.ts diff --git a/packages/internal/src/generate/templates/web-routerRoutes.d.ts.template b/packages/internal/src/generate/templates/web-routerRoutes.d.ts.template index 9be9a4069fda..15416872783d 100644 --- a/packages/internal/src/generate/templates/web-routerRoutes.d.ts.template +++ b/packages/internal/src/generate/templates/web-routerRoutes.d.ts.template @@ -1,14 +1,7 @@ -import '@redwoodjs/router' +import { RouteParams, QueryParams } from '@redwoodjs/router' import { A } from 'ts-toolbelt' - -type RouteParams = ${"Route extends `${string}/${infer Rest}`"} - ? A.Compute> - : {} - -type QueryParams = Record - declare module '@redwoodjs/router' { interface AvailableRoutes { // Only "" components with a "name" and "path" prop will be populated here. @@ -20,39 +13,3 @@ ${routes.map( } } -type ParamType = match extends 'Int' - ? number - : match extends 'Boolean' - ? boolean - : match extends 'Float' - ? number - : string - -// Path string parser for Redwood Routes -type ParsedParams = - // {a:Int}/[...moar] - ${"PartialRoute extends `{${infer Param}:${infer Match}}/${infer Rest}`"} - ? // check for greedy match e.g. {b}/{c:Int} - // Param = b}/{c, Rest2 = {c, Match = Int so we reconstruct the old one {c + : + Int + } - ${"Param extends `${infer Param2}}/${infer Rest2}`"} - ? { [ParamName in Param2]: string } & - ${"ParsedParams<`${Rest2}:${Match}}`> &"} - ${"ParsedParams<`${Rest}`>"} - ${": { [Entry in Param]: ParamType } & ParsedParams<`${Rest}`>"} - : // has type, but at the end e.g. {d:Int} - ${"PartialRoute extends `{${infer Param}:${infer Match}}`"} - ? // Greedy match order 2 - ${"Param extends `${infer Param2}}/${infer Rest2}`"} - ? { [ParamName in Param2]: string } & - ${"ParsedParams<`${Rest2}:${Match}}`>"} - : { [Entry in Param]: ParamType } - : // no type, but has stuff after it, e.g. {c}/{d} - ${"PartialRoute extends `{${infer Param}}/${infer Rest}`"} - ${"? { [ParamName in Param]: string } & ParsedParams<`${Rest}`>"} - : // last one with no type e.g. {d} - ${"PartialRoute extends `{${infer Param}}`"} - ? { [ParamName in Param]: string } - : // if theres a non param - ${"PartialRoute extends `${string}/${infer Rest}`"} - ${"? ParsedParams<`${Rest}`>"} - : {} diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 41992980aecb..c6988b438455 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -39,3 +39,5 @@ export interface AvailableRoutes { } export { SkipNavLink, SkipNavContent } from '@reach/skip-nav' + +export * from './routeParserTypes' diff --git a/packages/router/src/routeParserTypes.ts b/packages/router/src/routeParserTypes.ts new file mode 100644 index 000000000000..15b09a9e3bff --- /dev/null +++ b/packages/router/src/routeParserTypes.ts @@ -0,0 +1,44 @@ +import { A } from 'ts-toolbelt' + +type GenericParams = Record + +export type QueryParams = GenericParams + +export type RouteParams = Route extends `${string}/${infer Rest}` + ? A.Compute> + : GenericParams + +export type ParamType = match extends 'Int' + ? number + : match extends 'Boolean' + ? boolean + : match extends 'Float' + ? number + : string + +// Path string parser for Redwood Routes +type ParsedParams = + // {a:Int}/[...moar] + PartialRoute extends `{${infer Param}:${infer Match}}/${infer Rest}` + ? // check for greedy match e.g. {b}/{c:Int} + // Param = b}/{c, Rest2 = {c, Match = Int so we reconstruct the old one {c + : + Int + } + Param extends `${infer Param2}}/${infer Rest2}` + ? { [ParamName in Param2]: string } & ParsedParams<`${Rest2}:${Match}}`> & + ParsedParams<`${Rest}`> + : { [Entry in Param]: ParamType } & ParsedParams<`${Rest}`> + : // has type, but at the end e.g. {d:Int} + PartialRoute extends `{${infer Param}:${infer Match}}` + ? // Greedy match order 2 + Param extends `${infer Param2}}/${infer Rest2}` + ? { [ParamName in Param2]: string } & ParsedParams<`${Rest2}:${Match}}`> + : { [Entry in Param]: ParamType } + : // no type, but has stuff after it, e.g. {c}/{d} + PartialRoute extends `{${infer Param}}/${infer Rest}` + ? { [ParamName in Param]: string } & ParsedParams<`${Rest}`> + : // last one with no type e.g. {d} + PartialRoute extends `{${infer Param}}` + ? { [ParamName in Param]: string } + : // if theres a non param + PartialRoute extends `${string}/${infer Rest}` + ? ParsedParams<`${Rest}`> + : Record From 09e462e42b0b436e0032991a4d4eede28f189a62 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Wed, 7 Sep 2022 13:43:00 +0100 Subject: [PATCH 02/19] Simple tsdlite setup with jest --- package.json | 2 + packages/router/jest.config.js | 18 ++++- .../router/src/__typetests__/example.test.ts | 10 +++ packages/router/type-test.jest.config.js | 8 ++ yarn.lock | 73 ++++++++++++++++++- 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 packages/router/src/__typetests__/example.test.ts create mode 100644 packages/router/type-test.jest.config.js diff --git a/package.json b/package.json index 3368bf64408c..1a130de233aa 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", + "@tsd/typescript": "^4.8.2", "@types/babel__generator": "7.6.4", "@types/fs-extra": "9.0.13", "@types/jest": "29.0.0", @@ -81,6 +82,7 @@ "fs-extra": "10.1.0", "is-port-reachable": "3.1.0", "jest": "29.0.1", + "jest-runner-tsd": "^4.0.0", "jscodeshift": "0.13.1", "lerna": "5.4.3", "lodash.template": "4.5.0", diff --git a/packages/router/jest.config.js b/packages/router/jest.config.js index 8e8e379fef38..1f849c26f933 100644 --- a/packages/router/jest.config.js +++ b/packages/router/jest.config.js @@ -1,5 +1,19 @@ /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - setupFilesAfterEnv: ['./jest.setup.js'], - testEnvironment: 'jest-environment-jsdom', + projects: [ + { + displayName: 'code', + setupFilesAfterEnv: ['./jest.setup.js'], + testEnvironment: 'jest-environment-jsdom', + testMatch: ['**/*.test.+(ts|tsx|js)', '!**/__typetests__/*.ts'], + }, + { + displayName: { + color: 'blue', + name: 'types', + }, + runner: 'jest-runner-tsd', + testMatch: ['**/__typetests__/*.test.ts'], + }, + ], } diff --git a/packages/router/src/__typetests__/example.test.ts b/packages/router/src/__typetests__/example.test.ts new file mode 100644 index 000000000000..d4e1050949a3 --- /dev/null +++ b/packages/router/src/__typetests__/example.test.ts @@ -0,0 +1,10 @@ +import { expectType } from 'tsd-lite' + +const users = [ + { name: 'Oby', age: 12 }, + { name: 'Heera', age: 32 }, +] + +const loggedInUser = users[0].age + +expectType(loggedInUser) diff --git a/packages/router/type-test.jest.config.js b/packages/router/type-test.jest.config.js new file mode 100644 index 000000000000..66ba0bf35c04 --- /dev/null +++ b/packages/router/type-test.jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + displayName: { + color: 'blue', + name: 'types', + }, + runner: 'jest-runner-tsd', + testMatch: ['**/__typetests__/*.test.ts'], +} diff --git a/yarn.lock b/yarn.lock index 8eea530f2cd6..c5aae31c7429 100644 --- a/yarn.lock +++ b/yarn.lock @@ -184,7 +184,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.15.8, @babel/code-frame@npm:^7.18.6, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": version: 7.18.6 resolution: "@babel/code-frame@npm:7.18.6" dependencies: @@ -8272,6 +8272,13 @@ __metadata: languageName: node linkType: hard +"@tsd/typescript@npm:^4.8.2": + version: 4.8.2 + resolution: "@tsd/typescript@npm:4.8.2" + checksum: f77f252757ec658d730e7b58cba1700e53593e6ef35da92b90f432c0efb536deba9200b3bf4fd715d9cee47e0d322f566eb637dda749f3c61f1e7d9b2435e244 + languageName: node + linkType: hard + "@types/aria-query@npm:^4.2.0": version: 4.2.2 resolution: "@types/aria-query@npm:4.2.2" @@ -13575,6 +13582,27 @@ __metadata: languageName: node linkType: hard +"create-jest-runner@npm:^0.12.0": + version: 0.12.0 + resolution: "create-jest-runner@npm:0.12.0" + dependencies: + chalk: ^4.1.0 + jest-worker: ^29.0.0 + throat: ^6.0.1 + peerDependencies: + "@jest/test-result": ^28.0.0 || ^29.0.0 + jest-runner: ^28.0.0 || ^29.0.0 + peerDependenciesMeta: + "@jest/test-result": + optional: true + jest-runner: + optional: true + bin: + create-jest-runner: generator/index.js + checksum: 8f808ed045666a83a25bb38080a888b3b9d1a3adcadfb1a51fdc622df6db185c1e4d62d5a00670383082e0a0b5affee03bc09d9082ee9125de8234201766e6c9 + languageName: node + linkType: hard + "create-redwood-app@workspace:packages/create-redwood-app": version: 0.0.0-use.local resolution: "create-redwood-app@workspace:packages/create-redwood-app" @@ -20223,6 +20251,20 @@ __metadata: languageName: node linkType: hard +"jest-runner-tsd@npm:^4.0.0": + version: 4.0.0 + resolution: "jest-runner-tsd@npm:4.0.0" + dependencies: + "@babel/code-frame": ^7.15.8 + chalk: ^4.1.2 + create-jest-runner: ^0.12.0 + tsd-lite: ^0.6.0 + peerDependencies: + "@tsd/typescript": ^3.8.3 || ^4.0.7 + checksum: 4c64092881d29575589ad97ce3cf849d430a132319607fd6b8e172d45c231dcb67e2d1f4c4ca74e567c651421a30438c41077a452a3acfb59a27f7b34b1e47ac + languageName: node + linkType: hard + "jest-runner@npm:^29.0.1": version: 29.0.1 resolution: "jest-runner@npm:29.0.1" @@ -20451,6 +20493,17 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:^29.0.0": + version: 29.0.2 + resolution: "jest-worker@npm:29.0.2" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 64d32fe918e1f70294e3b7a2f5e873412c2809efaa8c960face611d0cce71324a30563484d20e906bcfbf872eabd0ea8f1c0e28f6678e9448ae1730064e1c811 + languageName: node + linkType: hard + "jest-worker@npm:^29.0.1": version: 29.0.1 resolution: "jest-worker@npm:29.0.1" @@ -27115,6 +27168,7 @@ __metadata: "@testing-library/react": 12.1.5 "@testing-library/react-hooks": 8.0.1 "@testing-library/user-event": 14.4.3 + "@tsd/typescript": ^4.8.2 "@types/babel__generator": 7.6.4 "@types/fs-extra": 9.0.13 "@types/jest": 29.0.0 @@ -27136,6 +27190,7 @@ __metadata: fs-extra: 10.1.0 is-port-reachable: 3.1.0 jest: 29.0.1 + jest-runner-tsd: ^4.0.0 jscodeshift: 0.13.1 lerna: 5.4.3 lodash.template: 4.5.0 @@ -29332,6 +29387,13 @@ __metadata: languageName: node linkType: hard +"throat@npm:^6.0.1": + version: 6.0.1 + resolution: "throat@npm:6.0.1" + checksum: 60a42d762a35d21ac71abd9eb4026b665fbbbf6ddd7bcbdcacc3c3b20f7b99f41939afedf9fe3273611f1b7c003ee98ac4dc94aa5edd1a6dc2a49985ad2545e1 + languageName: node + linkType: hard + "throttle-debounce@npm:^2.1.0": version: 2.3.0 resolution: "throttle-debounce@npm:2.3.0" @@ -29790,6 +29852,15 @@ __metadata: languageName: node linkType: hard +"tsd-lite@npm:^0.6.0": + version: 0.6.0 + resolution: "tsd-lite@npm:0.6.0" + peerDependencies: + "@tsd/typescript": ^3.8.3 || ^4.0.7 + checksum: f8d66238d50c764c7afb93a355e8c437b853d0a66ab6dedf55c08b63614619d8a94aaab19c4c4b0a183b76c1bad088fccdc812778f574e02fd62206cc4b8ec30 + languageName: node + linkType: hard + "tslib@npm:2.3.1, tslib@npm:~2.3.0": version: 2.3.1 resolution: "tslib@npm:2.3.1" From 6c860582ccabc978c6d1e183b223083b3993c2a2 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Wed, 7 Sep 2022 18:20:39 +0100 Subject: [PATCH 03/19] Add tsd tests for routeParamsTypes --- .../router/src/__typetests__/example.test.ts | 10 ----- .../__typetests__/routeParamsTypes.test.ts | 41 +++++++++++++++++++ packages/router/type-test.jest.config.js | 8 ---- 3 files changed, 41 insertions(+), 18 deletions(-) delete mode 100644 packages/router/src/__typetests__/example.test.ts create mode 100644 packages/router/src/__typetests__/routeParamsTypes.test.ts delete mode 100644 packages/router/type-test.jest.config.js diff --git a/packages/router/src/__typetests__/example.test.ts b/packages/router/src/__typetests__/example.test.ts deleted file mode 100644 index d4e1050949a3..000000000000 --- a/packages/router/src/__typetests__/example.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { expectType } from 'tsd-lite' - -const users = [ - { name: 'Oby', age: 12 }, - { name: 'Heera', age: 32 }, -] - -const loggedInUser = users[0].age - -expectType(loggedInUser) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts new file mode 100644 index 000000000000..ec1bca5af13f --- /dev/null +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -0,0 +1,41 @@ +import { expectType } from 'tsd-lite' + +import { RouteParams } from '@redwoodjs/router' + +test('Single parameters', () => { + const singleParameters: RouteParams<'bazinga/{id:Int}'> = { + id: 1, + } + + expectType<{ id: number }>(singleParameters) +}) + +test('Route string with no types defaults to string', () => { + const singleParameters: RouteParams<'/blog/{year}/{month}/{day}/{slug}'> = { + year: '2020', + month: '01', + day: '01', + slug: 'hello-world', + } + + expectType<{ year: string; month: string; day: string; slug: string }>( + singleParameters + ) +}) + +test('Custom param types', () => { + const customParams: RouteParams<'/post/{name:slug}'> = { + name: 'hello-world-slug', + } + + expectType<{ name: string }>(customParams) +}) + +test('Glob route params', () => { + const globRoutes: RouteParams<'/from/{fromDate...}/to/{toDate...}'> = { + fromDate: '2021/11/03', + toDate: '2021/11/17', + } + + expectType<{ fromDate: string; toDate: string }>(globRoutes) +}) diff --git a/packages/router/type-test.jest.config.js b/packages/router/type-test.jest.config.js deleted file mode 100644 index 66ba0bf35c04..000000000000 --- a/packages/router/type-test.jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - displayName: { - color: 'blue', - name: 'types', - }, - runner: 'jest-runner-tsd', - testMatch: ['**/__typetests__/*.test.ts'], -} From 73d5906932835e1b6f8210b6b56240f7864c374e Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 8 Sep 2022 18:26:26 +0100 Subject: [PATCH 04/19] Don't use caret --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1a130de233aa..9d1687d174e2 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", - "@tsd/typescript": "^4.8.2", + "@tsd/typescript": "4.8.2", "@types/babel__generator": "7.6.4", "@types/fs-extra": "9.0.13", "@types/jest": "29.0.0", @@ -82,7 +82,7 @@ "fs-extra": "10.1.0", "is-port-reachable": "3.1.0", "jest": "29.0.1", - "jest-runner-tsd": "^4.0.0", + "jest-runner-tsd": "4.0.0", "jscodeshift": "0.13.1", "lerna": "5.4.3", "lodash.template": "4.5.0", diff --git a/yarn.lock b/yarn.lock index c5aae31c7429..9ac7569a9fe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8272,7 +8272,7 @@ __metadata: languageName: node linkType: hard -"@tsd/typescript@npm:^4.8.2": +"@tsd/typescript@npm:4.8.2": version: 4.8.2 resolution: "@tsd/typescript@npm:4.8.2" checksum: f77f252757ec658d730e7b58cba1700e53593e6ef35da92b90f432c0efb536deba9200b3bf4fd715d9cee47e0d322f566eb637dda749f3c61f1e7d9b2435e244 @@ -20251,7 +20251,7 @@ __metadata: languageName: node linkType: hard -"jest-runner-tsd@npm:^4.0.0": +"jest-runner-tsd@npm:4.0.0": version: 4.0.0 resolution: "jest-runner-tsd@npm:4.0.0" dependencies: @@ -27168,7 +27168,7 @@ __metadata: "@testing-library/react": 12.1.5 "@testing-library/react-hooks": 8.0.1 "@testing-library/user-event": 14.4.3 - "@tsd/typescript": ^4.8.2 + "@tsd/typescript": 4.8.2 "@types/babel__generator": 7.6.4 "@types/fs-extra": 9.0.13 "@types/jest": 29.0.0 @@ -27190,7 +27190,7 @@ __metadata: fs-extra: 10.1.0 is-port-reachable: 3.1.0 jest: 29.0.1 - jest-runner-tsd: ^4.0.0 + jest-runner-tsd: 4.0.0 jscodeshift: 0.13.1 lerna: 5.4.3 lodash.template: 4.5.0 From b98bba6dd848ee44021984fd963f48567926b5f5 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 8 Sep 2022 18:33:14 +0100 Subject: [PATCH 05/19] Dedupe yarn --- yarn.lock | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9ac7569a9fe1..a8e33d536d69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20493,7 +20493,7 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:^29.0.0": +"jest-worker@npm:^29.0.0, jest-worker@npm:^29.0.1": version: 29.0.2 resolution: "jest-worker@npm:29.0.2" dependencies: @@ -20504,17 +20504,6 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:^29.0.1": - version: 29.0.1 - resolution: "jest-worker@npm:29.0.1" - dependencies: - "@types/node": "*" - merge-stream: ^2.0.0 - supports-color: ^8.0.0 - checksum: 5d064c4b4868bc65726733b52d0d1d6316065abf531387c63fbee592cb5e33bd7da820ca5cf50e0819f35bbffa4c78569c048b4dbf0c264b9e18f5dfca8dde21 - languageName: node - linkType: hard - "jest@npm:29.0.1": version: 29.0.1 resolution: "jest@npm:29.0.1" From 7ca4f7875b6a14db9078c999f248bd12f817fac4 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Fri, 9 Sep 2022 13:55:02 +0100 Subject: [PATCH 06/19] Break up RouteParser, add aditional tests --- packages/router/package.json | 2 +- .../__typetests__/routeParamsTypes.test.ts | 119 +++++++++++++----- packages/router/src/routeParserTypes.ts | 76 +++++++++-- 3 files changed, 153 insertions(+), 44 deletions(-) diff --git a/packages/router/package.json b/packages/router/package.json index 5f8edcdccb80..75ae2855b740 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -18,7 +18,7 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,ts,tsx\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "jest src", + "test": "jest", "test:watch": "yarn test --watch" }, "dependencies": { diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index ec1bca5af13f..88819b3f0049 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -1,41 +1,100 @@ +// import { A } from 'ts-toolbelt' import { expectType } from 'tsd-lite' -import { RouteParams } from '@redwoodjs/router' +import type { RouteParams, ParamType } from '@redwoodjs/router' -test('Single parameters', () => { - const singleParameters: RouteParams<'bazinga/{id:Int}'> = { - id: 1, - } +describe('RouteParams<>', () => { + test('Single parameters', () => { + const singleParameters: RouteParams<'bazinga/{id:Int}'> = { + id: 1, + } - expectType<{ id: number }>(singleParameters) -}) + expectType<{ id: number }>(singleParameters) + }) -test('Route string with no types defaults to string', () => { - const singleParameters: RouteParams<'/blog/{year}/{month}/{day}/{slug}'> = { - year: '2020', - month: '01', - day: '01', - slug: 'hello-world', - } - - expectType<{ year: string; month: string; day: string; slug: string }>( - singleParameters - ) -}) + test('Route string with no types defaults to string', () => { + const singleParameters: RouteParams<'/blog/{year}/{month}/{day}/{slug}'> = { + year: '2020', + month: '01', + day: '01', + slug: 'hello-world', + } + + expectType<{ year: string; month: string; day: string; slug: string }>( + singleParameters + ) + }) + + test('Custom param types', () => { + const customParams: RouteParams<'/post/{name:slug}'> = { + name: 'hello-world-slug', + } + + expectType<{ name: string }>(customParams) + }) + + // test('Glob route params', () => { + // const globRoutes: RouteParams<'/from/{fromDate...}/to/{toDate...}'> = { + // fromDate: '2021/11/03', + // toDate: '2021/11/17', + // } + + // expectType<{ fromDate: string; toDate: string }>(globRoutes) + // }) + + test('Mixed typed and untyped params', () => { + const untypedFirst: RouteParams<'/mixed/{b}/{c:Boolean}'> = { + b: 'bazinga', + c: true, + } + + const typedFirst: RouteParams<'/mixed/{b:Float}/{c}'> = { + b: 1245, + c: 'stringy-string', + } -test('Custom param types', () => { - const customParams: RouteParams<'/post/{name:slug}'> = { - name: 'hello-world-slug', - } + expectType<{ b: string; c: boolean }>(untypedFirst) + expectType<{ b: number; c: string }>(typedFirst) + }) - expectType<{ name: string }>(customParams) + // test('Params in the middle', () => { + // const paramsInTheMiddle: RouteParams<'/posts/{authorId:string}/{id:Int}/edit'> = + // { + // authorId: 'id:author', + // id: 10, + // } + + // // A.Compute<{ authorId: string; id: number } & Record> + + // expectType<{ authorId: string; id: number }>(paramsInTheMiddle) + // }) }) -test('Glob route params', () => { - const globRoutes: RouteParams<'/from/{fromDate...}/to/{toDate...}'> = { - fromDate: '2021/11/03', - toDate: '2021/11/17', - } +describe('ParamType<>', () => { + test('Float', () => { + const float: ParamType<'Float'> = 1 + + expectType(float) + }) + + test('Boolean', () => { + // Use a function because assigning a boolean narrows the type automatically + const returnBool: (a?: any) => ParamType<'Boolean'> = (a) => { + return !!a + } + + expectType(returnBool()) + }) + + test('Int', () => { + const myInt: ParamType<'Int'> = 1 + + expectType(myInt) + }) + + test('String', () => { + const myString: ParamType<'String'> = 'bazinga' - expectType<{ fromDate: string; toDate: string }>(globRoutes) + expectType(myString) + }) }) diff --git a/packages/router/src/routeParserTypes.ts b/packages/router/src/routeParserTypes.ts index 15b09a9e3bff..b49feebc5674 100644 --- a/packages/router/src/routeParserTypes.ts +++ b/packages/router/src/routeParserTypes.ts @@ -16,29 +16,79 @@ export type ParamType = match extends 'Int' ? number : string +// This is used for a specific case where the first param doesnt have a type, but second one does +type AdjacentParams< + TParam extends string, + TMatch extends string, + TRest extends string +> = { [ParamName in TParam]: string } & ParsedParams<`${TRest}:${TMatch}}`> & + ParsedParams<`${TRest}`> + +type TypedParamInFront< + TParam extends string, + TMatch extends string, + TRest extends string +> = TParam extends `${infer Param2}}/${infer Rest2}` + ? // check for greedy match (basically if the param contains a slash in it) + // e.g. in {b}/{c:Int} it matches b}/{c as the param + // Rest2 = {c, Match = Int so we reconstruct the old one {c + : + Int + } + AdjacentParams + : // Otherwise its a regular match + { [Entry in TParam]: ParamType } & ParsedParams<`${TRest}`> + +// has type, but at the end e.g. {d:Int} +type TypedParamAtEnd< + TParam extends string, + TMatch extends string +> = TParam extends `${infer Param2}}/${infer Rest2}` + ? { [ParamName in Param2]: string } & ParsedParams<`${Rest2}:${TMatch}}`> + : { [Entry in TParam]: ParamType } + +// no type, but has stuff after it, e.g. {c}/{d} +type NoTypesButParams = { + [ParamName in TParam]: string +} & ParsedParams<`${TRest}`> + +type JustParamNoType = { [ParamName in TParam]: string } + // Path string parser for Redwood Routes type ParsedParams = // {a:Int}/[...moar] PartialRoute extends `{${infer Param}:${infer Match}}/${infer Rest}` - ? // check for greedy match e.g. {b}/{c:Int} - // Param = b}/{c, Rest2 = {c, Match = Int so we reconstruct the old one {c + : + Int + } - Param extends `${infer Param2}}/${infer Rest2}` - ? { [ParamName in Param2]: string } & ParsedParams<`${Rest2}:${Match}}`> & - ParsedParams<`${Rest}`> - : { [Entry in Param]: ParamType } & ParsedParams<`${Rest}`> + ? TypedParamInFront : // has type, but at the end e.g. {d:Int} PartialRoute extends `{${infer Param}:${infer Match}}` ? // Greedy match order 2 - Param extends `${infer Param2}}/${infer Rest2}` - ? { [ParamName in Param2]: string } & ParsedParams<`${Rest2}:${Match}}`> - : { [Entry in Param]: ParamType } + TypedParamAtEnd : // no type, but has stuff after it, e.g. {c}/{d} PartialRoute extends `{${infer Param}}/${infer Rest}` - ? { [ParamName in Param]: string } & ParsedParams<`${Rest}`> - : // last one with no type e.g. {d} + ? NoTypesButParams + : // last one with no type e.g. {d} - just a param PartialRoute extends `{${infer Param}}` - ? { [ParamName in Param]: string } + ? JustParamNoType : // if theres a non param PartialRoute extends `${string}/${infer Rest}` ? ParsedParams<`${Rest}`> - : Record + : // Fallback when doesn't match any of these + Record + +/** + * Translation in pseudocode without ternaries + * +if ('{c:Int}/...rest') { + checkForGreedyMatch() +} else if ('{c:Int}') { + typedParamAtEnd() +} else if ('{c}/...rest') { + noTypesButParams() +} else if('{d}') { + justParamNoType() +} else if ('bazinga/..rest') { + // Call itself + parseParamsRecursiveCall(rest) +} + + +{...glob}/...rest // beginning +{...glob} // end +**/ From fbd69a05a227e66f343bb0c49b80bb9bdc313159 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Fri, 9 Sep 2022 16:47:12 +0100 Subject: [PATCH 07/19] Fix glob type | Switch to expectAssignable --- .../__typetests__/routeParamsTypes.test.ts | 102 +++++++++--------- ...teParserTypes.ts => routeParserTypes.d.ts} | 53 ++++++--- 2 files changed, 91 insertions(+), 64 deletions(-) rename packages/router/src/{routeParserTypes.ts => routeParserTypes.d.ts} (57%) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 88819b3f0049..f45733c48919 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -1,100 +1,102 @@ -// import { A } from 'ts-toolbelt' -import { expectType } from 'tsd-lite' +import { expectAssignable } from 'tsd-lite' -import type { RouteParams, ParamType } from '@redwoodjs/router' +import type { RouteParams, ParamType } from '../routeParserTypes' describe('RouteParams<>', () => { test('Single parameters', () => { - const singleParameters: RouteParams<'bazinga/{id:Int}'> = { + expectAssignable>({ id: 1, - } - - expectType<{ id: number }>(singleParameters) + }) }) test('Route string with no types defaults to string', () => { - const singleParameters: RouteParams<'/blog/{year}/{month}/{day}/{slug}'> = { + expectAssignable>({ year: '2020', month: '01', day: '01', slug: 'hello-world', + }) + }) + + test('Custom param types', () => { + const customParams = { + name: 'hello-world-slug', } - expectType<{ year: string; month: string; day: string; slug: string }>( - singleParameters + expectAssignable>(customParams) + }) + + test('Multiple Glob route params', () => { + const globRoutes = { + fromDate: '2021/11/03', + toDate: '2021/11/17', + } + + expectAssignable>( + globRoutes ) }) - test('Custom param types', () => { - const customParams: RouteParams<'/post/{name:slug}'> = { - name: 'hello-world-slug', + test('Single Glob route params', () => { + const globRoutes = { + fromDate: '2021/11/03', } - expectType<{ name: string }>(customParams) + expectAssignable>(globRoutes) }) - // test('Glob route params', () => { - // const globRoutes: RouteParams<'/from/{fromDate...}/to/{toDate...}'> = { - // fromDate: '2021/11/03', - // toDate: '2021/11/17', - // } + test('Glob params in the middle', () => { + test('Multiple Glob route params', () => { + const middleGlob = { + folders: 'src/lib/auth.js', + } - // expectType<{ fromDate: string; toDate: string }>(globRoutes) - // }) + expectAssignable>(middleGlob) + }) + }) test('Mixed typed and untyped params', () => { - const untypedFirst: RouteParams<'/mixed/{b}/{c:Boolean}'> = { + const untypedFirst = { b: 'bazinga', c: true, } - const typedFirst: RouteParams<'/mixed/{b:Float}/{c}'> = { + const typedFirst = { b: 1245, c: 'stringy-string', } - expectType<{ b: string; c: boolean }>(untypedFirst) - expectType<{ b: number; c: string }>(typedFirst) + expectAssignable>(untypedFirst) + expectAssignable>(typedFirst) }) - // test('Params in the middle', () => { - // const paramsInTheMiddle: RouteParams<'/posts/{authorId:string}/{id:Int}/edit'> = - // { - // authorId: 'id:author', - // id: 10, - // } - - // // A.Compute<{ authorId: string; id: number } & Record> + test('Params in the middle', () => { + const paramsInTheMiddle = { + authorId: 'id:author', + id: 10, + } - // expectType<{ authorId: string; id: number }>(paramsInTheMiddle) - // }) + expectAssignable>( + paramsInTheMiddle + ) + }) }) describe('ParamType<>', () => { test('Float', () => { - const float: ParamType<'Float'> = 1 - - expectType(float) + expectAssignable>(1.02) }) test('Boolean', () => { - // Use a function because assigning a boolean narrows the type automatically - const returnBool: (a?: any) => ParamType<'Boolean'> = (a) => { - return !!a - } - - expectType(returnBool()) + expectAssignable>(true) + expectAssignable>(false) }) test('Int', () => { - const myInt: ParamType<'Int'> = 1 - - expectType(myInt) + expectAssignable>(51) }) test('String', () => { - const myString: ParamType<'String'> = 'bazinga' - - expectType(myString) + expectAssignable>('bazinga') }) }) diff --git a/packages/router/src/routeParserTypes.ts b/packages/router/src/routeParserTypes.d.ts similarity index 57% rename from packages/router/src/routeParserTypes.ts rename to packages/router/src/routeParserTypes.d.ts index b49feebc5674..cc569ad72545 100644 --- a/packages/router/src/routeParserTypes.ts +++ b/packages/router/src/routeParserTypes.d.ts @@ -1,5 +1,8 @@ import { A } from 'ts-toolbelt' +// @NOTE: This file is a .d.ts - definition file +// It does not contain any actual code + type GenericParams = Record export type QueryParams = GenericParams @@ -16,14 +19,19 @@ export type ParamType = match extends 'Int' ? number : string -// This is used for a specific case where the first param doesnt have a type, but second one does +// This is used for a specific case where the first param +// doesnt have a type, but second one does +// See comment above it's usage type AdjacentParams< TParam extends string, TMatch extends string, TRest extends string -> = { [ParamName in TParam]: string } & ParsedParams<`${TRest}:${TMatch}}`> & +> = { + [ParamName in TParam as RemoveGlobDots]: string +} & ParsedParams<`${TRest}:${TMatch}}`> & ParsedParams<`${TRest}`> +// Note that this has to be first in the list, because it does greedy param checks type TypedParamInFront< TParam extends string, TMatch extends string, @@ -34,35 +42,52 @@ type TypedParamInFront< // Rest2 = {c, Match = Int so we reconstruct the old one {c + : + Int + } AdjacentParams : // Otherwise its a regular match - { [Entry in TParam]: ParamType } & ParsedParams<`${TRest}`> + { + [ParamName in TParam]: ParamType + } & ParsedParams<`${TRest}`> -// has type, but at the end e.g. {d:Int} +// This is the second part of greedy match +// has type, but at the end e.g. {d:Int} or d:Int} <-- no opening brace +// Needs to be right after TypedParamInFront type TypedParamAtEnd< TParam extends string, TMatch extends string > = TParam extends `${infer Param2}}/${infer Rest2}` - ? { [ParamName in Param2]: string } & ParsedParams<`${Rest2}:${TMatch}}`> - : { [Entry in TParam]: ParamType } + ? { + [ParamName in Param2]: string + } & ParsedParams<`${Rest2}:${TMatch}}`> + : { [ParamName in TParam]: ParamType } + +// This mapper takes a param name and will remove dots if its a glob +// e.g. fromDate... -> fromDate +// Only used when the param doesn't have a type, because glob params dont have types +type RemoveGlobDots = Param extends `${infer GlobParamName}...` + ? GlobParamName + : Param -// no type, but has stuff after it, e.g. {c}/{d} -type NoTypesButParams = { - [ParamName in TParam]: string +// no type, but has stuff after it, e.g. {c}/{d} or {c}/bazinga +type MultiParamsWithoutType = { + [ParamName in TParam as RemoveGlobDots]: string } & ParsedParams<`${TRest}`> -type JustParamNoType = { [ParamName in TParam]: string } +type JustParamNoType = { + [ParamName in TParam as RemoveGlobDots]: string +} // Path string parser for Redwood Routes type ParsedParams = - // {a:Int}/[...moar] + // PartialRoute extends `{${infer GlobParam}...}/${infer Rest}}` + // ? ParsedParams & ParsedParams + // : // {a:Int}/[...moar] PartialRoute extends `{${infer Param}:${infer Match}}/${infer Rest}` ? TypedParamInFront : // has type, but at the end e.g. {d:Int} PartialRoute extends `{${infer Param}:${infer Match}}` ? // Greedy match order 2 TypedParamAtEnd - : // no type, but has stuff after it, e.g. {c}/{d} + : // no type, but has stuff after it, e.g. {c}/{d} or {c}/bazinga PartialRoute extends `{${infer Param}}/${infer Rest}` - ? NoTypesButParams + ? MultiParamsWithoutType : // last one with no type e.g. {d} - just a param PartialRoute extends `{${infer Param}}` ? JustParamNoType @@ -80,7 +105,7 @@ if ('{c:Int}/...rest') { } else if ('{c:Int}') { typedParamAtEnd() } else if ('{c}/...rest') { - noTypesButParams() + multipleParamsNoTypes() } else if('{d}') { justParamNoType() } else if ('bazinga/..rest') { From e671ab2704cb850b780676a31c4656b2d79e1c13 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Fri, 9 Sep 2022 16:49:29 +0100 Subject: [PATCH 08/19] Comment --- packages/router/src/routeParserTypes.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/routeParserTypes.d.ts b/packages/router/src/routeParserTypes.d.ts index cc569ad72545..a47ddb262841 100644 --- a/packages/router/src/routeParserTypes.d.ts +++ b/packages/router/src/routeParserTypes.d.ts @@ -1,7 +1,7 @@ import { A } from 'ts-toolbelt' // @NOTE: This file is a .d.ts - definition file -// It does not contain any actual code +// It does not contain any actual code - required because tsd-jest-runner complains otherwise type GenericParams = Record From 684f7c0065bc49e6559edba47764c5ec6073ed7c Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Fri, 9 Sep 2022 16:54:49 +0100 Subject: [PATCH 09/19] Rename one of the types --- packages/router/src/routeParserTypes.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/routeParserTypes.d.ts b/packages/router/src/routeParserTypes.d.ts index a47ddb262841..9a4191cdafc0 100644 --- a/packages/router/src/routeParserTypes.d.ts +++ b/packages/router/src/routeParserTypes.d.ts @@ -22,7 +22,7 @@ export type ParamType = match extends 'Int' // This is used for a specific case where the first param // doesnt have a type, but second one does // See comment above it's usage -type AdjacentParams< +type ParamsFromGreedyMatch< TParam extends string, TMatch extends string, TRest extends string @@ -40,7 +40,7 @@ type TypedParamInFront< ? // check for greedy match (basically if the param contains a slash in it) // e.g. in {b}/{c:Int} it matches b}/{c as the param // Rest2 = {c, Match = Int so we reconstruct the old one {c + : + Int + } - AdjacentParams + ParamsFromGreedyMatch : // Otherwise its a regular match { [ParamName in TParam]: ParamType From 50a7671c6d9f81c99f3e94669a59d02c4ae5a4c8 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 12 Sep 2022 12:32:38 +0100 Subject: [PATCH 10/19] Use a ts file for types instead, because *.d.ts don't get copied to dist --- packages/router/src/__typetests__/routeParamsTypes.test.ts | 5 ++++- packages/router/src/__typetests__/tsconfig.json | 3 +++ packages/router/src/index.ts | 3 ++- .../src/{routeParserTypes.d.ts => routeParamsTypes.ts} | 3 --- 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 packages/router/src/__typetests__/tsconfig.json rename packages/router/src/{routeParserTypes.d.ts => routeParamsTypes.ts} (96%) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index f45733c48919..881a477d2ae5 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -1,6 +1,9 @@ import { expectAssignable } from 'tsd-lite' -import type { RouteParams, ParamType } from '../routeParserTypes' +// @WARN!: I'm importing this from the built package. +// So you will need to build, before running test again! +// See https://github.com/jest-community/jest-runner-tsd/issues/111 +import type { RouteParams, ParamType } from '@redwoodjs/router' describe('RouteParams<>', () => { test('Single parameters', () => { diff --git a/packages/router/src/__typetests__/tsconfig.json b/packages/router/src/__typetests__/tsconfig.json new file mode 100644 index 000000000000..4082f16a5d91 --- /dev/null +++ b/packages/router/src/__typetests__/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index c6988b438455..15b0edfed6c3 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -40,4 +40,5 @@ export interface AvailableRoutes { export { SkipNavLink, SkipNavContent } from '@reach/skip-nav' -export * from './routeParserTypes' +// Used by packages/internal/src/generate/templates/web-routerRoutes.d.ts.template +export * from './routeParamsTypes' diff --git a/packages/router/src/routeParserTypes.d.ts b/packages/router/src/routeParamsTypes.ts similarity index 96% rename from packages/router/src/routeParserTypes.d.ts rename to packages/router/src/routeParamsTypes.ts index 9a4191cdafc0..6e22a807c08c 100644 --- a/packages/router/src/routeParserTypes.d.ts +++ b/packages/router/src/routeParamsTypes.ts @@ -1,8 +1,5 @@ import { A } from 'ts-toolbelt' -// @NOTE: This file is a .d.ts - definition file -// It does not contain any actual code - required because tsd-jest-runner complains otherwise - type GenericParams = Record export type QueryParams = GenericParams From 1845e6295a3368a97f46ee1cb0bd8fcaed068bb0 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 12 Sep 2022 14:38:55 +0100 Subject: [PATCH 11/19] Update packages/router/src/routeParamsTypes.ts --- packages/router/src/routeParamsTypes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/router/src/routeParamsTypes.ts b/packages/router/src/routeParamsTypes.ts index 6e22a807c08c..240ae5d96c5c 100644 --- a/packages/router/src/routeParamsTypes.ts +++ b/packages/router/src/routeParamsTypes.ts @@ -111,6 +111,4 @@ if ('{c:Int}/...rest') { } -{...glob}/...rest // beginning -{...glob} // end **/ From 6fdeae51f2ea567a775c54e1f65f0a5bd9a4ea98 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 12 Sep 2022 15:14:57 +0100 Subject: [PATCH 12/19] Add extra tsconfig to typetests | Import type file directly --- .../router/src/__typetests__/routeParamsTypes.test.ts | 2 +- packages/router/src/__typetests__/tsconfig.json | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 881a477d2ae5..29e418726401 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -3,7 +3,7 @@ import { expectAssignable } from 'tsd-lite' // @WARN!: I'm importing this from the built package. // So you will need to build, before running test again! // See https://github.com/jest-community/jest-runner-tsd/issues/111 -import type { RouteParams, ParamType } from '@redwoodjs/router' +import type { RouteParams, ParamType } from '../routeParamsTypes' describe('RouteParams<>', () => { test('Single parameters', () => { diff --git a/packages/router/src/__typetests__/tsconfig.json b/packages/router/src/__typetests__/tsconfig.json index 4082f16a5d91..a8ba755cc20a 100644 --- a/packages/router/src/__typetests__/tsconfig.json +++ b/packages/router/src/__typetests__/tsconfig.json @@ -1,3 +1,10 @@ { - "extends": "../../tsconfig.json" + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": false, + "incremental": true, + "declaration": false, + "declarationMap": false, + "emitDeclarationOnly": false + }, } From 88edc3901924a248146b31741e76c88e6e92d8f5 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 12 Sep 2022 18:17:42 +0100 Subject: [PATCH 13/19] Update packages/router/src/__typetests__/routeParamsTypes.test.ts Co-authored-by: Tom Mrazauskas --- packages/router/src/__typetests__/routeParamsTypes.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 29e418726401..c0d89c523a6d 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -1,8 +1,5 @@ import { expectAssignable } from 'tsd-lite' -// @WARN!: I'm importing this from the built package. -// So you will need to build, before running test again! -// See https://github.com/jest-community/jest-runner-tsd/issues/111 import type { RouteParams, ParamType } from '../routeParamsTypes' describe('RouteParams<>', () => { From 061bcc86bfa68429812e1b2cc2c038e04bb9ba81 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Thu, 15 Sep 2022 14:13:30 +0200 Subject: [PATCH 14/19] Add a couple of more router matchPath tests --- packages/router/src/__tests__/util.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/router/src/__tests__/util.test.ts b/packages/router/src/__tests__/util.test.ts index 3e34aabf27f5..04e4c4af6f63 100644 --- a/packages/router/src/__tests__/util.test.ts +++ b/packages/router/src/__tests__/util.test.ts @@ -64,6 +64,11 @@ describe('matchPath', () => { match: true, params: { id: 789 }, }) + + expect(matchPath('/{id:Int}/bazinga', '/89/bazinga')).toEqual({ + match: true, + params: { id: 89 }, + }) }) it('transforms a param for Boolean', () => { @@ -213,7 +218,7 @@ describe('matchPath', () => { }) // suffixed - expect(matchPath('/{a...}-a', '/1/2-a')).toEqual({ + expect(matchPath('/{a...}-a/kittens', '/1/2-a/kittens')).toEqual({ match: true, params: { a: '1/2', From e68fb7f0238e8ca2cfc552d5131273d20757d8f2 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 15 Sep 2022 19:23:48 +0100 Subject: [PATCH 15/19] Add more tests --- .../__typetests__/routeParamsTypes.test.ts | 31 +++++++++++++++++++ packages/router/src/routeParamsTypes.ts | 6 +--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index c0d89c523a6d..6adb973992b9 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -9,6 +9,15 @@ describe('RouteParams<>', () => { }) }) + test('Starts with parameter', () => { + + + expectAssignable>({ + position: 1, + driver: 44, + }) + }) + test('Route string with no types defaults to string', () => { expectAssignable>({ year: '2020', @@ -26,6 +35,16 @@ describe('RouteParams<>', () => { expectAssignable>(customParams) }) + test('Parameter inside string', () => { + // @NOTE: this is currently falling back to GenericParams + // because the type parser doesn't handle this case + const stringConcat: RouteParams<'/signedUp/e{status:Boolean}y'> = { + status: true + } + + expectAssignable>(stringConcat) + }) + test('Multiple Glob route params', () => { const globRoutes = { fromDate: '2021/11/03', @@ -45,6 +64,18 @@ describe('RouteParams<>', () => { expectAssignable>(globRoutes) }) + + test('Starts with Glob route params', () => { + const globRoutes = { + cuddles: '1/2', + } + + // @NOTE: this is currently falling back to GenericParams + // because the type parser doesn't handle this case + + expectAssignable>(globRoutes) + }) + test('Glob params in the middle', () => { test('Multiple Glob route params', () => { const middleGlob = { diff --git a/packages/router/src/routeParamsTypes.ts b/packages/router/src/routeParamsTypes.ts index 240ae5d96c5c..438f7eeb627d 100644 --- a/packages/router/src/routeParamsTypes.ts +++ b/packages/router/src/routeParamsTypes.ts @@ -73,8 +73,6 @@ type JustParamNoType = { // Path string parser for Redwood Routes type ParsedParams = - // PartialRoute extends `{${infer GlobParam}...}/${infer Rest}}` - // ? ParsedParams & ParsedParams // : // {a:Int}/[...moar] PartialRoute extends `{${infer Param}:${infer Match}}/${infer Rest}` ? TypedParamInFront @@ -92,7 +90,7 @@ type ParsedParams = PartialRoute extends `${string}/${infer Rest}` ? ParsedParams<`${Rest}`> : // Fallback when doesn't match any of these - Record + GenericParams /** * Translation in pseudocode without ternaries @@ -109,6 +107,4 @@ if ('{c:Int}/...rest') { // Call itself parseParamsRecursiveCall(rest) } - - **/ From 64758f6c382d93053edeee7f0fd39bd12ca08b54 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 15 Sep 2022 19:40:16 +0100 Subject: [PATCH 16/19] Fix the strange edge cases --- .../__typetests__/routeParamsTypes.test.ts | 11 +++----- packages/router/src/routeParamsTypes.ts | 25 ++++++++++++------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 6adb973992b9..69d15fcff726 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -1,6 +1,6 @@ import { expectAssignable } from 'tsd-lite' -import type { RouteParams, ParamType } from '../routeParamsTypes' +import type { RouteParams, ParamType, GenericParams } from '../routeParamsTypes' describe('RouteParams<>', () => { test('Single parameters', () => { @@ -36,8 +36,6 @@ describe('RouteParams<>', () => { }) test('Parameter inside string', () => { - // @NOTE: this is currently falling back to GenericParams - // because the type parser doesn't handle this case const stringConcat: RouteParams<'/signedUp/e{status:Boolean}y'> = { status: true } @@ -67,13 +65,10 @@ describe('RouteParams<>', () => { test('Starts with Glob route params', () => { const globRoutes = { - cuddles: '1/2', + description: 'cute', } - // @NOTE: this is currently falling back to GenericParams - // because the type parser doesn't handle this case - - expectAssignable>(globRoutes) + expectAssignable>(globRoutes) }) test('Glob params in the middle', () => { diff --git a/packages/router/src/routeParamsTypes.ts b/packages/router/src/routeParamsTypes.ts index 438f7eeb627d..ef808947ff8c 100644 --- a/packages/router/src/routeParamsTypes.ts +++ b/packages/router/src/routeParamsTypes.ts @@ -1,9 +1,10 @@ import { A } from 'ts-toolbelt' -type GenericParams = Record +export type GenericParams = Record export type QueryParams = GenericParams +// @Note that '' is matched by ${string} so it still parses from the beginning. export type RouteParams = Route extends `${string}/${infer Rest}` ? A.Compute> : GenericParams @@ -74,17 +75,17 @@ type JustParamNoType = { // Path string parser for Redwood Routes type ParsedParams = // : // {a:Int}/[...moar] - PartialRoute extends `{${infer Param}:${infer Match}}/${infer Rest}` + PartialRoute extends `${string}{${infer Param}:${infer Match}}${string}/${infer Rest}` ? TypedParamInFront : // has type, but at the end e.g. {d:Int} - PartialRoute extends `{${infer Param}:${infer Match}}` + PartialRoute extends `${string}{${infer Param}:${infer Match}}${string}` ? // Greedy match order 2 TypedParamAtEnd : // no type, but has stuff after it, e.g. {c}/{d} or {c}/bazinga - PartialRoute extends `{${infer Param}}/${infer Rest}` + PartialRoute extends `${string}{${infer Param}}${string}/${infer Rest}` ? MultiParamsWithoutType : // last one with no type e.g. {d} - just a param - PartialRoute extends `{${infer Param}}` + PartialRoute extends `${string}{${infer Param}}${string}` ? JustParamNoType : // if theres a non param PartialRoute extends `${string}/${infer Rest}` @@ -95,16 +96,22 @@ type ParsedParams = /** * Translation in pseudocode without ternaries * -if ('{c:Int}/...rest') { +if ('he{c:Int}lo/...rest') { checkForGreedyMatch() -} else if ('{c:Int}') { +} else if ('he{c:Int}lo') { typedParamAtEnd() -} else if ('{c}/...rest') { +} else if ('he{c}yo/...rest') { multipleParamsNoTypes() -} else if('{d}') { +} else if('he{d}yo') { justParamNoType() } else if ('bazinga/..rest') { // Call itself parseParamsRecursiveCall(rest) +} else{ + // fallback, because it doesn't match any of the above + GenericParams } + +Its a bit odd, but the he{d}llo is a form we support in the router +// e.g. /signedUp/e{status:Boolean}y **/ From c1d424fab7e9d36271f3e98aee78d1e30d50d06f Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 15 Sep 2022 19:54:52 +0100 Subject: [PATCH 17/19] Use expectType in a different way for more accurate tests --- .../__typetests__/routeParamsTypes.test.ts | 83 ++++++++++--------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 69d15fcff726..8b76cd2d85d6 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -1,110 +1,119 @@ -import { expectAssignable } from 'tsd-lite' +import { expectAssignable, expectType } from 'tsd-lite' -import type { RouteParams, ParamType, GenericParams } from '../routeParamsTypes' +import type { RouteParams, ParamType } from '../routeParamsTypes' describe('RouteParams<>', () => { test('Single parameters', () => { - expectAssignable>({ - id: 1, - }) + const simple: RouteParams<'bazinga/{id:Int}'> = { + id: 2, + } + + expectType<{ id: number }>(simple) }) test('Starts with parameter', () => { - - - expectAssignable>({ + const startParam: RouteParams<'/{position:Int}/{driver:Float}/stats'> = { position: 1, driver: 44, - }) + } + + expectType(startParam.driver) + expectType(startParam.position) }) test('Route string with no types defaults to string', () => { - expectAssignable>({ + const untypedParams: RouteParams<'/blog/{year}/{month}/{day}/{slug}'> = { year: '2020', month: '01', day: '01', slug: 'hello-world', - }) + } + + expectType(untypedParams.year) + expectType(untypedParams.month) + expectType(untypedParams.day) + expectType(untypedParams.slug) }) test('Custom param types', () => { - const customParams = { + const customParams: RouteParams<'/post/{name:slug}'> = { name: 'hello-world-slug', } - expectAssignable>(customParams) + expectType(customParams.name) }) test('Parameter inside string', () => { const stringConcat: RouteParams<'/signedUp/e{status:Boolean}y'> = { - status: true + status: true, } - expectAssignable>(stringConcat) + expectType(stringConcat.status) }) test('Multiple Glob route params', () => { - const globRoutes = { + const globRoutes: RouteParams<'/from/{fromDate...}/to/{toDate...}'> = { fromDate: '2021/11/03', toDate: '2021/11/17', } - expectAssignable>( - globRoutes - ) + expectType(globRoutes.fromDate) + expectType(globRoutes.toDate) }) test('Single Glob route params', () => { - const globRoutes = { + const globRoutes: RouteParams<'/from/{fromDate...}'> = { fromDate: '2021/11/03', } - expectAssignable>(globRoutes) + expectType(globRoutes.fromDate) }) - test('Starts with Glob route params', () => { - const globRoutes = { + const globRoutes: RouteParams<'/{description...}-little/kittens'> = { description: 'cute', } - expectAssignable>(globRoutes) + expectType(globRoutes.description) }) test('Glob params in the middle', () => { test('Multiple Glob route params', () => { - const middleGlob = { + const middleGlob: RouteParams<'/repo/{folders...}/edit'> = { folders: 'src/lib/auth.js', } - expectAssignable>(middleGlob) + expectType(middleGlob.folders) }) }) test('Mixed typed and untyped params', () => { - const untypedFirst = { + const untypedFirst: RouteParams<'/mixed/{b}/{c:Boolean}'> = { b: 'bazinga', c: true, } - const typedFirst = { + const typedFirst: RouteParams<'/mixed/{b:Float}/{c}'> = { b: 1245, c: 'stringy-string', } - expectAssignable>(untypedFirst) - expectAssignable>(typedFirst) + expectType(untypedFirst.b) + expectType(untypedFirst.c) + + expectType(typedFirst.b) + expectType(typedFirst.c) }) test('Params in the middle', () => { - const paramsInTheMiddle = { - authorId: 'id:author', - id: 10, - } + const paramsInTheMiddle: RouteParams<'/posts/{authorId:string}/{id:Int}/edit'> = + { + authorId: 'id:author', + id: 10, + } - expectAssignable>( - paramsInTheMiddle - ) + expectType(paramsInTheMiddle.authorId) + expectType(paramsInTheMiddle.id) }) }) From 9e3f586b2255ddcf0bca791f48e1a2cf0f6d5658 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 15 Sep 2022 20:11:18 +0100 Subject: [PATCH 18/19] Add FAQ to tests --- .../src/__typetests__/routeParamsTypes.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 8b76cd2d85d6..5364f635cdba 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -2,13 +2,26 @@ import { expectAssignable, expectType } from 'tsd-lite' import type { RouteParams, ParamType } from '../routeParamsTypes' +/** + * FAQ: + * - why aren't you using expectAssignable in all tests? + * because {b: string} is assignable to Record, and then test isn't deep enough + * + * - why aren't you just checking the entire type? + * because sometimes, the parser returns {Params & GenericParams} (and thats ok!), checking the full type will cause failures + * + * - why are you assigning the const values if you're just checking the types? + * for readability param?.id! everywhere is ugly too - it helps with making these tests read like documentation + * + */ + describe('RouteParams<>', () => { test('Single parameters', () => { const simple: RouteParams<'bazinga/{id:Int}'> = { id: 2, } - expectType<{ id: number }>(simple) + expectType(simple.id) }) test('Starts with parameter', () => { From 37e1c7969ed8d774a53cd7047e53a673995896cf Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Thu, 15 Sep 2022 20:14:04 +0100 Subject: [PATCH 19/19] tweak messages --- packages/router/src/__typetests__/routeParamsTypes.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/__typetests__/routeParamsTypes.test.ts b/packages/router/src/__typetests__/routeParamsTypes.test.ts index 5364f635cdba..173c8ee9be6b 100644 --- a/packages/router/src/__typetests__/routeParamsTypes.test.ts +++ b/packages/router/src/__typetests__/routeParamsTypes.test.ts @@ -5,13 +5,13 @@ import type { RouteParams, ParamType } from '../routeParamsTypes' /** * FAQ: * - why aren't you using expectAssignable in all tests? - * because {b: string} is assignable to Record, and then test isn't deep enough + * because {b: string} is assignable to Record, and then test isn't accurate enough * * - why aren't you just checking the entire type? * because sometimes, the parser returns {Params & GenericParams} (and thats ok!), checking the full type will cause failures * * - why are you assigning the const values if you're just checking the types? - * for readability param?.id! everywhere is ugly too - it helps with making these tests read like documentation + * for readability: param?.id! everywhere is ugly - it helps with making these tests read like documentation * */