From 64101524ebd28392a62b3c232c3e74f429fe4079 Mon Sep 17 00:00:00 2001 From: Elmer Bulthuis Date: Sat, 9 Mar 2024 00:10:41 +0100 Subject: [PATCH] scripts for building and cleaning (#13) --- .editorconfig | 7 +- .github/workflows/test-goodrouter-npm.yml | 4 +- .gitignore | 4 +- .prettierignore | 4 +- goodrouter.code-workspace | 2 + package-lock.json | 218 ++++- package.json | 5 +- .../Goodrouter.Bench/Goodrouter.Bench.csproj | 4 +- packages/net/Goodrouter.Bench/Program.cs | 8 +- packages/net/Goodrouter.Bench/RouterBench.cs | 126 +-- .../Goodrouter.Spec/Goodrouter.Spec.csproj | 4 +- packages/net/Goodrouter.Spec/RouteNodeSpec.cs | 14 +- packages/net/Goodrouter.Spec/RouterSpec.cs | 310 +++---- .../net/Goodrouter.Spec/StringUtilitySpec.cs | 32 +- .../Goodrouter.Spec/TemplateUtilitySpec.cs | 96 +-- packages/net/Goodrouter/RouteNode.cs | 792 +++++++++--------- packages/net/Goodrouter/Router.cs | 332 ++++---- packages/net/Goodrouter/StringUtility.cs | 32 +- packages/net/Goodrouter/TemplateUtility.cs | 72 +- packages/npm/goodrouter/.vscode/launch.json | 2 +- packages/npm/goodrouter/package.json | 33 +- packages/npm/goodrouter/scripts/build.js | 34 + packages/npm/goodrouter/scripts/clean.js | 8 + packages/npm/goodrouter/src/json.ts | 14 +- packages/npm/goodrouter/src/root.ts | 8 + .../npm/goodrouter/src/route-node.spec.ts | 62 -- .../npm/goodrouter/src/route-node.test.ts | 49 ++ packages/npm/goodrouter/src/route-node.ts | 607 +++++++------- packages/npm/goodrouter/src/router-options.ts | 102 ++- .../npm/goodrouter/src/router-parse.bench.ts | 46 +- .../goodrouter/src/router-stringify.bench.ts | 36 +- packages/npm/goodrouter/src/router.spec.ts | 266 ------ packages/npm/goodrouter/src/router.test.ts | 258 ++++++ packages/npm/goodrouter/src/router.ts | 268 +++--- packages/npm/goodrouter/src/template.spec.ts | 82 -- packages/npm/goodrouter/src/template.test.ts | 60 ++ packages/npm/goodrouter/src/template.ts | 55 +- .../npm/goodrouter/src/testing/parameters.ts | 27 +- .../npm/goodrouter/src/testing/templates.ts | 25 +- packages/npm/goodrouter/src/utils/root.ts | 9 - .../npm/goodrouter/src/utils/string.spec.ts | 11 - .../npm/goodrouter/src/utils/string.test.ts | 11 + packages/npm/goodrouter/src/utils/string.ts | 23 +- packages/npm/goodrouter/tsconfig.json | 6 +- packages/npm/www/package.json | 1 + 45 files changed, 2153 insertions(+), 2016 deletions(-) create mode 100755 packages/npm/goodrouter/scripts/build.js create mode 100755 packages/npm/goodrouter/scripts/clean.js create mode 100644 packages/npm/goodrouter/src/root.ts delete mode 100644 packages/npm/goodrouter/src/route-node.spec.ts create mode 100644 packages/npm/goodrouter/src/route-node.test.ts delete mode 100644 packages/npm/goodrouter/src/router.spec.ts create mode 100644 packages/npm/goodrouter/src/router.test.ts delete mode 100644 packages/npm/goodrouter/src/template.spec.ts create mode 100644 packages/npm/goodrouter/src/template.test.ts delete mode 100644 packages/npm/goodrouter/src/utils/root.ts delete mode 100644 packages/npm/goodrouter/src/utils/string.spec.ts create mode 100644 packages/npm/goodrouter/src/utils/string.test.ts diff --git a/.editorconfig b/.editorconfig index e8581be..0e89cad 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,12 @@ root = true [*] -end_of_line = lf +charset = utf-8 insert_final_newline = true +end_of_line = lf indent_style = space -indent_size = 4 - -[*.{json,yaml,yml,md}] indent_size = 2 +max_line_length = 100 [Makefile] indent_style = tab diff --git a/.github/workflows/test-goodrouter-npm.yml b/.github/workflows/test-goodrouter-npm.yml index ef6e8fd..22abf45 100644 --- a/.github/workflows/test-goodrouter-npm.yml +++ b/.github/workflows/test-goodrouter-npm.yml @@ -9,9 +9,9 @@ on: - packages/npm/** jobs: - test-unit: + test: runs-on: ubuntu-latest - container: node:20.9-alpine3.17 + container: node:21.5.0-alpine3.19 steps: - uses: actions/checkout@v4 - run: npm ci diff --git a/.gitignore b/.gitignore index 9ac3e03..7f2942d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,10 @@ node_modules/ target/ +transpiled/ +types/ +bundled/ out/ -out-*/ coverage/ obj/ bin/ diff --git a/.prettierignore b/.prettierignore index 89f9ac0..ed7a7b5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,3 @@ -out/ +transpiled/ +types/ +bundled/ diff --git a/goodrouter.code-workspace b/goodrouter.code-workspace index 0b03680..1ee30ca 100644 --- a/goodrouter.code-workspace +++ b/goodrouter.code-workspace @@ -42,6 +42,8 @@ "editor.rulers": [100], "editor.defaultFormatter": "esbenp.prettier-vscode", "rust-analyzer.check.command": "clippy", + "typescript.tsdk": "./node_modules/typescript/lib", + "npm.packageManager": "npm", }, "extensions": { "recommendations": [ diff --git a/package-lock.json b/package-lock.json index fee1ed4..f363ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ ], "devDependencies": { "cspell": "^8.6.0", - "prettier": "^3.2.5" + "prettier": "^3.2.5", + "rollup": "^4.12.1", + "typescript": "^5.4.2" } }, "node_modules/@11ty/dependency-tree": { @@ -645,6 +647,175 @@ "node": ">= 8" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz", + "integrity": "sha512-iU2Sya8hNn1LhsYyf0N+L4Gf9Qc+9eBTJJJsaOGUp+7x4n2M9dxTt8UvhJl3oeftSjblSlpCfvjA/IfP3g5VjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.1.tgz", + "integrity": "sha512-wlzcWiH2Ir7rdMELxFE5vuM7D6TsOcJ2Yw0c3vaBR3VOsJFVTx9xvwnAvhgU5Ii8Gd6+I11qNHwndDscIm0HXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.1.tgz", + "integrity": "sha512-YRXa1+aZIFN5BaImK+84B3uNK8C6+ynKLPgvn29X9s0LTVCByp54TB7tdSMHDR7GTV39bz1lOmlLDuedgTwwHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.1.tgz", + "integrity": "sha512-opjWJ4MevxeA8FhlngQWPBOvVWYNPFkq6/25rGgG+KOy0r8clYwL1CFd+PGwRqqMFVQ4/Qd3sQu5t7ucP7C/Uw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.1.tgz", + "integrity": "sha512-uBkwaI+gBUlIe+EfbNnY5xNyXuhZbDSx2nzzW8tRMjUmpScd6lCQYKY2V9BATHtv5Ef2OBq6SChEP8h+/cxifQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.1.tgz", + "integrity": "sha512-0bK9aG1kIg0Su7OcFTlexkVeNZ5IzEsnz1ept87a0TUgZ6HplSgkJAnFpEVRW7GRcikT4GlPV0pbtVedOaXHQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.1.tgz", + "integrity": "sha512-qB6AFRXuP8bdkBI4D7UPUbE7OQf7u5OL+R94JE42Z2Qjmyj74FtDdLGeriRyBDhm4rQSvqAGCGC01b8Fu2LthQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.1.tgz", + "integrity": "sha512-sHig3LaGlpNgDj5o8uPEoGs98RII8HpNIqFtAI8/pYABO8i0nb1QzT0JDoXF/pxzqO+FkxvwkHZo9k0NJYDedg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.1.tgz", + "integrity": "sha512-nD3YcUv6jBJbBNFvSbp0IV66+ba/1teuBcu+fBBPZ33sidxitc6ErhON3JNavaH8HlswhWMC3s5rgZpM4MtPqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.1.tgz", + "integrity": "sha512-7/XVZqgBby2qp/cO0TQ8uJK+9xnSdJ9ct6gSDdEr4MfABrjTyrW6Bau7HQ73a2a5tPB7hno49A0y1jhWGDN9OQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.1.tgz", + "integrity": "sha512-CYc64bnICG42UPL7TrhIwsJW4QcKkIt9gGlj21gq3VV0LL6XNb1yAdHVp1pIi9gkts9gGcT3OfUYHjGP7ETAiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.1.tgz", + "integrity": "sha512-LN+vnlZ9g0qlHGlS920GR4zFCqAwbv2lULrR29yGaWP9u7wF5L7GqWu9Ah6/kFZPXPUkpdZwd//TNR+9XC9hvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.1.tgz", + "integrity": "sha512-n+vkrSyphvmU0qkQ6QBNXCGr2mKjhP08mPRM/Xp5Ck2FV4NrHU+y6axzDeixUrCBHVUS51TZhjqrKBBsHLKb2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sindresorhus/slugify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.2.tgz", @@ -698,6 +869,12 @@ "integrity": "sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==", "dev": true }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -3201,6 +3378,38 @@ "rimraf": "bin.js" } }, + "node_modules/rollup": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.1.tgz", + "integrity": "sha512-ggqQKvx/PsB0FaWXhIvVkSWh7a/PCLQAsMjBc+nA2M8Rv2/HG0X6zvixAB7KyZBRtifBUhy5k8voQX/mRnABPg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.12.1", + "@rollup/rollup-android-arm64": "4.12.1", + "@rollup/rollup-darwin-arm64": "4.12.1", + "@rollup/rollup-darwin-x64": "4.12.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.1", + "@rollup/rollup-linux-arm64-gnu": "4.12.1", + "@rollup/rollup-linux-arm64-musl": "4.12.1", + "@rollup/rollup-linux-riscv64-gnu": "4.12.1", + "@rollup/rollup-linux-x64-gnu": "4.12.1", + "@rollup/rollup-linux-x64-musl": "4.12.1", + "@rollup/rollup-win32-arm64-msvc": "4.12.1", + "@rollup/rollup-win32-ia32-msvc": "4.12.1", + "@rollup/rollup-win32-x64-msvc": "4.12.1", + "fsevents": "~2.3.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3649,7 +3858,7 @@ } }, "packages/npm/goodrouter": { - "version": "2.1.4", + "version": "2.1.5", "license": "ISC", "dependencies": { "@types/node": "^20.11.25", @@ -3660,9 +3869,7 @@ "@types/benchmark": "^2.1.5", "benchmark": "^2.1.4", "itertools": "^2.2.5", - "microtime": "^3.1.1", - "prettier": "^3.2.5", - "typescript": "^5.4.2" + "microtime": "^3.1.1" } }, "packages/npm/www": { @@ -3671,6 +3878,7 @@ "@11ty/eleventy": "^2.0.1", "github-markdown-css": "^5.5.1", "prettier": "^3.2.5", + "rollup": "^4.12.1", "typescript": "^5.4.2" } } diff --git a/package.json b/package.json index 434af01..fd1705a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "private": true, + "packageManager": "npm@10.3.0", "workspaces": [ "packages/npm/*" ], @@ -9,6 +10,8 @@ }, "devDependencies": { "cspell": "^8.6.0", - "prettier": "^3.2.5" + "prettier": "^3.2.5", + "rollup": "^4.12.1", + "typescript": "^5.4.2" } } diff --git a/packages/net/Goodrouter.Bench/Goodrouter.Bench.csproj b/packages/net/Goodrouter.Bench/Goodrouter.Bench.csproj index f342bac..d9ee580 100644 --- a/packages/net/Goodrouter.Bench/Goodrouter.Bench.csproj +++ b/packages/net/Goodrouter.Bench/Goodrouter.Bench.csproj @@ -17,8 +17,8 @@ - + PreserveNewest - \ No newline at end of file + diff --git a/packages/net/Goodrouter.Bench/Program.cs b/packages/net/Goodrouter.Bench/Program.cs index 3478496..da59940 100644 --- a/packages/net/Goodrouter.Bench/Program.cs +++ b/packages/net/Goodrouter.Bench/Program.cs @@ -2,9 +2,9 @@ public class Program { - public static void Main(string[] args) - { - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - } + public static void Main(string[] args) + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } } diff --git a/packages/net/Goodrouter.Bench/RouterBench.cs b/packages/net/Goodrouter.Bench/RouterBench.cs index 74bce40..673ec98 100644 --- a/packages/net/Goodrouter.Bench/RouterBench.cs +++ b/packages/net/Goodrouter.Bench/RouterBench.cs @@ -4,88 +4,88 @@ public class RouterBenchSmall : RouterBenchBase { - public RouterBenchSmall() : base("small") - { - } + public RouterBenchSmall() : base("small") + { + } } public class RouterBenchDocker : RouterBenchBase { - public RouterBenchDocker() : base("docker") - { - } + public RouterBenchDocker() : base("docker") + { + } } public class RouterBenchGithub : RouterBenchBase { - public RouterBenchGithub() : base("github") - { - } + public RouterBenchGithub() : base("github") + { + } } public abstract class RouterBenchBase { - protected RouterBenchBase(string name) - { - templates = System.IO.File.ReadLines("" + name + ".txt"). - Where(line => line.Length > 0). - ToArray(); - - var allParameterNames = templates. - SelectMany( - template => TemplateUtility.ParseTemplateParts(template, parameterPlaceholderRE). - Where((part, index) => index % 2 != 0) - ). - ToHashSet(); - - allParameters = allParameterNames. - Select((name, index) => (name, "p" + index)). - ToDictionary(pair => pair.name, pair => pair.Item2); - - templateCount = templates.Count; - - router = new Router(); - foreach (var template in templates) - { - router.InsertRoute(template, template); - } - - paths = templates. - Select(template => - { - var path = router.StringifyRoute(template, allParameters); - return path; - } - ). - ToArray() as string[]; - } + protected RouterBenchBase(string name) + { + templates = System.IO.File.ReadLines("" + name + ".txt"). + Where(line => line.Length > 0). + ToArray(); - private Router router; - private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); - private int index = 0; - private readonly IList paths; - private readonly IList templates; - private readonly int templateCount; - private readonly IReadOnlyDictionary allParameters; + var allParameterNames = templates. + SelectMany( + template => TemplateUtility.ParseTemplateParts(template, parameterPlaceholderRE). + Where((part, index) => index % 2 != 0) + ). + ToHashSet(); - [Benchmark] - public void RouterParseBench() - { - var path = paths[index % templateCount]; - router.ParseRoute(path); + allParameters = allParameterNames. + Select((name, index) => (name, "p" + index)). + ToDictionary(pair => pair.name, pair => pair.Item2); - index++; - } + templateCount = templates.Count; - [Benchmark] - public void RouterStringifyBench() + router = new Router(); + foreach (var template in templates) { - var template = templates[index % templateCount]; - router.StringifyRoute(template, allParameters); - - index++; + router.InsertRoute(template, template); } + paths = templates. + Select(template => + { + var path = router.StringifyRoute(template, allParameters); + return path; + } + ). + ToArray() as string[]; + } + + private Router router; + private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); + private int index = 0; + private readonly IList paths; + private readonly IList templates; + private readonly int templateCount; + private readonly IReadOnlyDictionary allParameters; + + [Benchmark] + public void RouterParseBench() + { + var path = paths[index % templateCount]; + router.ParseRoute(path); + + index++; + } + + [Benchmark] + public void RouterStringifyBench() + { + var template = templates[index % templateCount]; + router.StringifyRoute(template, allParameters); + + index++; + } + } diff --git a/packages/net/Goodrouter.Spec/Goodrouter.Spec.csproj b/packages/net/Goodrouter.Spec/Goodrouter.Spec.csproj index 25413c0..33df965 100644 --- a/packages/net/Goodrouter.Spec/Goodrouter.Spec.csproj +++ b/packages/net/Goodrouter.Spec/Goodrouter.Spec.csproj @@ -24,9 +24,9 @@ all - + PreserveNewest - \ No newline at end of file + diff --git a/packages/net/Goodrouter.Spec/RouteNodeSpec.cs b/packages/net/Goodrouter.Spec/RouteNodeSpec.cs index dea1a98..1a1648e 100644 --- a/packages/net/Goodrouter.Spec/RouteNodeSpec.cs +++ b/packages/net/Goodrouter.Spec/RouteNodeSpec.cs @@ -3,18 +3,18 @@ public class RouteNodeSpec { - [Fact] - public void RouteNodeSortTest() - { - var routeNodes = new RouteNode[]{ + [Fact] + public void RouteNodeSortTest() + { + var routeNodes = new RouteNode[]{ new RouteNode("aa"), new RouteNode("xx"), new RouteNode("aa", true), new RouteNode("x") }; - var sortedRouteNodes = new SortedSet>(routeNodes).ToArray(); + var sortedRouteNodes = new SortedSet>(routeNodes).ToArray(); - Assert.Equal(routeNodes, sortedRouteNodes); - } + Assert.Equal(routeNodes, sortedRouteNodes); + } } diff --git a/packages/net/Goodrouter.Spec/RouterSpec.cs b/packages/net/Goodrouter.Spec/RouterSpec.cs index 9fba475..f1d6339 100644 --- a/packages/net/Goodrouter.Spec/RouterSpec.cs +++ b/packages/net/Goodrouter.Spec/RouterSpec.cs @@ -3,183 +3,183 @@ enum Route { - NotFound, - A, - B, - C, - D + NotFound, + A, + B, + C, + D } public class RouterSpec { - private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); + private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); + + [Fact] + public void RouterTest() + { + var router = new Router(); + + router.InsertRoute(Route.A, "/a"); + router.InsertRoute(Route.B, "/b/{x}"); + router.InsertRoute(Route.C, "/b/{x}/c"); + router.InsertRoute(Route.D, "/b/{x}/d"); - [Fact] - public void RouterTest() { - var router = new Router(); - - router.InsertRoute(Route.A, "/a"); - router.InsertRoute(Route.B, "/b/{x}"); - router.InsertRoute(Route.C, "/b/{x}/c"); - router.InsertRoute(Route.D, "/b/{x}/d"); - - { - var (routeKey, routeParameters) = router.ParseRoute("/not-found"); - Assert.Equal(Route.NotFound, routeKey); - Assert.Equal(new Dictionary() { }, routeParameters); - } - - { - var (routeKey, routeParameters) = router.ParseRoute("/a"); - Assert.Equal(Route.A, routeKey); - Assert.Equal(new Dictionary() { }, routeParameters); - } - - { - var (routeKey, routeParameters) = router.ParseRoute("/b/x"); - Assert.Equal(Route.B, routeKey); - Assert.Equal(new Dictionary() { { "x", "x" } }, routeParameters); - } - - { - var (routeKey, routeParameters) = router.ParseRoute("/b/y/c"); - Assert.Equal(Route.C, routeKey); - Assert.Equal(new Dictionary() { { "x", "y" } }, routeParameters); - } - - { - var (routeKey, routeParameters) = router.ParseRoute("/b/z/d"); - Assert.Equal(Route.D, routeKey); - Assert.Equal(new Dictionary() { { "x", "z" } }, routeParameters); - } + var (routeKey, routeParameters) = router.ParseRoute("/not-found"); + Assert.Equal(Route.NotFound, routeKey); + Assert.Equal(new Dictionary() { }, routeParameters); + } + { + var (routeKey, routeParameters) = router.ParseRoute("/a"); + Assert.Equal(Route.A, routeKey); + Assert.Equal(new Dictionary() { }, routeParameters); } - [Fact] - public void RouterTestReadme() { - var router = new Router(); - - router - .InsertRoute("all-products", "/product/all") - .InsertRoute("product-detail", "/product/{id}"); - - // And now we can parse routes! - - { - var (routeKey, routeParameters) = router.ParseRoute("/not-found"); - Assert.Null(routeKey); - Assert.Equal( - new Dictionary(), - routeParameters - ); - } - - { - var (routeKey, routeParameters) = router.ParseRoute("/product/all"); - Assert.Equal("all-products", routeKey); - Assert.Equal( - new Dictionary(), - routeParameters - ); - } - - { - var (routeKey, routeParameters) = router.ParseRoute("/product/1"); - Assert.Equal("product-detail", routeKey); - Assert.Equal( - new Dictionary() { - {"id", "1"} - }, - routeParameters - ); - } - - // And we can stringify routes - - { - var path = router.StringifyRoute("all-products"); - Assert.Equal("/product/all", path); - } - - { - var path = router.StringifyRoute( - "product-detail", - new Dictionary() { - {"id", "2"} - } - ); - Assert.Equal("/product/2", path); - } + var (routeKey, routeParameters) = router.ParseRoute("/b/x"); + Assert.Equal(Route.B, routeKey); + Assert.Equal(new Dictionary() { { "x", "x" } }, routeParameters); + } + { + var (routeKey, routeParameters) = router.ParseRoute("/b/y/c"); + Assert.Equal(Route.C, routeKey); + Assert.Equal(new Dictionary() { { "x", "y" } }, routeParameters); } - [Fact] - public void RouterTestSmall() { - RouterTestTemplates("small"); + var (routeKey, routeParameters) = router.ParseRoute("/b/z/d"); + Assert.Equal(Route.D, routeKey); + Assert.Equal(new Dictionary() { { "x", "z" } }, routeParameters); } - [Fact] - public void RouterTestDocker() + } + + [Fact] + public void RouterTestReadme() + { + var router = new Router(); + + router + .InsertRoute("all-products", "/product/all") + .InsertRoute("product-detail", "/product/{id}"); + + // And now we can parse routes! + { - RouterTestTemplates("docker"); + var (routeKey, routeParameters) = router.ParseRoute("/not-found"); + Assert.Null(routeKey); + Assert.Equal( + new Dictionary(), + routeParameters + ); } - [Fact] - public void RouterTestGithub() { - RouterTestTemplates("github"); + var (routeKey, routeParameters) = router.ParseRoute("/product/all"); + Assert.Equal("all-products", routeKey); + Assert.Equal( + new Dictionary(), + routeParameters + ); } - private void RouterTestTemplates(string name) { - var templates = System.IO.File.ReadLines("" + name + ".txt"). - Where(line => line.Length > 0). - ToArray(); - - var allParameterNames = templates. - SelectMany( - template => TemplateUtility.ParseTemplateParts(template, parameterPlaceholderRE). - Where((part, index) => index % 2 != 0) - ). - ToHashSet(); - - var allParameters = allParameterNames. - Select((name, index) => (name, "p" + index)). - ToDictionary(pair => pair.name, pair => pair.Item2); - - var templateCount = templates.Length; - - var router = new Router(); - foreach (var template in templates) - { - router.InsertRoute(template, template); - } - - var paths = templates. - Select(template => - { - var path = router.StringifyRoute(template, allParameters); - return path; - } - ). - ToArray() as string[]; - - for (var index = 0; index < templateCount; index++) - { - var path = paths[index]; - var template = templates[index]; - - var (routeKey, routeParameters) = router.ParseRoute(path); - - var expectedParameters = routeParameters.Keys. - Select(name => (name, allParameters[name])). - ToDictionary(pair => pair.name, pair => pair.Item2); - - Assert.Equal(template, routeKey); - Assert.Equal(expectedParameters, routeParameters); - } + var (routeKey, routeParameters) = router.ParseRoute("/product/1"); + Assert.Equal("product-detail", routeKey); + Assert.Equal( + new Dictionary() { + {"id", "1"} + }, + routeParameters + ); + } + + // And we can stringify routes + + { + var path = router.StringifyRoute("all-products"); + Assert.Equal("/product/all", path); + } + + { + var path = router.StringifyRoute( + "product-detail", + new Dictionary() { + {"id", "2"} + } + ); + Assert.Equal("/product/2", path); + } + + } + + [Fact] + public void RouterTestSmall() + { + RouterTestTemplates("small"); + } + + [Fact] + public void RouterTestDocker() + { + RouterTestTemplates("docker"); + } + + [Fact] + public void RouterTestGithub() + { + RouterTestTemplates("github"); + } + + private void RouterTestTemplates(string name) + { + var templates = System.IO.File.ReadLines("" + name + ".txt"). + Where(line => line.Length > 0). + ToArray(); + + var allParameterNames = templates. + SelectMany( + template => TemplateUtility.ParseTemplateParts(template, parameterPlaceholderRE). + Where((part, index) => index % 2 != 0) + ). + ToHashSet(); + + var allParameters = allParameterNames. + Select((name, index) => (name, "p" + index)). + ToDictionary(pair => pair.name, pair => pair.Item2); + + var templateCount = templates.Length; + + var router = new Router(); + foreach (var template in templates) + { + router.InsertRoute(template, template); + } + + var paths = templates. + Select(template => + { + var path = router.StringifyRoute(template, allParameters); + return path; + } + ). + ToArray() as string[]; + + for (var index = 0; index < templateCount; index++) + { + var path = paths[index]; + var template = templates[index]; + + var (routeKey, routeParameters) = router.ParseRoute(path); + + var expectedParameters = routeParameters.Keys. + Select(name => (name, allParameters[name])). + ToDictionary(pair => pair.name, pair => pair.Item2); + + Assert.Equal(template, routeKey); + Assert.Equal(expectedParameters, routeParameters); } + } } diff --git a/packages/net/Goodrouter.Spec/StringUtilitySpec.cs b/packages/net/Goodrouter.Spec/StringUtilitySpec.cs index c5da3d1..c2695d0 100644 --- a/packages/net/Goodrouter.Spec/StringUtilitySpec.cs +++ b/packages/net/Goodrouter.Spec/StringUtilitySpec.cs @@ -3,23 +3,23 @@ public class StringUtilitySpec { - [Fact] - public void FindCommonPrefixLengthTest() - { - Assert.Equal( - 2, - StringUtility.FindCommonPrefixLength("ab", "abc") - ); + [Fact] + public void FindCommonPrefixLengthTest() + { + Assert.Equal( + 2, + StringUtility.FindCommonPrefixLength("ab", "abc") + ); - Assert.Equal( - 3, - StringUtility.FindCommonPrefixLength("abc", "abc") - ); + Assert.Equal( + 3, + StringUtility.FindCommonPrefixLength("abc", "abc") + ); - Assert.Equal( - 0, - StringUtility.FindCommonPrefixLength("bc", "abc") - ); - } + Assert.Equal( + 0, + StringUtility.FindCommonPrefixLength("bc", "abc") + ); + } } diff --git a/packages/net/Goodrouter.Spec/TemplateUtilitySpec.cs b/packages/net/Goodrouter.Spec/TemplateUtilitySpec.cs index 9e82585..6e8b4db 100644 --- a/packages/net/Goodrouter.Spec/TemplateUtilitySpec.cs +++ b/packages/net/Goodrouter.Spec/TemplateUtilitySpec.cs @@ -3,69 +3,69 @@ public class TemplateUtilitySpec { - private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); + private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); - [Fact] - public void ParseTemplatePartsTest() + [Fact] + public void ParseTemplatePartsTest() + { { - { - var parts = TemplateUtility.ParseTemplateParts("/a/{b}/{c}", parameterPlaceholderRE).ToArray(); + var parts = TemplateUtility.ParseTemplateParts("/a/{b}/{c}", parameterPlaceholderRE).ToArray(); - Assert.Equal( - new string[] { "/a/", "b", "/", "c", "" }, - parts - ); - } - - { - var parts = TemplateUtility.ParseTemplateParts("/a/{b}/{c}/", parameterPlaceholderRE).ToArray(); + Assert.Equal( + new string[] { "/a/", "b", "/", "c", "" }, + parts + ); + } - Assert.Equal( - new string[] { "/a/", "b", "/", "c", "/" }, - parts - ); - } + { + var parts = TemplateUtility.ParseTemplateParts("/a/{b}/{c}/", parameterPlaceholderRE).ToArray(); - { - var parts = TemplateUtility.ParseTemplateParts("", parameterPlaceholderRE).ToArray(); + Assert.Equal( + new string[] { "/a/", "b", "/", "c", "/" }, + parts + ); + } - Assert.Equal( - new string[] { "" }, - parts - ); - } + { + var parts = TemplateUtility.ParseTemplateParts("", parameterPlaceholderRE).ToArray(); + Assert.Equal( + new string[] { "" }, + parts + ); } - [Fact] - public void ParseTemplatePairsTest() + } + + [Fact] + public void ParseTemplatePairsTest() + { { - { - var parts = TemplateUtility.ParseTemplatePairs("/a/{b}/{c}", parameterPlaceholderRE).ToArray(); + var parts = TemplateUtility.ParseTemplatePairs("/a/{b}/{c}", parameterPlaceholderRE).ToArray(); - Assert.Equal( - new (string, string?)[] { ("/a/", null), ("/", "b"), ("", "c") }, - parts - ); - } + Assert.Equal( + new (string, string?)[] { ("/a/", null), ("/", "b"), ("", "c") }, + parts + ); + } - { - var parts = TemplateUtility.ParseTemplatePairs("/a/{b}/{c}/", parameterPlaceholderRE).ToArray(); + { + var parts = TemplateUtility.ParseTemplatePairs("/a/{b}/{c}/", parameterPlaceholderRE).ToArray(); - Assert.Equal( - new (string, string?)[] { ("/a/", null), ("/", "b"), ("/", "c") }, - parts - ); - } + Assert.Equal( + new (string, string?)[] { ("/a/", null), ("/", "b"), ("/", "c") }, + parts + ); + } - { - var parts = TemplateUtility.ParseTemplatePairs("", parameterPlaceholderRE).ToArray(); + { + var parts = TemplateUtility.ParseTemplatePairs("", parameterPlaceholderRE).ToArray(); - Assert.Equal( - new (string, string?)[] { ("", null) }, - parts - ); - } + Assert.Equal( + new (string, string?)[] { ("", null) }, + parts + ); } + } } diff --git a/packages/net/Goodrouter/RouteNode.cs b/packages/net/Goodrouter/RouteNode.cs index 0662b87..570cf76 100644 --- a/packages/net/Goodrouter/RouteNode.cs +++ b/packages/net/Goodrouter/RouteNode.cs @@ -2,464 +2,464 @@ internal class RouteNode : IComparable>, IEquatable> where K : notnull { - public string Anchor { get; private set; } - public bool HasParameter { get; private set; } - public K? RouteKey { get; private set; } + public string Anchor { get; private set; } + public bool HasParameter { get; private set; } + public K? RouteKey { get; private set; } - public IList RouteParameterNames { get; private set; } + public IList RouteParameterNames { get; private set; } - private SortedSet> children = new SortedSet>(); - public IReadOnlySet> Children + private SortedSet> children = new SortedSet>(); + public IReadOnlySet> Children + { + get { - get - { - return this.children; - } + return this.children; } + } - private void AddChild(RouteNode node) + private void AddChild(RouteNode node) + { + if (node.parent != null) { - if (node.parent != null) - { - throw new ArgumentException("node already has a parent", "node"); - } - - node.parent = this; - this.children.Add(node); + throw new ArgumentException("node already has a parent", "node"); } - private void RemoveChild(RouteNode node) - { - node.parent = null; - this.children.Remove(node); - } + node.parent = this; + this.children.Add(node); + } + private void RemoveChild(RouteNode node) + { + node.parent = null; + this.children.Remove(node); + } - private RouteNode? parent; - public RouteNode? Parent - { - get - { - return this.parent; - } - } - - public RouteNode( - string anchor = "", - bool hasParameter = false, - K? routeKey = default(K?) - ) : this( - anchor, - hasParameter, - routeKey, - new string[] { } - ) + private RouteNode? parent; + public RouteNode? Parent + { + get { + return this.parent; } - public RouteNode( - string anchor, - bool hasParameter, - K? routeKey, - IList routeParameterNames - ) + } + + + public RouteNode( + string anchor = "", + bool hasParameter = false, + K? routeKey = default(K?) + ) : this( + anchor, + hasParameter, + routeKey, + new string[] { } + ) + { + } + public RouteNode( + string anchor, + bool hasParameter, + K? routeKey, + IList routeParameterNames + ) + { + this.Anchor = anchor; + this.HasParameter = hasParameter; + this.RouteKey = routeKey; + this.RouteParameterNames = routeParameterNames; + } + + public RouteNode Insert( + K routeKey, + string routeTemplate, + Regex parameterPlaceholderRE + ) + { + var pairs = + TemplateUtility.ParseTemplatePairs(routeTemplate, parameterPlaceholderRE).ToArray(); + + var routeParameterNames = pairs. + Select(pair => + { + var (anchor, parameterName) = pair; + return parameterName; + }). + Where(parameterName => parameterName != null). + ToArray() as string[]; + + var currentNode = this; + for (var index = 0; index < pairs.Length; index++) { - this.Anchor = anchor; - this.HasParameter = hasParameter; - this.RouteKey = routeKey; - this.RouteParameterNames = routeParameterNames; - } + var (anchor, parameterName) = pairs[index]; + var hasParameter = parameterName != null; - public RouteNode Insert( - K routeKey, - string routeTemplate, - Regex parameterPlaceholderRE - ) - { - var pairs = - TemplateUtility.ParseTemplatePairs(routeTemplate, parameterPlaceholderRE).ToArray(); - - var routeParameterNames = pairs. - Select(pair => - { - var (anchor, parameterName) = pair; - return parameterName; - }). - Where(parameterName => parameterName != null). - ToArray() as string[]; - - var currentNode = this; - for (var index = 0; index < pairs.Length; index++) - { - var (anchor, parameterName) = pairs[index]; - var hasParameter = parameterName != null; + var (commonPrefixLength, childNode) = + currentNode.FindSimilarChild(anchor, hasParameter); - var (commonPrefixLength, childNode) = - currentNode.FindSimilarChild(anchor, hasParameter); + currentNode = currentNode.Merge( + childNode, + anchor, + hasParameter, + index == pairs.Length - 1 ? routeKey : default(K?), + routeParameterNames, + commonPrefixLength + ); - currentNode = currentNode.Merge( - childNode, - anchor, - hasParameter, - index == pairs.Length - 1 ? routeKey : default(K?), - routeParameterNames, - commonPrefixLength - ); + } - } + return currentNode; + } - return currentNode; - } + public (K?, IList, IList) Parse( + string path, + int maximumParameterValueLength + ) + { + var parameterValues = new List(); - public (K?, IList, IList) Parse( - string path, - int maximumParameterValueLength - ) + if (this.HasParameter) { - var parameterValues = new List(); - - if (this.HasParameter) - { - if (path.Length == 0) - { - return (default(K?), new string[] { }, new string[] { }); - } - - var index = this.Anchor.Length == 0 ? - path.Length : - path.Substring(0, Math.Min( - maximumParameterValueLength + this.Anchor.Length, - path.Length - )). - IndexOf(this.Anchor); - - if (index < 0) - { - return (default(K?), new string[] { }, new string[] { }); - } - - var value = path.Substring(0, index); - - path = path.Substring(index + this.Anchor.Length); - - parameterValues.Add(value); - } - else - { - if (!path.StartsWith(this.Anchor)) - { - return (default(K?), new string[] { }, new string[] { }); - } + if (path.Length == 0) + { + return (default(K?), new string[] { }, new string[] { }); + } + + var index = this.Anchor.Length == 0 ? + path.Length : + path.Substring(0, Math.Min( + maximumParameterValueLength + this.Anchor.Length, + path.Length + )). + IndexOf(this.Anchor); + + if (index < 0) + { + return (default(K?), new string[] { }, new string[] { }); + } - path = path.Substring(this.Anchor.Length); - } + var value = path.Substring(0, index); - foreach (var childNode in this.children) - { - var (childRouteKey, childRouteParameterNames, childParameterValues) = childNode.Parse( - path, - maximumParameterValueLength - ); - - if (!Object.Equals(childRouteKey, default(K?))) - { - return ( - childRouteKey, - childRouteParameterNames, - parameterValues.Concat(childParameterValues).ToArray() - ); - } - } - - if (!Object.Equals(this.RouteKey, default(K?)) && path.Length == 0) - { - return ( - this.RouteKey, - this.RouteParameterNames, - parameterValues - ); - } + path = path.Substring(index + this.Anchor.Length); + parameterValues.Add(value); + } + else + { + if (!path.StartsWith(this.Anchor)) + { return (default(K?), new string[] { }, new string[] { }); + } + + path = path.Substring(this.Anchor.Length); } - public string Stringify( - IList parameterValues - ) + foreach (var childNode in this.children) { - var parameterIndex = parameterValues.Count; - var path = ""; - var currentNode = this; - while (currentNode != null) - { - path = currentNode.Anchor + path; - if (currentNode.HasParameter) - { - parameterIndex--; - var value = parameterValues[parameterIndex]; - path = value + path; - } - currentNode = currentNode.Parent; - } - return path; + var (childRouteKey, childRouteParameterNames, childParameterValues) = childNode.Parse( + path, + maximumParameterValueLength + ); + + if (!Object.Equals(childRouteKey, default(K?))) + { + return ( + childRouteKey, + childRouteParameterNames, + parameterValues.Concat(childParameterValues).ToArray() + ); + } } - - private RouteNode Merge( - RouteNode? childNode, - string anchor, - bool hasParameter, - K? routeKey, - IList routeParameterNames, - int commmonPrefixLength - ) + if (!Object.Equals(this.RouteKey, default(K?)) && path.Length == 0) { - if (childNode == null) - { - return this.MergeNew( - anchor, - hasParameter, - routeKey, - routeParameterNames - ); - } - - var commonPrefix = childNode.Anchor.Substring(0, commmonPrefixLength); - - if (childNode.Anchor == anchor) - { - return this.MergeJoin( - childNode, - routeKey, - routeParameterNames - ); - } - else if (childNode.Anchor == commonPrefix) - { - return this.MergeAddToChild( - childNode, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commmonPrefixLength - ); - } - else if (anchor == commonPrefix) - { - return this.MergeAddToNew( - childNode, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commmonPrefixLength - ); - } - else - { - return this.MergeIntermediate( - childNode, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commmonPrefixLength - ); - } + return ( + this.RouteKey, + this.RouteParameterNames, + parameterValues + ); } - private RouteNode MergeNew( - string anchor, - bool hasParameter, - K? routeKey, - IList routeParameterNames - ) + return (default(K?), new string[] { }, new string[] { }); + } + + public string Stringify( + IList parameterValues + ) + { + var parameterIndex = parameterValues.Count; + var path = ""; + var currentNode = this; + while (currentNode != null) { - var newNode = new RouteNode( - anchor, - hasParameter, - routeKey, - routeParameterNames - ); - this.AddChild(newNode); - return newNode; + path = currentNode.Anchor + path; + if (currentNode.HasParameter) + { + parameterIndex--; + var value = parameterValues[parameterIndex]; + path = value + path; + } + currentNode = currentNode.Parent; } - - private RouteNode MergeJoin( - RouteNode childNode, - K? routeKey, - IList routeParameterNames - ) + return path; + } + + + private RouteNode Merge( + RouteNode? childNode, + string anchor, + bool hasParameter, + K? routeKey, + IList routeParameterNames, + int commmonPrefixLength + ) + { + if (childNode == null) { - if ( - !Object.Equals(childNode.RouteKey, default(K?)) && - !Object.Equals(routeKey, default(K?)) - ) - { - throw new Exception("ambiguous route"); - } + return this.MergeNew( + anchor, + hasParameter, + routeKey, + routeParameterNames + ); + } - if (Object.Equals(childNode.RouteKey, default(K?))) - { - childNode.RouteKey = routeKey; - childNode.RouteParameterNames = routeParameterNames; - } + var commonPrefix = childNode.Anchor.Substring(0, commmonPrefixLength); - return childNode; + if (childNode.Anchor == anchor) + { + return this.MergeJoin( + childNode, + routeKey, + routeParameterNames + ); } - - private RouteNode MergeIntermediate( - RouteNode childNode, - string anchor, - bool hasParameter, - K? routeKey, - IList routeParameterNames, - int commmonPrefixLength - ) + else if (childNode.Anchor == commonPrefix) { - this.RemoveChild(childNode); - - var newNode = new RouteNode( - anchor.Substring(commmonPrefixLength), - false, - routeKey, - routeParameterNames - ); - - childNode.Anchor = childNode.Anchor.Substring(commmonPrefixLength); - childNode.HasParameter = false; - - var intermediateNode = new RouteNode( - anchor.Substring(0, commmonPrefixLength), - hasParameter - ); - intermediateNode.AddChild(childNode); - intermediateNode.AddChild(newNode); - - this.AddChild(intermediateNode); - - return newNode; + return this.MergeAddToChild( + childNode, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commmonPrefixLength + ); } - - private RouteNode MergeAddToChild( - RouteNode childNode, - string anchor, - bool hasParameter, - K? routeKey, - IList routeParameterNames, - int commmonPrefixLength + else if (anchor == commonPrefix) + { + return this.MergeAddToNew( + childNode, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commmonPrefixLength + ); + } + else + { + return this.MergeIntermediate( + childNode, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commmonPrefixLength + ); + } + } + + private RouteNode MergeNew( + string anchor, + bool hasParameter, + K? routeKey, + IList routeParameterNames + ) + { + var newNode = new RouteNode( + anchor, + hasParameter, + routeKey, + routeParameterNames + ); + this.AddChild(newNode); + return newNode; + } + + private RouteNode MergeJoin( + RouteNode childNode, + K? routeKey, + IList routeParameterNames + ) + { + if ( + !Object.Equals(childNode.RouteKey, default(K?)) && + !Object.Equals(routeKey, default(K?)) ) { - anchor = anchor.Substring(commmonPrefixLength); - hasParameter = false; - - var (commonPrefixLength2, childNode2) = - childNode.FindSimilarChild(anchor, hasParameter); - - return childNode.Merge( - childNode2, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commonPrefixLength2 - ); + throw new Exception("ambiguous route"); } - private RouteNode MergeAddToNew( - RouteNode childNode, - string anchor, - bool hasParameter, - K? routeKey, - IList routeParameterNames, - int commmonPrefixLength - ) + if (Object.Equals(childNode.RouteKey, default(K?))) { - var newNode = new RouteNode( - anchor, - hasParameter, - routeKey, - routeParameterNames - ); - this.AddChild(newNode); + childNode.RouteKey = routeKey; + childNode.RouteParameterNames = routeParameterNames; + } - this.RemoveChild(childNode); + return childNode; + } + + private RouteNode MergeIntermediate( + RouteNode childNode, + string anchor, + bool hasParameter, + K? routeKey, + IList routeParameterNames, + int commmonPrefixLength + ) + { + this.RemoveChild(childNode); + + var newNode = new RouteNode( + anchor.Substring(commmonPrefixLength), + false, + routeKey, + routeParameterNames + ); + + childNode.Anchor = childNode.Anchor.Substring(commmonPrefixLength); + childNode.HasParameter = false; + + var intermediateNode = new RouteNode( + anchor.Substring(0, commmonPrefixLength), + hasParameter + ); + intermediateNode.AddChild(childNode); + intermediateNode.AddChild(newNode); + + this.AddChild(intermediateNode); + + return newNode; + } + + private RouteNode MergeAddToChild( + RouteNode childNode, + string anchor, + bool hasParameter, + K? routeKey, + IList routeParameterNames, + int commmonPrefixLength + ) + { + anchor = anchor.Substring(commmonPrefixLength); + hasParameter = false; + + var (commonPrefixLength2, childNode2) = + childNode.FindSimilarChild(anchor, hasParameter); + + return childNode.Merge( + childNode2, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commonPrefixLength2 + ); + } + + private RouteNode MergeAddToNew( + RouteNode childNode, + string anchor, + bool hasParameter, + K? routeKey, + IList routeParameterNames, + int commmonPrefixLength + ) + { + var newNode = new RouteNode( + anchor, + hasParameter, + routeKey, + routeParameterNames + ); + this.AddChild(newNode); - childNode.Anchor = childNode.Anchor.Substring(commmonPrefixLength); - childNode.HasParameter = false; + this.RemoveChild(childNode); - newNode.AddChild(childNode); + childNode.Anchor = childNode.Anchor.Substring(commmonPrefixLength); + childNode.HasParameter = false; - return newNode; - } + newNode.AddChild(childNode); - private (int, RouteNode?) FindSimilarChild( - string anchor, - bool hasParameter - ) - { - foreach (var childNode in this.children) - { - if (childNode.HasParameter != hasParameter) - { - continue; - } + return newNode; + } - var commonPrefixLength = StringUtility.FindCommonPrefixLength(anchor, childNode.Anchor); - if (commonPrefixLength == 0) - { - continue; - } + private (int, RouteNode?) FindSimilarChild( + string anchor, + bool hasParameter + ) + { + foreach (var childNode in this.children) + { + if (childNode.HasParameter != hasParameter) + { + continue; + } + + var commonPrefixLength = StringUtility.FindCommonPrefixLength(anchor, childNode.Anchor); + if (commonPrefixLength == 0) + { + continue; + } + + return (commonPrefixLength, childNode); + } - return (commonPrefixLength, childNode); - } + return (0, null); + } - return (0, null); + public int CompareTo(RouteNode? other) + { + if (other == null) + { + return 1; } - public int CompareTo(RouteNode? other) { - if (other == null) - { - return 1; - } - - { - var compared = this.Anchor.Length.CompareTo(other.Anchor.Length); - if (compared != 0) - { - return 0 - compared; - } - } - - { - var compared = this.HasParameter.CompareTo(other.HasParameter); - if (compared != 0) - { - return compared; - } - } - - { - var compared = this.Anchor.CompareTo(other.Anchor); - if (compared != 0) - { - return compared; - } - } - - return 0; + var compared = this.Anchor.Length.CompareTo(other.Anchor.Length); + if (compared != 0) + { + return 0 - compared; + } } - public bool Equals(RouteNode? other) { - if (other == null) return false; + var compared = this.HasParameter.CompareTo(other.HasParameter); + if (compared != 0) + { + return compared; + } + } - return this.Anchor == other.Anchor && - this.HasParameter == other.HasParameter && - Object.Equals(this.RouteKey, other.RouteKey); + { + var compared = this.Anchor.CompareTo(other.Anchor); + if (compared != 0) + { + return compared; + } } + return 0; + } + + public bool Equals(RouteNode? other) + { + if (other == null) return false; + + return this.Anchor == other.Anchor && + this.HasParameter == other.HasParameter && + Object.Equals(this.RouteKey, other.RouteKey); + } + } diff --git a/packages/net/Goodrouter/Router.cs b/packages/net/Goodrouter/Router.cs index 0c0d415..fd63368 100644 --- a/packages/net/Goodrouter/Router.cs +++ b/packages/net/Goodrouter/Router.cs @@ -5,178 +5,178 @@ /// public class Router where K : notnull { - private RouteNode rootNode = new RouteNode(); - private readonly Dictionary> leafNodes = new Dictionary>(); - - private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); - private int maximumParameterValueLength = 20; - - private Func parameterValueEncoder = - (string decodedValue) => Uri.EscapeDataString(decodedValue); - - private Func parameterValueDecoder = - (string encodedValue) => Uri.UnescapeDataString(encodedValue); - - /// - /// Set the maximum length of an encoded parameter value - /// - /// - /// The maximum length - /// - /// - /// Router object, so you can chain! - /// - public Router SetMaximumParameterValueLength(int value) + private RouteNode rootNode = new RouteNode(); + private readonly Dictionary> leafNodes = new Dictionary>(); + + private Regex parameterPlaceholderRE = new Regex("\\{(.*?)\\}"); + private int maximumParameterValueLength = 20; + + private Func parameterValueEncoder = + (string decodedValue) => Uri.EscapeDataString(decodedValue); + + private Func parameterValueDecoder = + (string encodedValue) => Uri.UnescapeDataString(encodedValue); + + /// + /// Set the maximum length of an encoded parameter value + /// + /// + /// The maximum length + /// + /// + /// Router object, so you can chain! + /// + public Router SetMaximumParameterValueLength(int value) + { + maximumParameterValueLength = value; + return this; + } + + /// + /// Set the regular expression that will be used to parse placeholders from a route template + /// + /// + /// The regular expression + /// + /// + /// Router object, so you can chain! + /// + public Router SetParameterPlaceholderRE(Regex value) + { + parameterPlaceholderRE = value; + return this; + } + + /// + /// Sets the function that will be used to encode parameter values + /// + /// + /// Function to use + /// + /// + /// Router object, so you can chain! + /// + public Router SetParameterValueEncoder(Func value) + { + parameterValueEncoder = value; + return this; + } + + /// + /// Sets the function that will be used when decoding parameter values + /// + /// + /// Function to use + /// + /// + /// Router object, so you can chain! + /// + public Router SetParameterValueDecoder(Func value) + { + parameterValueDecoder = value; + return this; + } + + /// + /// Adds a new route + /// + /// + /// name of the route + /// + /// + /// template for the route, als defines parameters + /// + public Router InsertRoute( + K routeKey, + string routeTemplate + ) + { + var leafNode = this.rootNode.Insert( + routeKey, + routeTemplate, + this.parameterPlaceholderRE + ); + this.leafNodes.Add(routeKey, leafNode); + return this; + } + + /// + /// Match the path against one of the provided routes and parse the parameters in it + /// + /// + /// path to match + /// + /// + /// route that is matches to the path or null if no match is found + /// + public (K?, IReadOnlyDictionary) ParseRoute( + string path + ) + { + var parameters = new Dictionary(); + + var (routeKey, parameterNames, parameterValues) = this.rootNode.Parse( + path, + this.maximumParameterValueLength + ); + + for (var index = 0; index < parameterNames.Count; index++) { - maximumParameterValueLength = value; - return this; + var parameterName = parameterNames[index]; + var parameterValue = parameterValues[index]; + parameters[parameterName] = parameterValueDecoder(parameterValue); } - /// - /// Set the regular expression that will be used to parse placeholders from a route template - /// - /// - /// The regular expression - /// - /// - /// Router object, so you can chain! - /// - public Router SetParameterPlaceholderRE(Regex value) + return ( + routeKey, + parameters + ); + } + + /// + /// Convert a route to a path string. + /// + /// + /// route to stringify + /// + /// + /// string representing the route or null if the route is not found by name + /// + public string? StringifyRoute( + K routeKey + ) + { + return StringifyRoute(routeKey, new Dictionary()); + } + /// + /// Convert a route to a path string. + /// + /// + /// route to stringify + /// + /// + /// parameters for the route + /// + /// + /// string representing the route or null if the route is not found by name + /// + public string? StringifyRoute( + K routeKey, + IReadOnlyDictionary routeParameters + ) + { + var node = this.leafNodes[routeKey]; + if (node == null) return null; + + var parameterValues = new List(); + foreach (var parameterName in node.RouteParameterNames) { - parameterPlaceholderRE = value; - return this; + var parameterValue = routeParameters[parameterName]; + parameterValues.Add(parameterValueEncoder(parameterValue)); } - /// - /// Sets the function that will be used to encode parameter values - /// - /// - /// Function to use - /// - /// - /// Router object, so you can chain! - /// - public Router SetParameterValueEncoder(Func value) - { - parameterValueEncoder = value; - return this; - } - - /// - /// Sets the function that will be used when decoding parameter values - /// - /// - /// Function to use - /// - /// - /// Router object, so you can chain! - /// - public Router SetParameterValueDecoder(Func value) - { - parameterValueDecoder = value; - return this; - } - - /// - /// Adds a new route - /// - /// - /// name of the route - /// - /// - /// template for the route, als defines parameters - /// - public Router InsertRoute( - K routeKey, - string routeTemplate - ) - { - var leafNode = this.rootNode.Insert( - routeKey, - routeTemplate, - this.parameterPlaceholderRE - ); - this.leafNodes.Add(routeKey, leafNode); - return this; - } - - /// - /// Match the path against one of the provided routes and parse the parameters in it - /// - /// - /// path to match - /// - /// - /// route that is matches to the path or null if no match is found - /// - public (K?, IReadOnlyDictionary) ParseRoute( - string path - ) - { - var parameters = new Dictionary(); - - var (routeKey, parameterNames, parameterValues) = this.rootNode.Parse( - path, - this.maximumParameterValueLength - ); - - for (var index = 0; index < parameterNames.Count; index++) - { - var parameterName = parameterNames[index]; - var parameterValue = parameterValues[index]; - parameters[parameterName] = parameterValueDecoder(parameterValue); - } - - return ( - routeKey, - parameters - ); - } - - /// - /// Convert a route to a path string. - /// - /// - /// route to stringify - /// - /// - /// string representing the route or null if the route is not found by name - /// - public string? StringifyRoute( - K routeKey - ) - { - return StringifyRoute(routeKey, new Dictionary()); - } - /// - /// Convert a route to a path string. - /// - /// - /// route to stringify - /// - /// - /// parameters for the route - /// - /// - /// string representing the route or null if the route is not found by name - /// - public string? StringifyRoute( - K routeKey, - IReadOnlyDictionary routeParameters - ) - { - var node = this.leafNodes[routeKey]; - if (node == null) return null; - - var parameterValues = new List(); - foreach (var parameterName in node.RouteParameterNames) - { - var parameterValue = routeParameters[parameterName]; - parameterValues.Add(parameterValueEncoder(parameterValue)); - } - - return node.Stringify(parameterValues); - } + return node.Stringify(parameterValues); + } } diff --git a/packages/net/Goodrouter/StringUtility.cs b/packages/net/Goodrouter/StringUtility.cs index af954c5..e4d19b0 100644 --- a/packages/net/Goodrouter/StringUtility.cs +++ b/packages/net/Goodrouter/StringUtility.cs @@ -1,22 +1,22 @@ internal static class StringUtility { - public static int FindCommonPrefixLength( - string stringLeft, - string stringRight - ) + public static int FindCommonPrefixLength( + string stringLeft, + string stringRight + ) + { + var length = Math.Min(stringLeft.Length, stringRight.Length); + int index; + for (index = 0; index < length; index++) { - var length = Math.Min(stringLeft.Length, stringRight.Length); - int index; - for (index = 0; index < length; index++) - { - var charLeft = stringLeft[index]; - var charRight = stringRight[index]; - if (charLeft != charRight) - { - break; - } - } - return index; + var charLeft = stringLeft[index]; + var charRight = stringRight[index]; + if (charLeft != charRight) + { + break; + } } + return index; + } } diff --git a/packages/net/Goodrouter/TemplateUtility.cs b/packages/net/Goodrouter/TemplateUtility.cs index fb06508..00a0cca 100644 --- a/packages/net/Goodrouter/TemplateUtility.cs +++ b/packages/net/Goodrouter/TemplateUtility.cs @@ -3,45 +3,45 @@ internal static class TemplateUtility { - public static IEnumerable ParseTemplateParts( - string routeTemplate, - Regex parameterPlaceholderRE - ) + public static IEnumerable ParseTemplateParts( + string routeTemplate, + Regex parameterPlaceholderRE + ) + { + var offsetIndex = 0; + foreach (Match match in parameterPlaceholderRE.Matches(routeTemplate)) { - var offsetIndex = 0; - foreach (Match match in parameterPlaceholderRE.Matches(routeTemplate)) - { - yield return routeTemplate.Substring( - offsetIndex, - match.Index - offsetIndex - ); - yield return match.Groups[1].Value; - offsetIndex = match.Index + match.Length; - } - yield return routeTemplate.Substring(offsetIndex); + yield return routeTemplate.Substring( + offsetIndex, + match.Index - offsetIndex + ); + yield return match.Groups[1].Value; + offsetIndex = match.Index + match.Length; } + yield return routeTemplate.Substring(offsetIndex); + } - public static IEnumerable<(string, string?)> ParseTemplatePairs( - string routeTemplate, - Regex parameterPlaceholderRE - ) - { - var parts = ParseTemplateParts(routeTemplate, parameterPlaceholderRE); - - var index = 0; - string? parameter = null; - foreach (var part in parts) - { - if (index % 2 == 0) - { - yield return (part, parameter); - } - else - { - parameter = part; - } - index++; - } + public static IEnumerable<(string, string?)> ParseTemplatePairs( + string routeTemplate, + Regex parameterPlaceholderRE + ) + { + var parts = ParseTemplateParts(routeTemplate, parameterPlaceholderRE); + var index = 0; + string? parameter = null; + foreach (var part in parts) + { + if (index % 2 == 0) + { + yield return (part, parameter); + } + else + { + parameter = part; + } + index++; } + + } } diff --git a/packages/npm/goodrouter/.vscode/launch.json b/packages/npm/goodrouter/.vscode/launch.json index 6b8b913..2706bb9 100644 --- a/packages/npm/goodrouter/.vscode/launch.json +++ b/packages/npm/goodrouter/.vscode/launch.json @@ -9,7 +9,7 @@ "args": [], "cwd": "${workspaceFolder}", "sourceMaps": true, - "outFiles": ["${workspaceFolder}/out/**/*.js"], + "outFiles": ["${workspaceFolder}/transpiled/**/*.js"], "preLaunchTask": "build", "outputCapture": "std" } diff --git a/packages/npm/goodrouter/package.json b/packages/npm/goodrouter/package.json index 188a1ef..5104ec6 100644 --- a/packages/npm/goodrouter/package.json +++ b/packages/npm/goodrouter/package.json @@ -1,29 +1,28 @@ { "name": "goodrouter", - "version": "2.1.4", + "version": "2.1.5", "description": "a good router", "type": "module", - "main": "./out-commonjs/main.js", - "module": "./out/main.js", - "types": "./out/main.d.ts", + "main": "./bundled/main.cjs", + "module": "./bundled/main.js", + "types": "./types/main.d.ts", "exports": { ".": { - "require": "./out-commonjs/main.js", - "import": "./out/main.js", - "types": "./out/main.d.ts", - "browser": "./out/browser.js" + "require": "./bundled/main.cjs", + "import": "./bundled/main.js", + "types": "./types/main.d.ts" } }, "files": [ - "./out/**", - "./out-commonjs/**" + "./types/**", + "./bundled/**" ], "scripts": { - "prepack": "tsc --composite false; tsc --composite false --outDir out-commonjs --declaration false --module commonjs --moduleResolution Node10 ; echo {\\\"type\\\":\\\"commonjs\\\"}> out-commonjs/package.json", - "pretest": "tsc --build", - "build": "tsc --build", - "clean": "rm -rf out out-* ; tsc --build --clean", - "test": "node --test ./out/**/*.test.js" + "prepack": "node ./scripts/build.js", + "pretest": "node ./scripts/build.js", + "build": "node ./scripts/build.js", + "clean": "node ./scripts/clean.js", + "test": "node --test ./transpiled/**/*.test.js" }, "repository": { "type": "git", @@ -46,9 +45,7 @@ "@types/benchmark": "^2.1.5", "benchmark": "^2.1.4", "itertools": "^2.2.5", - "microtime": "^3.1.1", - "prettier": "^3.2.5", - "typescript": "^5.4.2" + "microtime": "^3.1.1" }, "dependencies": { "@types/node": "^20.11.25", diff --git a/packages/npm/goodrouter/scripts/build.js b/packages/npm/goodrouter/scripts/build.js new file mode 100755 index 0000000..f5a4012 --- /dev/null +++ b/packages/npm/goodrouter/scripts/build.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +import cp from "child_process"; +import path from "path"; + +cp.execFileSync("tsc", [], { shell: true }); + +cp.execFileSync( + "rollup", + [ + "--input", + path.resolve("transpiled", "main.js"), + "--file", + path.resolve("bundled", "main.js"), + "--sourcemap", + "--format", + "es", + ], + { shell: true }, +); + +cp.execFileSync( + "rollup", + [ + "--input", + path.resolve("transpiled", "main.js"), + "--file", + path.resolve("bundled", "main.cjs"), + "--sourcemap", + "--format", + "cjs", + ], + { shell: true }, +); diff --git a/packages/npm/goodrouter/scripts/clean.js b/packages/npm/goodrouter/scripts/clean.js new file mode 100755 index 0000000..d4cb073 --- /dev/null +++ b/packages/npm/goodrouter/scripts/clean.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; + +fs.rmSync(path.resolve("transpiled"), { recursive: true, force: true }); +fs.rmSync(path.resolve("types"), { recursive: true, force: true }); +fs.rmSync(path.resolve("bundled"), { recursive: true, force: true }); diff --git a/packages/npm/goodrouter/src/json.ts b/packages/npm/goodrouter/src/json.ts index 8e4d9db..9456d39 100644 --- a/packages/npm/goodrouter/src/json.ts +++ b/packages/npm/goodrouter/src/json.ts @@ -1,13 +1,11 @@ export interface RouteNodeJson { - anchor: string; - hasParameter: boolean; - routeKey: K | null; - children: RouteNodeJson[]; + anchor: string; + hasParameter: boolean; + routeKey: K | null; + children: RouteNodeJson[]; } export interface RouterJson { - rootNode?: RouteNodeJson; - templatePairs?: Array< - readonly [K, Array] - >; + rootNode?: RouteNodeJson; + templatePairs?: Array]>; } diff --git a/packages/npm/goodrouter/src/root.ts b/packages/npm/goodrouter/src/root.ts new file mode 100644 index 0000000..5b258dc --- /dev/null +++ b/packages/npm/goodrouter/src/root.ts @@ -0,0 +1,8 @@ +import path from "path"; + +export const projectRoot = getProjectRoot(); + +function getProjectRoot() { + const dirname = import.meta.dirname ?? __dirname; + return path.resolve(dirname, ".."); +} diff --git a/packages/npm/goodrouter/src/route-node.spec.ts b/packages/npm/goodrouter/src/route-node.spec.ts deleted file mode 100644 index 7117d12..0000000 --- a/packages/npm/goodrouter/src/route-node.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { permutations } from "itertools"; -import assert from "node:assert/strict"; -import test from "node:test"; -import { RouteNode } from "./route-node.js"; -import { defaultRouterOptions } from "./router-options.js"; -import { parseTemplatePairs } from "./template.js"; - -test("route-node-permutations", () => { - const routeConfigs = [ - "/a", - "/b/{x}", - "/b/{x}/", - "/b/{x}/c", - "/b/{x}/d", - "/b/e/{x}/f", - ]; - - const permutedRouteConfigs = permutations( - routeConfigs, - routeConfigs.length, - ); - - let rootNodePrevious: RouteNode | null = null; - - for (const routeConfigs of permutedRouteConfigs) { - const rootNode = new RouteNode(); - - for (const template of routeConfigs) { - const templatePairs = [ - ...parseTemplatePairs( - template, - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - rootNode.insert(template, templatePairs); - } - - { - assert.equal(rootNode.countChildren(), 1); - } - - if (rootNodePrevious != null) { - assert.deepEqual(rootNode, rootNodePrevious); - } - - rootNodePrevious = rootNode; - } -}); - -test("route-node-sort", () => { - const nodes: RouteNode[] = [ - new RouteNode("aa"), - new RouteNode("xx"), - new RouteNode("aa", true), - new RouteNode("x"), - ]; - - const nodesExpected = [...nodes]; - const nodesActual = [...nodes].sort((a, b) => a.compare(b)); - - assert.deepEqual(nodesActual, nodesExpected); -}); diff --git a/packages/npm/goodrouter/src/route-node.test.ts b/packages/npm/goodrouter/src/route-node.test.ts new file mode 100644 index 0000000..dfb6a5a --- /dev/null +++ b/packages/npm/goodrouter/src/route-node.test.ts @@ -0,0 +1,49 @@ +import { permutations } from "itertools"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { RouteNode } from "./route-node.js"; +import { defaultRouterOptions } from "./router-options.js"; +import { parseTemplatePairs } from "./template.js"; + +test("route-node-permutations", () => { + const routeConfigs = ["/a", "/b/{x}", "/b/{x}/", "/b/{x}/c", "/b/{x}/d", "/b/e/{x}/f"]; + + const permutedRouteConfigs = permutations(routeConfigs, routeConfigs.length); + + let rootNodePrevious: RouteNode | null = null; + + for (const routeConfigs of permutedRouteConfigs) { + const rootNode = new RouteNode(); + + for (const template of routeConfigs) { + const templatePairs = [ + ...parseTemplatePairs(template, defaultRouterOptions.parameterPlaceholderRE), + ]; + rootNode.insert(template, templatePairs); + } + + { + assert.equal(rootNode.countChildren(), 1); + } + + if (rootNodePrevious != null) { + assert.deepEqual(rootNode, rootNodePrevious); + } + + rootNodePrevious = rootNode; + } +}); + +test("route-node-sort", () => { + const nodes: RouteNode[] = [ + new RouteNode("aa"), + new RouteNode("xx"), + new RouteNode("aa", true), + new RouteNode("x"), + ]; + + const nodesExpected = [...nodes]; + const nodesActual = [...nodes].sort((a, b) => a.compare(b)); + + assert.deepEqual(nodesActual, nodesExpected); +}); diff --git a/packages/npm/goodrouter/src/route-node.ts b/packages/npm/goodrouter/src/route-node.ts index b9d0c92..86bf0c3 100644 --- a/packages/npm/goodrouter/src/route-node.ts +++ b/packages/npm/goodrouter/src/route-node.ts @@ -7,342 +7,313 @@ import { findCommonPrefixLength } from "./utils/string.js"; * for the routes */ export class RouteNode { - constructor( - /** - * @description - * suffix that comes after the parameter value (if any!) of the path - */ - public anchor = "", - /** - * @description - * does this node have a parameter value - */ - public hasParameter = false, - /** - * @description - * key that identifies the route, if this is a leaf node for the route - */ - public routeKey: K | null = null, - /** - * @description - * children that represent the rest of the path that needs to be matched - */ - private readonly children = new Array>(), - ) {} - - private addChild(childNode: RouteNode) { - this.children.push(childNode); + constructor( + /** + * @description + * suffix that comes after the parameter value (if any!) of the path + */ + public anchor = "", + /** + * @description + * does this node have a parameter value + */ + public hasParameter = false, + /** + * @description + * key that identifies the route, if this is a leaf node for the route + */ + public routeKey: K | null = null, + /** + * @description + * children that represent the rest of the path that needs to be matched + */ + private readonly children = new Array>(), + ) {} + + private addChild(childNode: RouteNode) { + this.children.push(childNode); + } + + private removeChild(childNode: RouteNode) { + const childIndex = this.children.indexOf(childNode); + this.children.splice(childIndex, 1); + } + + countChildren() { + return this.children.length; + } + + insert(routeKey: K, templatePairs: Array) { + const routeParameterNames = templatePairs + .map(([, parameterName]) => parameterName) + .filter((parameterName) => parameterName) as string[]; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + let currentNode: RouteNode = this; + for (let index = 0; index < templatePairs.length; index++) { + const [anchor, parameterName] = templatePairs[index]; + const hasParameter = parameterName != null; + + const [commonPrefixLength, childNode] = currentNode.findSimilarChild(anchor, hasParameter); + + currentNode = currentNode.merge( + childNode, + anchor, + hasParameter, + index === templatePairs.length - 1 ? routeKey : null, + routeParameterNames, + commonPrefixLength, + ); } - private removeChild(childNode: RouteNode) { - const childIndex = this.children.indexOf(childNode); - this.children.splice(childIndex, 1); - } - - countChildren() { - return this.children.length; - } + return currentNode; + } - insert( - routeKey: K, - templatePairs: Array, - ) { - const routeParameterNames = templatePairs - .map(([, parameterName]) => parameterName) - .filter((parameterName) => parameterName) as string[]; - - // eslint-disable-next-line @typescript-eslint/no-this-alias - let currentNode: RouteNode = this; - for (let index = 0; index < templatePairs.length; index++) { - const [anchor, parameterName] = templatePairs[index]; - const hasParameter = parameterName != null; - - const [commonPrefixLength, childNode] = - currentNode.findSimilarChild(anchor, hasParameter); - - currentNode = currentNode.merge( - childNode, - anchor, - hasParameter, - index === templatePairs.length - 1 ? routeKey : null, - routeParameterNames, - commonPrefixLength, - ); - } - - return currentNode; - } + parse(path: string, maximumParameterValueLength: number): [K | null, string[]] { + const parameterValues = new Array(); - parse( - path: string, - maximumParameterValueLength: number, - ): [K | null, string[]] { - const parameterValues = new Array(); - - if (this.hasParameter) { - // we are matching a parameter value! If the path's length is 0, there is no match, because a parameter value should have at least length 1 - if (path.length === 0) { - return [null, []]; - } - - // look for the anchor in the path (note: indexOf is probably the most expensive operation!) If the anchor is empty, match the remainder of the path - const index = - this.anchor.length === 0 - ? path.length - : path - .substring( - 0, - maximumParameterValueLength + this.anchor.length, - ) - .indexOf(this.anchor); - if (index < 0) { - return [null, []]; - } - - // get the parameter value - const value = path.substring(0, index); - - // remove the matches part from the path - path = path.substring(index + this.anchor.length); - - // add value to parameters - parameterValues.push(value); - } else { - // if this node does not represent a parameter we expect the path to start with the `anchor` - if (!path.startsWith(this.anchor)) { - // this node does not match the path - return [null, []]; - } - - // we successfully matches the node to the path, now remove the matched part from the path - path = path.substring(this.anchor.length); - } - - for (const childNode of this.children) { - // find a route in every child node - const [childRouteKey, childParameterValues] = childNode.parse( - path, - maximumParameterValueLength, - ); - - // if a child node is matched, return that node instead of the current! So child nodes are matched first! - if (childRouteKey != null) { - return [ - childRouteKey, - [...parameterValues, ...childParameterValues], - ]; - } - } - - // if the node had a route name and there is no path left to match against then we found a route - if (this.routeKey != null && path.length === 0) { - return [this.routeKey, parameterValues]; - } - - // we did not found a route :-( + if (this.hasParameter) { + // we are matching a parameter value! If the path's length is 0, there is no match, because a parameter value should have at least length 1 + if (path.length === 0) { return [null, []]; - } - - private merge( - childNode: RouteNode | null, - anchor: string, - hasParameter: boolean, - routeKey: K | null, - routeParameterNames: string[], - commonPrefixLength: number, - ) { - if (childNode == null) { - return this.mergeNew(anchor, hasParameter, routeKey); - } - - const commonPrefix = childNode.anchor.substring(0, commonPrefixLength); - - if (childNode.anchor === anchor) { - return this.mergeJoin(childNode, routeKey); - } else if (childNode.anchor === commonPrefix) { - return this.mergeAddToChild( - childNode, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commonPrefixLength, - ); - } else if (anchor === commonPrefix) { - return this.mergeAddToNew( - childNode, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commonPrefixLength, - ); - } else { - return this.mergeIntermediate( - childNode, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commonPrefixLength, - ); - } - } - private mergeNew( - anchor: string, - hasParameter: boolean, - routeKey: K | null, - ) { - const newNode = new RouteNode(anchor, hasParameter, routeKey); - this.addChild(newNode); - this.children.sort((a, b) => a.compare(b)); - return newNode; - } - private mergeJoin(childNode: RouteNode, routeKey: K | null) { - if (childNode.routeKey != null && routeKey != null) { - throw new Error("ambiguous route"); - } - - if (childNode.routeKey == null) { - childNode.routeKey = routeKey; - } - - return childNode; - } - private mergeIntermediate( - childNode: RouteNode, - anchor: string, - hasParameter: boolean, - routeKey: K | null, - routeParameterNames: string[], - commonPrefixLength: number, - ) { - this.removeChild(childNode); - - const newNode = new RouteNode( - anchor.substring(commonPrefixLength), - false, - routeKey, - ); - - childNode.anchor = childNode.anchor.substring(commonPrefixLength); - childNode.hasParameter = false; - - const intermediateNode = new RouteNode( - anchor.substring(0, commonPrefixLength), - hasParameter, - ); - intermediateNode.addChild(childNode); - intermediateNode.addChild(newNode); - - this.addChild(intermediateNode); - - this.children.sort((a, b) => a.compare(b)); - intermediateNode.children.sort((a, b) => a.compare(b)); - - return newNode; - } - private mergeAddToChild( - childNode: RouteNode, - anchor: string, - hasParameter: boolean, - routeKey: K | null, - routeParameterNames: string[], - commonPrefixLength: number, - ): RouteNode { - anchor = anchor.substring(commonPrefixLength); - hasParameter = false; - - const [commonPrefixLength2, childNode2] = childNode.findSimilarChild( - anchor, - hasParameter, - ); - - return childNode.merge( - childNode2, - anchor, - hasParameter, - routeKey, - routeParameterNames, - commonPrefixLength2, - ); - } - private mergeAddToNew( - childNode: RouteNode, - anchor: string, - hasParameter: boolean, - routeKey: K | null, - routeParameterNames: string[], - commonPrefixLength: number, - ): RouteNode { - const newNode = new RouteNode(anchor, hasParameter, routeKey); - this.addChild(newNode); - - this.removeChild(childNode); + } + + // look for the anchor in the path (note: indexOf is probably the most expensive operation!) If the anchor is empty, match the remainder of the path + const index = + this.anchor.length === 0 + ? path.length + : path + .substring(0, maximumParameterValueLength + this.anchor.length) + .indexOf(this.anchor); + if (index < 0) { + return [null, []]; + } - childNode.anchor = childNode.anchor.substring(commonPrefixLength); - childNode.hasParameter = false; + // get the parameter value + const value = path.substring(0, index); - newNode.addChild(childNode); + // remove the matches part from the path + path = path.substring(index + this.anchor.length); - this.children.sort((a, b) => a.compare(b)); - newNode.children.sort((a, b) => a.compare(b)); + // add value to parameters + parameterValues.push(value); + } else { + // if this node does not represent a parameter we expect the path to start with the `anchor` + if (!path.startsWith(this.anchor)) { + // this node does not match the path + return [null, []]; + } - return newNode; + // we successfully matches the node to the path, now remove the matched part from the path + path = path.substring(this.anchor.length); } - private findSimilarChild(anchor: string, hasParameter: boolean) { - for (const childNode of this.children) { - if (childNode.hasParameter !== hasParameter) { - continue; - } - - const commonPrefixLength = findCommonPrefixLength( - anchor, - childNode.anchor, - ); - if (commonPrefixLength === 0) { - continue; - } - - return [commonPrefixLength, childNode] as const; - } - - return [0, null] as const; + for (const childNode of this.children) { + // find a route in every child node + const [childRouteKey, childParameterValues] = childNode.parse( + path, + maximumParameterValueLength, + ); + + // if a child node is matched, return that node instead of the current! So child nodes are matched first! + if (childRouteKey != null) { + return [childRouteKey, [...parameterValues, ...childParameterValues]]; + } } - compare(other: RouteNode) { - if (this.anchor.length < other.anchor.length) return 1; - if (this.anchor.length > other.anchor.length) return -1; - - if (!this.hasParameter && other.hasParameter) return -1; - if (this.hasParameter && !other.hasParameter) return 1; + // if the node had a route name and there is no path left to match against then we found a route + if (this.routeKey != null && path.length === 0) { + return [this.routeKey, parameterValues]; + } - if (this.anchor < other.anchor) return -1; - if (this.anchor > other.anchor) return 1; + // we did not found a route :-( + return [null, []]; + } + + private merge( + childNode: RouteNode | null, + anchor: string, + hasParameter: boolean, + routeKey: K | null, + routeParameterNames: string[], + commonPrefixLength: number, + ) { + if (childNode == null) { + return this.mergeNew(anchor, hasParameter, routeKey); + } - return 0; + const commonPrefix = childNode.anchor.substring(0, commonPrefixLength); + + if (childNode.anchor === anchor) { + return this.mergeJoin(childNode, routeKey); + } else if (childNode.anchor === commonPrefix) { + return this.mergeAddToChild( + childNode, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commonPrefixLength, + ); + } else if (anchor === commonPrefix) { + return this.mergeAddToNew( + childNode, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commonPrefixLength, + ); + } else { + return this.mergeIntermediate( + childNode, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commonPrefixLength, + ); + } + } + private mergeNew(anchor: string, hasParameter: boolean, routeKey: K | null) { + const newNode = new RouteNode(anchor, hasParameter, routeKey); + this.addChild(newNode); + this.children.sort((a, b) => a.compare(b)); + return newNode; + } + private mergeJoin(childNode: RouteNode, routeKey: K | null) { + if (childNode.routeKey != null && routeKey != null) { + throw new Error("ambiguous route"); } - public toJSON(): RouteNodeJson { - const json = { - anchor: this.anchor, - hasParameter: this.hasParameter, - routeKey: this.routeKey, - children: this.children.map((child) => child.toJSON()), - }; - return json; + if (childNode.routeKey == null) { + childNode.routeKey = routeKey; } - public static fromJSON( - json: RouteNodeJson, - ): RouteNode { - const node = new RouteNode( - json.anchor, - json.hasParameter, - json.routeKey, - json.children.map((child) => RouteNode.fromJSON(child)), - ); - return node; + return childNode; + } + private mergeIntermediate( + childNode: RouteNode, + anchor: string, + hasParameter: boolean, + routeKey: K | null, + routeParameterNames: string[], + commonPrefixLength: number, + ) { + this.removeChild(childNode); + + const newNode = new RouteNode(anchor.substring(commonPrefixLength), false, routeKey); + + childNode.anchor = childNode.anchor.substring(commonPrefixLength); + childNode.hasParameter = false; + + const intermediateNode = new RouteNode( + anchor.substring(0, commonPrefixLength), + hasParameter, + ); + intermediateNode.addChild(childNode); + intermediateNode.addChild(newNode); + + this.addChild(intermediateNode); + + this.children.sort((a, b) => a.compare(b)); + intermediateNode.children.sort((a, b) => a.compare(b)); + + return newNode; + } + private mergeAddToChild( + childNode: RouteNode, + anchor: string, + hasParameter: boolean, + routeKey: K | null, + routeParameterNames: string[], + commonPrefixLength: number, + ): RouteNode { + anchor = anchor.substring(commonPrefixLength); + hasParameter = false; + + const [commonPrefixLength2, childNode2] = childNode.findSimilarChild(anchor, hasParameter); + + return childNode.merge( + childNode2, + anchor, + hasParameter, + routeKey, + routeParameterNames, + commonPrefixLength2, + ); + } + private mergeAddToNew( + childNode: RouteNode, + anchor: string, + hasParameter: boolean, + routeKey: K | null, + routeParameterNames: string[], + commonPrefixLength: number, + ): RouteNode { + const newNode = new RouteNode(anchor, hasParameter, routeKey); + this.addChild(newNode); + + this.removeChild(childNode); + + childNode.anchor = childNode.anchor.substring(commonPrefixLength); + childNode.hasParameter = false; + + newNode.addChild(childNode); + + this.children.sort((a, b) => a.compare(b)); + newNode.children.sort((a, b) => a.compare(b)); + + return newNode; + } + + private findSimilarChild(anchor: string, hasParameter: boolean) { + for (const childNode of this.children) { + if (childNode.hasParameter !== hasParameter) { + continue; + } + + const commonPrefixLength = findCommonPrefixLength(anchor, childNode.anchor); + if (commonPrefixLength === 0) { + continue; + } + + return [commonPrefixLength, childNode] as const; } + + return [0, null] as const; + } + + compare(other: RouteNode) { + if (this.anchor.length < other.anchor.length) return 1; + if (this.anchor.length > other.anchor.length) return -1; + + if (!this.hasParameter && other.hasParameter) return -1; + if (this.hasParameter && !other.hasParameter) return 1; + + if (this.anchor < other.anchor) return -1; + if (this.anchor > other.anchor) return 1; + + return 0; + } + + public toJSON(): RouteNodeJson { + const json = { + anchor: this.anchor, + hasParameter: this.hasParameter, + routeKey: this.routeKey, + children: this.children.map((child) => child.toJSON()), + }; + return json; + } + + public static fromJSON(json: RouteNodeJson): RouteNode { + const node = new RouteNode( + json.anchor, + json.hasParameter, + json.routeKey, + json.children.map((child) => RouteNode.fromJSON(child)), + ); + return node; + } } diff --git a/packages/npm/goodrouter/src/router-options.ts b/packages/npm/goodrouter/src/router-options.ts index b8cd697..20c9261 100644 --- a/packages/npm/goodrouter/src/router-options.ts +++ b/packages/npm/goodrouter/src/router-options.ts @@ -3,34 +3,32 @@ * Default options to be passed to the router */ export const defaultRouterOptions = { - /** - * @description - * Default encoding function to use, this is the encodeUriComponent function by default - * - * @param decodedValue value to be encoded - * @returns encoded value - */ - parameterValueEncoder: (decodedValue: string) => - encodeURIComponent(decodedValue), - /** - * @description - * Default decoding function to use, this is the decodeURIComponent function by default - * - * @param encodedValue value to be decoded - * @returns decoded value - */ - parameterValueDecoder: (encodedValue: string) => - decodeURIComponent(encodedValue), + /** + * @description + * Default encoding function to use, this is the encodeUriComponent function by default + * + * @param decodedValue value to be encoded + * @returns encoded value + */ + parameterValueEncoder: (decodedValue: string) => encodeURIComponent(decodedValue), + /** + * @description + * Default decoding function to use, this is the decodeURIComponent function by default + * + * @param encodedValue value to be decoded + * @returns decoded value + */ + parameterValueDecoder: (encodedValue: string) => decodeURIComponent(encodedValue), - /** - * Use `{` and `}` as a default for matching placeholders in the route templates. - */ - parameterPlaceholderRE: /\{(.*?)\}/gu, + /** + * Use `{` and `}` as a default for matching placeholders in the route templates. + */ + parameterPlaceholderRE: /\{(.*?)\}/gu, - /** - * Assume a maximum parameter value length of 20 - */ - maximumParameterValueLength: 20, + /** + * Assume a maximum parameter value length of 20 + */ + maximumParameterValueLength: 20, }; /** @@ -38,32 +36,32 @@ export const defaultRouterOptions = { * Options to be passed to the router */ export interface RouterOptions { - /** - * @description - * This function wil be used on each parameter value when parsing a route - * - * @param decodedValue value to be encoded - * @returns encoded value - */ - parameterValueEncoder?: (decodedValue: string) => string; - /** - * @description - * This function wil be used on each parameter value when constructing a route - * - * @param encodedValue value to be decoded - * @returns decoded value - */ - parameterValueDecoder?: (encodedValue: string) => string; + /** + * @description + * This function wil be used on each parameter value when parsing a route + * + * @param decodedValue value to be encoded + * @returns encoded value + */ + parameterValueEncoder?: (decodedValue: string) => string; + /** + * @description + * This function wil be used on each parameter value when constructing a route + * + * @param encodedValue value to be decoded + * @returns decoded value + */ + parameterValueDecoder?: (encodedValue: string) => string; - /** - * Regular expression to use when parsing placeholders from a route template. This regular - * expression must have the global option set! Defaults to `/\{(.*?)\}/gu`. - */ - parameterPlaceholderRE?: RegExp; + /** + * Regular expression to use when parsing placeholders from a route template. This regular + * expression must have the global option set! Defaults to `/\{(.*?)\}/gu`. + */ + parameterPlaceholderRE?: RegExp; - /** - * The expected maximum character length of a parameter value. No parameter value should - * be longer than what is specified here! - */ - maximumParameterValueLength?: number; + /** + * The expected maximum character length of a parameter value. No parameter value should + * be longer than what is specified here! + */ + maximumParameterValueLength?: number; } diff --git a/packages/npm/goodrouter/src/router-parse.bench.ts b/packages/npm/goodrouter/src/router-parse.bench.ts index cf7e069..908d1d7 100644 --- a/packages/npm/goodrouter/src/router-parse.bench.ts +++ b/packages/npm/goodrouter/src/router-parse.bench.ts @@ -9,37 +9,35 @@ runBenchmark("docker"); runBenchmark("github"); function runBenchmark(name: string) { - const templates = loadTemplates(name); - const parameterNames = [...parametersFromTemplates(templates)]; - const parameters = Object.fromEntries( - parameterNames.map((name, index) => [name, `p${index}`]), - ); + const templates = loadTemplates(name); + const parameterNames = [...parametersFromTemplates(templates)]; + const parameters = Object.fromEntries(parameterNames.map((name, index) => [name, `p${index}`])); - const templateCount = templates.length; + const templateCount = templates.length; - const router = new Router(); - for (const template of templates) { - router.insertRoute(template, template); - } + const router = new Router(); + for (const template of templates) { + router.insertRoute(template, template); + } - const paths = templates.map((template) => { - const path = router.stringifyRoute(template, parameters); - assert(path != null); - return path; - }); + const paths = templates.map((template) => { + const path = router.stringifyRoute(template, parameters); + assert(path != null); + return path; + }); - let iteration = 0; - function benchmarkTask() { - const path = paths[iteration % templateCount]; + let iteration = 0; + function benchmarkTask() { + const path = paths[iteration % templateCount]; - router.parseRoute(path); + router.parseRoute(path); - iteration++; - } + iteration++; + } - const benchmark = new Benchmark(name, benchmarkTask); + const benchmark = new Benchmark(name, benchmarkTask); - benchmark.run(); + benchmark.run(); - console.log(String(benchmark)); + console.log(String(benchmark)); } diff --git a/packages/npm/goodrouter/src/router-stringify.bench.ts b/packages/npm/goodrouter/src/router-stringify.bench.ts index 3ad0645..7be6333 100644 --- a/packages/npm/goodrouter/src/router-stringify.bench.ts +++ b/packages/npm/goodrouter/src/router-stringify.bench.ts @@ -8,31 +8,29 @@ runBenchmark("docker"); runBenchmark("github"); function runBenchmark(name: string) { - const templates = loadTemplates(name); - const parameterNames = [...parametersFromTemplates(templates)]; - const parameters = Object.fromEntries( - parameterNames.map((name, index) => [name, `p${index}`]), - ); + const templates = loadTemplates(name); + const parameterNames = [...parametersFromTemplates(templates)]; + const parameters = Object.fromEntries(parameterNames.map((name, index) => [name, `p${index}`])); - const templateCount = templates.length; + const templateCount = templates.length; - const router = new Router(); - for (const template of templates) { - router.insertRoute(template, template); - } + const router = new Router(); + for (const template of templates) { + router.insertRoute(template, template); + } - let iteration = 0; - function benchmarkTask() { - const template = templates[iteration % templateCount]; + let iteration = 0; + function benchmarkTask() { + const template = templates[iteration % templateCount]; - router.stringifyRoute(template, parameters); + router.stringifyRoute(template, parameters); - iteration++; - } + iteration++; + } - const benchmark = new Benchmark(name, benchmarkTask); + const benchmark = new Benchmark(name, benchmarkTask); - benchmark.run(); + benchmark.run(); - console.log(String(benchmark)); + console.log(String(benchmark)); } diff --git a/packages/npm/goodrouter/src/router.spec.ts b/packages/npm/goodrouter/src/router.spec.ts deleted file mode 100644 index 0b67827..0000000 --- a/packages/npm/goodrouter/src/router.spec.ts +++ /dev/null @@ -1,266 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { Router, RouterMode } from "./router.js"; -import { parametersFromTemplates } from "./testing/parameters.js"; -import { loadTemplates } from "./testing/templates.js"; - -test("router-readme", () => { - const router = new Router(); - - router.insertRoute("all-products", "/product/all"); - router.insertRoute("product-detail", "/product/{id}"); - - // And now we can parse routes! - - { - const [routeKey] = router.parseRoute("/not-found"); - assert.equal(routeKey, null); - } - - { - const [routeKey] = router.parseRoute("/product/all"); - assert.equal(routeKey, "all-products"); - } - - { - const [routeKey, routeParameters] = router.parseRoute("/product/1"); - assert.equal(routeKey, "product-detail"); - assert.deepEqual(routeParameters, { id: "1" }); - } - - // And we can stringify routes - - { - const path = router.stringifyRoute("all-products"); - assert.equal(path, "/product/all"); - } - - { - const path = router.stringifyRoute("product-detail", { id: "2" }); - assert.equal(path, "/product/2"); - } -}); - -test("parse-route 1", () => { - enum Route { - A, - B, - C, - D, - } - - const router = new Router(); - - router.insertRoute(Route.A, "/a"); - router.insertRoute(Route.B, "/b/{x}"); - router.insertRoute(Route.C, "/b/{x}/c"); - router.insertRoute(Route.D, "/b/{x}/d"); - - { - const [routeKey] = router.parseRoute("/a"); - assert.equal(routeKey, Route.A); - } - { - const [routeKey] = router.parseRoute("/b/x"); - assert.equal(routeKey, Route.B); - } - { - const [routeKey] = router.parseRoute("/b/y/c"); - assert.equal(routeKey, Route.C); - } - { - const [routeKey] = router.parseRoute("/b/z/d"); - assert.equal(routeKey, Route.D); - } -}); - -test("parse-route 2", () => { - const router = new Router(); - - router.insertRoute("aa", "a/{a}/a"); - router.insertRoute("a", "a"); - - router.insertRoute("one", "/a"); - router.insertRoute("two", "/a/{x}/{y}"); - router.insertRoute("three", "/c/{x}"); - router.insertRoute("four", "/c/{x}/{y}/"); - - { - const [routeKey, routeParameters] = router.parseRoute("/a"); - assert.equal(routeKey, "one"); - } - - { - const [routeKey, routeParameters] = router.parseRoute("/a/1/2"); - assert.equal(routeKey, "two"); - assert.deepEqual(routeParameters, { x: "1", y: "2" }); - } - - { - const path = router.stringifyRoute("two", { x: "1", y: "2" }); - assert.equal(path, "/a/1/2"); - } - - { - const [routeKey, routeParameters] = router.parseRoute("/c/3"); - assert.equal(routeKey, "three"); - assert.deepEqual(routeParameters, { x: "3" }); - } - - { - const [routeKey, routeParameters] = router.parseRoute("/c/3/4"); - assert.equal(routeKey, "three"); - assert.deepEqual(routeParameters, { x: "3/4" }); - } - - { - const path = router.stringifyRoute("three", { x: "3/4" }); - assert.equal(path, "/c/3%2F4"); - } - - { - const [routeKey, routeParameters] = router.parseRoute("/c/3/4/"); - assert.equal(routeKey, "four"); - assert.deepEqual(routeParameters, { x: "3", y: "4" }); - } -}); - -test("router bug", () => { - const router = new Router(); - - router - .insertRoute("a", "/enterprises/{enterprise}/actions/runner-groups") - .insertRoute( - "b", - "/enterprises/{enterprise}/actions/runner-groups/{runner_group_id}", - ) - .insertRoute( - "c", - "/enterprises/{enterprise}/actions/runner-groups/{runner_group_id}/organizations", - ); - - assert.deepEqual( - router.parseRoute("/enterprises/xx/actions/runner-groups"), - ["a", { enterprise: "xx" }], - ); - - assert.deepEqual( - router.parseRoute("/enterprises/xx/actions/runner-groups/yy"), - ["b", { enterprise: "xx", runner_group_id: "yy" }], - ); - - assert.deepEqual( - router.parseRoute( - "/enterprises/xx/actions/runner-groups/yy/organizations", - ), - ["c", { enterprise: "xx", runner_group_id: "yy" }], - ); -}); - -test("route-node-json", () => { - const router = new Router(); - router.insertRoute(1, "x/y"); - router.insertRoute(2, "x/z"); - - const templatePairs = [ - [1, [["x/y", null]]], - [2, [["x/z", null]]], - ]; - const rootNode = { - anchor: "", - hasParameter: false, - routeKey: null, - children: [ - { - anchor: "x/", - hasParameter: false, - routeKey: null, - children: [ - { - anchor: "y", - hasParameter: false, - routeKey: 1, - children: [], - }, - { - anchor: "z", - hasParameter: false, - routeKey: 2, - children: [], - }, - ], - }, - ], - }; - - { - const actual = router.saveToJson(RouterMode.Client); - const expected = { - rootNode: undefined, - templatePairs, - }; - assert.deepEqual(actual, expected); - } - - { - const actual = router.saveToJson(RouterMode.Server); - const expected = { - rootNode, - templatePairs: undefined, - }; - assert.deepEqual(actual, expected); - } - - { - const actual = router.saveToJson(RouterMode.Bidirectional); - const expected = { - rootNode, - templatePairs, - }; - assert.deepEqual(actual, expected); - } -}); - -testTemplates("small"); -testTemplates("docker"); -testTemplates("github"); - -function testTemplates(name: string) { - test(`${name} templates`, () => { - const templates = loadTemplates(name); - const allParameterNames = [...parametersFromTemplates(templates)]; - - const allParameters = Object.fromEntries( - allParameterNames.map((name, index) => [name, `p${index}`]), - ); - - const templateCount = templates.length; - - const router = new Router(); - for (const template of templates) { - router.insertRoute(template, template); - } - - const paths = templates.map((template) => { - const path = router.stringifyRoute(template, allParameters); - assert(path != null); - return path; - }); - - for (let index = 0; index < templateCount; index++) { - const path = paths[index]; - const template = templates[index]; - - const [routeKey, routeParameters] = router.parseRoute(path); - const expectedParameters = Object.fromEntries( - Object.keys(routeParameters).map((name) => [ - name, - allParameters[name], - ]), - ); - - assert.equal(routeKey, template); - assert.deepEqual(routeParameters, expectedParameters); - } - }); -} diff --git a/packages/npm/goodrouter/src/router.test.ts b/packages/npm/goodrouter/src/router.test.ts new file mode 100644 index 0000000..fe4ee01 --- /dev/null +++ b/packages/npm/goodrouter/src/router.test.ts @@ -0,0 +1,258 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Router, RouterMode } from "./router.js"; +import { parametersFromTemplates } from "./testing/parameters.js"; +import { loadTemplates } from "./testing/templates.js"; + +test("router-readme", () => { + const router = new Router(); + + router.insertRoute("all-products", "/product/all"); + router.insertRoute("product-detail", "/product/{id}"); + + // And now we can parse routes! + + { + const [routeKey] = router.parseRoute("/not-found"); + assert.equal(routeKey, null); + } + + { + const [routeKey] = router.parseRoute("/product/all"); + assert.equal(routeKey, "all-products"); + } + + { + const [routeKey, routeParameters] = router.parseRoute("/product/1"); + assert.equal(routeKey, "product-detail"); + assert.deepEqual(routeParameters, { id: "1" }); + } + + // And we can stringify routes + + { + const path = router.stringifyRoute("all-products"); + assert.equal(path, "/product/all"); + } + + { + const path = router.stringifyRoute("product-detail", { id: "2" }); + assert.equal(path, "/product/2"); + } +}); + +test("parse-route 1", () => { + enum Route { + A, + B, + C, + D, + } + + const router = new Router(); + + router.insertRoute(Route.A, "/a"); + router.insertRoute(Route.B, "/b/{x}"); + router.insertRoute(Route.C, "/b/{x}/c"); + router.insertRoute(Route.D, "/b/{x}/d"); + + { + const [routeKey] = router.parseRoute("/a"); + assert.equal(routeKey, Route.A); + } + { + const [routeKey] = router.parseRoute("/b/x"); + assert.equal(routeKey, Route.B); + } + { + const [routeKey] = router.parseRoute("/b/y/c"); + assert.equal(routeKey, Route.C); + } + { + const [routeKey] = router.parseRoute("/b/z/d"); + assert.equal(routeKey, Route.D); + } +}); + +test("parse-route 2", () => { + const router = new Router(); + + router.insertRoute("aa", "a/{a}/a"); + router.insertRoute("a", "a"); + + router.insertRoute("one", "/a"); + router.insertRoute("two", "/a/{x}/{y}"); + router.insertRoute("three", "/c/{x}"); + router.insertRoute("four", "/c/{x}/{y}/"); + + { + const [routeKey, routeParameters] = router.parseRoute("/a"); + assert.equal(routeKey, "one"); + } + + { + const [routeKey, routeParameters] = router.parseRoute("/a/1/2"); + assert.equal(routeKey, "two"); + assert.deepEqual(routeParameters, { x: "1", y: "2" }); + } + + { + const path = router.stringifyRoute("two", { x: "1", y: "2" }); + assert.equal(path, "/a/1/2"); + } + + { + const [routeKey, routeParameters] = router.parseRoute("/c/3"); + assert.equal(routeKey, "three"); + assert.deepEqual(routeParameters, { x: "3" }); + } + + { + const [routeKey, routeParameters] = router.parseRoute("/c/3/4"); + assert.equal(routeKey, "three"); + assert.deepEqual(routeParameters, { x: "3/4" }); + } + + { + const path = router.stringifyRoute("three", { x: "3/4" }); + assert.equal(path, "/c/3%2F4"); + } + + { + const [routeKey, routeParameters] = router.parseRoute("/c/3/4/"); + assert.equal(routeKey, "four"); + assert.deepEqual(routeParameters, { x: "3", y: "4" }); + } +}); + +test("router bug", () => { + const router = new Router(); + + router + .insertRoute("a", "/enterprises/{enterprise}/actions/runner-groups") + .insertRoute("b", "/enterprises/{enterprise}/actions/runner-groups/{runner_group_id}") + .insertRoute( + "c", + "/enterprises/{enterprise}/actions/runner-groups/{runner_group_id}/organizations", + ); + + assert.deepEqual(router.parseRoute("/enterprises/xx/actions/runner-groups"), [ + "a", + { enterprise: "xx" }, + ]); + + assert.deepEqual(router.parseRoute("/enterprises/xx/actions/runner-groups/yy"), [ + "b", + { enterprise: "xx", runner_group_id: "yy" }, + ]); + + assert.deepEqual(router.parseRoute("/enterprises/xx/actions/runner-groups/yy/organizations"), [ + "c", + { enterprise: "xx", runner_group_id: "yy" }, + ]); +}); + +test("route-node-json", () => { + const router = new Router(); + router.insertRoute(1, "x/y"); + router.insertRoute(2, "x/z"); + + const templatePairs = [ + [1, [["x/y", null]]], + [2, [["x/z", null]]], + ]; + const rootNode = { + anchor: "", + hasParameter: false, + routeKey: null, + children: [ + { + anchor: "x/", + hasParameter: false, + routeKey: null, + children: [ + { + anchor: "y", + hasParameter: false, + routeKey: 1, + children: [], + }, + { + anchor: "z", + hasParameter: false, + routeKey: 2, + children: [], + }, + ], + }, + ], + }; + + { + const actual = router.saveToJson(RouterMode.Client); + const expected = { + rootNode: undefined, + templatePairs, + }; + assert.deepEqual(actual, expected); + } + + { + const actual = router.saveToJson(RouterMode.Server); + const expected = { + rootNode, + templatePairs: undefined, + }; + assert.deepEqual(actual, expected); + } + + { + const actual = router.saveToJson(RouterMode.Bidirectional); + const expected = { + rootNode, + templatePairs, + }; + assert.deepEqual(actual, expected); + } +}); + +testTemplates("small"); +testTemplates("docker"); +testTemplates("github"); + +function testTemplates(name: string) { + test(`${name} templates`, () => { + const templates = loadTemplates(name); + const allParameterNames = [...parametersFromTemplates(templates)]; + + const allParameters = Object.fromEntries( + allParameterNames.map((name, index) => [name, `p${index}`]), + ); + + const templateCount = templates.length; + + const router = new Router(); + for (const template of templates) { + router.insertRoute(template, template); + } + + const paths = templates.map((template) => { + const path = router.stringifyRoute(template, allParameters); + assert(path != null); + return path; + }); + + for (let index = 0; index < templateCount; index++) { + const path = paths[index]; + const template = templates[index]; + + const [routeKey, routeParameters] = router.parseRoute(path); + const expectedParameters = Object.fromEntries( + Object.keys(routeParameters).map((name) => [name, allParameters[name]]), + ); + + assert.equal(routeKey, template); + assert.deepEqual(routeParameters, expectedParameters); + } + }); +} diff --git a/packages/npm/goodrouter/src/router.ts b/packages/npm/goodrouter/src/router.ts index 1538ae7..b6c0d83 100644 --- a/packages/npm/goodrouter/src/router.ts +++ b/packages/npm/goodrouter/src/router.ts @@ -4,9 +4,9 @@ import { defaultRouterOptions, RouterOptions } from "./router-options.js"; import { parseTemplatePairs } from "./template.js"; export enum RouterMode { - Client = 1 << 1, - Server = 1 << 2, - Bidirectional = Client | Server, + Client = 1 << 1, + Server = 1 << 2, + Bidirectional = Client | Server, } /** @@ -59,155 +59,139 @@ export enum RouterMode { * ``` */ export class Router { - constructor( - options: RouterOptions = {}, - private mode = RouterMode.Bidirectional, - ) { - this.options = { - ...defaultRouterOptions, - ...options, - }; + constructor( + options: RouterOptions = {}, + private mode = RouterMode.Bidirectional, + ) { + this.options = { + ...defaultRouterOptions, + ...options, + }; + } + + protected options: RouterOptions & typeof defaultRouterOptions; + + private rootNode = new RouteNode(); + private templatePairs = new Map>(); + + /** + * @description + * Adds a new route + * + * @param routeKey name of the route + * @param routeTemplate template for the route, als defines parameters + */ + public insertRoute(routeKey: K, routeTemplate: string) { + const templatePairs = [ + ...parseTemplatePairs(routeTemplate, this.options.parameterPlaceholderRE), + ]; + if ((this.mode & RouterMode.Client) > 0) { + this.templatePairs.set(routeKey, templatePairs); } - - protected options: RouterOptions & typeof defaultRouterOptions; - - private rootNode = new RouteNode(); - private templatePairs = new Map< - K, - Array - >(); - - /** - * @description - * Adds a new route - * - * @param routeKey name of the route - * @param routeTemplate template for the route, als defines parameters - */ - public insertRoute(routeKey: K, routeTemplate: string) { - const templatePairs = [ - ...parseTemplatePairs( - routeTemplate, - this.options.parameterPlaceholderRE, - ), - ]; - if ((this.mode & RouterMode.Client) > 0) { - this.templatePairs.set(routeKey, templatePairs); - } - if ((this.mode & RouterMode.Server) > 0) { - this.rootNode.insert(routeKey, templatePairs); - } - return this; + if ((this.mode & RouterMode.Server) > 0) { + this.rootNode.insert(routeKey, templatePairs); } - - /** - * @description - * Match the path against a known routes and parse the parameters in it - * - * @param path path to match - * @returns tuple with the route name or null if no route found. Then the parameters - */ - public parseRoute(path: string): [K | null, Record] { - if ((this.mode & RouterMode.Server) === 0) { - throw new TypeError("Router needs to be in server mode to parse"); - } - - const parameters: Record = {}; - - const [routeKey, parameterValues] = this.rootNode.parse( - path, - this.options.maximumParameterValueLength, - ); - - if (routeKey == null) { - return [null, {}]; - } - - const templatePairs = this.templatePairs.get(routeKey); - if (templatePairs == null) { - // this never happens - return [null, {}]; - } - - for (let index = 0; index < parameterValues.length; index++) { - const [, parameterName] = templatePairs[index + 1]; - if (parameterName == null) { - // this never happens - return [null, {}]; - } - const parameterValue = parameterValues[index]; - parameters[parameterName] = - this.options.parameterValueDecoder(parameterValue); - } - - return [routeKey, parameters]; + return this; + } + + /** + * @description + * Match the path against a known routes and parse the parameters in it + * + * @param path path to match + * @returns tuple with the route name or null if no route found. Then the parameters + */ + public parseRoute(path: string): [K | null, Record] { + if ((this.mode & RouterMode.Server) === 0) { + throw new TypeError("Router needs to be in server mode to parse"); } - /** - * @description - * Convert a route to a path string. - * - * @param routeKey route to stringify - * @param routeParameters parameters to include in the path - * @returns string representing the route or null if the route is not found by name - */ - public stringifyRoute( - routeKey: K, - routeParameters: Record = {}, - ): string | null { - if ((this.mode & RouterMode.Client) === 0) { - throw new TypeError( - "Router needs to be in client mode to stringify", - ); - } - - let result = ""; - const templatePairs = this.templatePairs.get(routeKey); - if (templatePairs == null) { - return null; - } - for (let index = 0; index < templatePairs.length; index++) { - const [parameterAnchor, parameterName] = templatePairs[index]; - if (parameterName != null) { - const parameterValue = routeParameters[parameterName]; - result += this.options.parameterValueEncoder(parameterValue); - } - result += parameterAnchor; - } - return result; + const parameters: Record = {}; + + const [routeKey, parameterValues] = this.rootNode.parse( + path, + this.options.maximumParameterValueLength, + ); + + if (routeKey == null) { + return [null, {}]; } - public saveToJson(mode = this.mode): RouterJson { - const rootNode = - (this.mode & mode & RouterMode.Server) > 0 - ? this.rootNode.toJSON() - : undefined; - const templatePairs = - (this.mode & mode & RouterMode.Client) > 0 - ? [...this.templatePairs] - : undefined; - - return { - rootNode, - templatePairs, - }; + const templatePairs = this.templatePairs.get(routeKey); + if (templatePairs == null) { + // this never happens + return [null, {}]; } - public loadFromJson(json: RouterJson) { - this.mode = RouterMode.Bidirectional; + for (let index = 0; index < parameterValues.length; index++) { + const [, parameterName] = templatePairs[index + 1]; + if (parameterName == null) { + // this never happens + return [null, {}]; + } + const parameterValue = parameterValues[index]; + parameters[parameterName] = this.options.parameterValueDecoder(parameterValue); + } - if (json.rootNode == null) { - this.mode &= ~RouterMode.Server; - } else { - this.rootNode = RouteNode.fromJSON(json.rootNode); - } + return [routeKey, parameters]; + } + + /** + * @description + * Convert a route to a path string. + * + * @param routeKey route to stringify + * @param routeParameters parameters to include in the path + * @returns string representing the route or null if the route is not found by name + */ + public stringifyRoute(routeKey: K, routeParameters: Record = {}): string | null { + if ((this.mode & RouterMode.Client) === 0) { + throw new TypeError("Router needs to be in client mode to stringify"); + } - if (json.templatePairs == null) { - this.mode &= ~RouterMode.Client; - } else { - this.templatePairs = new Map(json.templatePairs); - } + let result = ""; + const templatePairs = this.templatePairs.get(routeKey); + if (templatePairs == null) { + return null; + } + for (let index = 0; index < templatePairs.length; index++) { + const [parameterAnchor, parameterName] = templatePairs[index]; + if (parameterName != null) { + const parameterValue = routeParameters[parameterName]; + result += this.options.parameterValueEncoder(parameterValue); + } + result += parameterAnchor; + } + return result; + } + + public saveToJson(mode = this.mode): RouterJson { + const rootNode = + (this.mode & mode & RouterMode.Server) > 0 ? this.rootNode.toJSON() : undefined; + const templatePairs = + (this.mode & mode & RouterMode.Client) > 0 ? [...this.templatePairs] : undefined; + + return { + rootNode, + templatePairs, + }; + } + + public loadFromJson(json: RouterJson) { + this.mode = RouterMode.Bidirectional; + + if (json.rootNode == null) { + this.mode &= ~RouterMode.Server; + } else { + this.rootNode = RouteNode.fromJSON(json.rootNode); + } - return this; + if (json.templatePairs == null) { + this.mode &= ~RouterMode.Client; + } else { + this.templatePairs = new Map(json.templatePairs); } + + return this; + } } diff --git a/packages/npm/goodrouter/src/template.spec.ts b/packages/npm/goodrouter/src/template.spec.ts deleted file mode 100644 index d182694..0000000 --- a/packages/npm/goodrouter/src/template.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { defaultRouterOptions } from "./router-options.js"; -import { parseTemplatePairs, parseTemplateParts } from "./template.js"; - -test("parse-template-parts", () => { - { - const parts = [ - ...parseTemplateParts( - "/a/{b}/{c}", - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - - assert.deepEqual(parts, ["/a/", "b", "/", "c", ""]); - } - - { - const parts = [ - ...parseTemplateParts( - "/a/{b}/{c}/", - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - - assert.deepEqual(parts, ["/a/", "b", "/", "c", "/"]); - } - - { - const parts = [ - ...parseTemplateParts( - "", - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - - assert.deepEqual(parts, [""]); - } -}); - -test("parse-template-pairs", () => { - { - const parts = [ - ...parseTemplatePairs( - "/a/{b}/{c}", - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - - assert.deepEqual(parts, [ - ["/a/", null], - ["/", "b"], - ["", "c"], - ]); - } - - { - const parts = [ - ...parseTemplatePairs( - "/a/{b}/{c}/", - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - - assert.deepEqual(parts, [ - ["/a/", null], - ["/", "b"], - ["/", "c"], - ]); - } - - { - const parts = [ - ...parseTemplatePairs( - "", - defaultRouterOptions.parameterPlaceholderRE, - ), - ]; - - assert.deepEqual(parts, [["", null]]); - } -}); diff --git a/packages/npm/goodrouter/src/template.test.ts b/packages/npm/goodrouter/src/template.test.ts new file mode 100644 index 0000000..fd44391 --- /dev/null +++ b/packages/npm/goodrouter/src/template.test.ts @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { defaultRouterOptions } from "./router-options.js"; +import { parseTemplatePairs, parseTemplateParts } from "./template.js"; + +test("parse-template-parts", () => { + { + const parts = [ + ...parseTemplateParts("/a/{b}/{c}", defaultRouterOptions.parameterPlaceholderRE), + ]; + + assert.deepEqual(parts, ["/a/", "b", "/", "c", ""]); + } + + { + const parts = [ + ...parseTemplateParts("/a/{b}/{c}/", defaultRouterOptions.parameterPlaceholderRE), + ]; + + assert.deepEqual(parts, ["/a/", "b", "/", "c", "/"]); + } + + { + const parts = [...parseTemplateParts("", defaultRouterOptions.parameterPlaceholderRE)]; + + assert.deepEqual(parts, [""]); + } +}); + +test("parse-template-pairs", () => { + { + const parts = [ + ...parseTemplatePairs("/a/{b}/{c}", defaultRouterOptions.parameterPlaceholderRE), + ]; + + assert.deepEqual(parts, [ + ["/a/", null], + ["/", "b"], + ["", "c"], + ]); + } + + { + const parts = [ + ...parseTemplatePairs("/a/{b}/{c}/", defaultRouterOptions.parameterPlaceholderRE), + ]; + + assert.deepEqual(parts, [ + ["/a/", null], + ["/", "b"], + ["/", "c"], + ]); + } + + { + const parts = [...parseTemplatePairs("", defaultRouterOptions.parameterPlaceholderRE)]; + + assert.deepEqual(parts, [["", null]]); + } +}); diff --git a/packages/npm/goodrouter/src/template.ts b/packages/npm/goodrouter/src/template.ts index 9fb9e8b..e347248 100644 --- a/packages/npm/goodrouter/src/template.ts +++ b/packages/npm/goodrouter/src/template.ts @@ -8,41 +8,32 @@ * @param parameterPlaceholderRE regular expression to use when searching for parameter placeholders * @returns Iterable of strings, always an uneven number of elements. */ -export function* parseTemplateParts( - routeTemplate: string, - parameterPlaceholderRE: RegExp, -) { - if (!parameterPlaceholderRE.global) { - throw new Error("regular expression needs to be global"); - } +export function* parseTemplateParts(routeTemplate: string, parameterPlaceholderRE: RegExp) { + if (!parameterPlaceholderRE.global) { + throw new Error("regular expression needs to be global"); + } - let match; - let offsetIndex = 0; - while ((match = parameterPlaceholderRE.exec(routeTemplate)) != null) { - yield routeTemplate.substring( - offsetIndex, - parameterPlaceholderRE.lastIndex - match[0].length, - ); - yield match[1]; - offsetIndex = parameterPlaceholderRE.lastIndex; - } - yield routeTemplate.substring(offsetIndex); + let match; + let offsetIndex = 0; + while ((match = parameterPlaceholderRE.exec(routeTemplate)) != null) { + yield routeTemplate.substring(offsetIndex, parameterPlaceholderRE.lastIndex - match[0].length); + yield match[1]; + offsetIndex = parameterPlaceholderRE.lastIndex; + } + yield routeTemplate.substring(offsetIndex); } -export function* parseTemplatePairs( - routeTemplate: string, - parameterPlaceholderRE: RegExp, -) { - const parts = parseTemplateParts(routeTemplate, parameterPlaceholderRE); +export function* parseTemplatePairs(routeTemplate: string, parameterPlaceholderRE: RegExp) { + const parts = parseTemplateParts(routeTemplate, parameterPlaceholderRE); - let index = 0; - let parameter: string | null = null; - for (const part of parts) { - if (index % 2 === 0) { - yield [part, parameter] as const; - } else { - parameter = part; - } - index++; + let index = 0; + let parameter: string | null = null; + for (const part of parts) { + if (index % 2 === 0) { + yield [part, parameter] as const; + } else { + parameter = part; } + index++; + } } diff --git a/packages/npm/goodrouter/src/testing/parameters.ts b/packages/npm/goodrouter/src/testing/parameters.ts index 2a65507..130de64 100644 --- a/packages/npm/goodrouter/src/testing/parameters.ts +++ b/packages/npm/goodrouter/src/testing/parameters.ts @@ -1,23 +1,18 @@ import { defaultRouterOptions } from "../router-options.js"; import { parseTemplateParts } from "../template.js"; -export function parametersFromTemplates( - templates: Iterable, -): Iterable { - const parameters = new Set(); - for (const template of templates) { - let index = 0; - for (const part of parseTemplateParts( - template, - defaultRouterOptions.parameterPlaceholderRE, - )) { - if (index % 2 !== 0) { - parameters.add(part); - } +export function parametersFromTemplates(templates: Iterable): Iterable { + const parameters = new Set(); + for (const template of templates) { + let index = 0; + for (const part of parseTemplateParts(template, defaultRouterOptions.parameterPlaceholderRE)) { + if (index % 2 !== 0) { + parameters.add(part); + } - index++; - } + index++; } + } - return parameters; + return parameters; } diff --git a/packages/npm/goodrouter/src/testing/templates.ts b/packages/npm/goodrouter/src/testing/templates.ts index 602cb83..b3e94d6 100644 --- a/packages/npm/goodrouter/src/testing/templates.ts +++ b/packages/npm/goodrouter/src/testing/templates.ts @@ -1,23 +1,16 @@ import * as fs from "fs"; import * as path from "path"; -import { projectRoot } from "../utils/root.js"; +import { projectRoot } from "../root.js"; export function loadTemplates(name: string) { - const filePath = path.join( - projectRoot, - "..", - "..", - "..", - "fixtures", - name + ".txt", - ); - // eslint-disable-next-line security/detect-non-literal-fs-filename - const fileContent = fs.readFileSync(filePath, "utf8"); + const filePath = path.join(projectRoot, "..", "..", "..", "fixtures", name + ".txt"); + // eslint-disable-next-line security/detect-non-literal-fs-filename + const fileContent = fs.readFileSync(filePath, "utf8"); - const templates = fileContent - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.length > 0); + const templates = fileContent + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); - return templates; + return templates; } diff --git a/packages/npm/goodrouter/src/utils/root.ts b/packages/npm/goodrouter/src/utils/root.ts deleted file mode 100644 index 0abd364..0000000 --- a/packages/npm/goodrouter/src/utils/root.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as path from "path"; -import { fileURLToPath } from "url"; - -export const projectRoot = makeProjectRoot(); - -function makeProjectRoot() { - const dirname = path.dirname(fileURLToPath(new URL(import.meta.url))); - return path.resolve(dirname, "..", ".."); -} diff --git a/packages/npm/goodrouter/src/utils/string.spec.ts b/packages/npm/goodrouter/src/utils/string.spec.ts deleted file mode 100644 index 3ab6b46..0000000 --- a/packages/npm/goodrouter/src/utils/string.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { findCommonPrefixLength } from "./string.js"; - -test("find-common-prefix-length", () => { - assert.equal(findCommonPrefixLength("ab", "abc"), 2); - - assert.equal(findCommonPrefixLength("abc", "abc"), 3); - - assert.equal(findCommonPrefixLength("bc", "abc"), 0); -}); diff --git a/packages/npm/goodrouter/src/utils/string.test.ts b/packages/npm/goodrouter/src/utils/string.test.ts new file mode 100644 index 0000000..cb433b3 --- /dev/null +++ b/packages/npm/goodrouter/src/utils/string.test.ts @@ -0,0 +1,11 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { findCommonPrefixLength } from "./string.js"; + +test("find-common-prefix-length", () => { + assert.equal(findCommonPrefixLength("ab", "abc"), 2); + + assert.equal(findCommonPrefixLength("abc", "abc"), 3); + + assert.equal(findCommonPrefixLength("bc", "abc"), 0); +}); diff --git a/packages/npm/goodrouter/src/utils/string.ts b/packages/npm/goodrouter/src/utils/string.ts index 682d1f5..ab9e67e 100644 --- a/packages/npm/goodrouter/src/utils/string.ts +++ b/packages/npm/goodrouter/src/utils/string.ts @@ -1,15 +1,12 @@ -export function findCommonPrefixLength( - stringLeft: string, - stringRight: string, -) { - const length = Math.min(stringLeft.length, stringRight.length); - let index; - for (index = 0; index < length; index++) { - const charLeft = stringLeft.charAt(index); - const charRight = stringRight.charAt(index); - if (charLeft !== charRight) { - break; - } +export function findCommonPrefixLength(stringLeft: string, stringRight: string) { + const length = Math.min(stringLeft.length, stringRight.length); + let index; + for (index = 0; index < length; index++) { + const charLeft = stringLeft.charAt(index); + const charRight = stringRight.charAt(index); + if (charLeft !== charRight) { + break; } - return index; + } + return index; } diff --git a/packages/npm/goodrouter/tsconfig.json b/packages/npm/goodrouter/tsconfig.json index 461aed9..2f39409 100644 --- a/packages/npm/goodrouter/tsconfig.json +++ b/packages/npm/goodrouter/tsconfig.json @@ -2,10 +2,12 @@ "extends": "@tsconfig/node20", "compilerOptions": { "rootDir": "./src", - "outDir": "./out", + "outDir": "./transpiled", + "declarationDir": "./types", "sourceMap": true, "declaration": true, - "composite": true + "composite": true, + "lib": ["es2023", "DOM"] }, "include": ["src/**/*"] } diff --git a/packages/npm/www/package.json b/packages/npm/www/package.json index 6f9d11c..874eb72 100644 --- a/packages/npm/www/package.json +++ b/packages/npm/www/package.json @@ -9,6 +9,7 @@ "@11ty/eleventy": "^2.0.1", "github-markdown-css": "^5.5.1", "prettier": "^3.2.5", + "rollup": "^4.12.1", "typescript": "^5.4.2" } }