From 1e16c53624b1d30962976756d290ebe9e6859c66 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 2 Sep 2026 16:08:09 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20.tsrx=20route=20modules=20=E2=80=94=20e?= =?UTF-8?q?xport=20analysis=20routes=20by=20filename=20through=20@tsrx/oxc?= =?UTF-8?q?'s=20parser=20facade=20as=20an=20optional=20lazily-loaded=20pee?= =?UTF-8?q?r;=20tsrx=20joins=20the=20Vite=20adapter's=20default=20extensio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/tsrx-route-modules.md | 5 + package.json | 5 + pnpm-lock.yaml | 613 +++++++++++++++++++++++++++++++ src/analyze.ts | 51 ++- src/vite/constants.ts | 2 +- test/analyze.spec.ts | 62 +++- 6 files changed, 734 insertions(+), 4 deletions(-) create mode 100644 .changeset/tsrx-route-modules.md diff --git a/.changeset/tsrx-route-modules.md b/.changeset/tsrx-route-modules.md new file mode 100644 index 0000000..5768265 --- /dev/null +++ b/.changeset/tsrx-route-modules.md @@ -0,0 +1,5 @@ +--- +"filesystem-routing": minor +--- + +Support `.tsrx` route modules. Export analysis routes by filename: `.tsrx` files parse through `@tsrx/oxc/parser` (a new optional peer dependency, loaded lazily only when a `.tsrx` route is scanned — its `ParseResult` mirrors `oxc-parser`, so analysis semantics are unchanged), everything else keeps the exact `oxc-parser` path. The Vite adapter's `DEFAULT_EXTENSIONS` now includes `tsrx`, so `.tsrx` pages participate in routing without an `extensions` override. Scanning a `.tsrx` route without `@tsrx/oxc` installed fails with an error naming the module and the fix. diff --git a/package.json b/package.json index f5d72fc..6c37827 100644 --- a/package.json +++ b/package.json @@ -73,15 +73,20 @@ "radix3": "^1.1.2" }, "peerDependencies": { + "@tsrx/oxc": ">=0.9.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { + "@tsrx/oxc": { + "optional": true + }, "vite": { "optional": true } }, "devDependencies": { "@changesets/cli": "^2.31.1", + "@tsrx/oxc": "^0.9.0", "@types/node": "^22.10.0", "prettier": "^3.4.1", "typescript": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f5e255..08c12b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: '@changesets/cli': specifier: ^2.31.1 version: 2.31.1(@types/node@22.20.1) + '@tsrx/oxc': + specifier: ^0.9.0 + version: 0.9.0 '@types/node': specifier: ^22.10.0 version: 22.20.1 @@ -662,6 +665,283 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.140.0': + resolution: {integrity: sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==} + + '@oxfmt/binding-android-arm-eabi@0.59.0': + resolution: {integrity: sha512-bNTnfbuG7sAwb2PakMNaDukx5kXeW9duXOBeWtTOiLz3fXz3q2DlWguufPZ+c2IHEVrRXHD+M4aUgEWm841LDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.59.0': + resolution: {integrity: sha512-R/Sn7z52QtdAKNqQLLY0EK7hVMjXiz3XUlvoCFCm/60jgIzAnQtiqLKBCFaBkimCQL5rs2ezPMcicpjCsrl54Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.59.0': + resolution: {integrity: sha512-vm/ynUqE4HjC0ZIEjmXv1UJu1/GngccQ+T+TJudTMxUxm6r+GQTg1TO3E5jJfI71pBaXxSzs1+vWHIwuilGHhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.59.0': + resolution: {integrity: sha512-uTtYDpLN/obfKVWGpgEc8BqYlLZBQTPz2uYEvLRy3HPZxjZ34wiFzukUBU2bf64JuCYZI//GTV1EOMmWlPjf/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.59.0': + resolution: {integrity: sha512-e2UnxL/ifStSPy8ffBCDbdy595SYsGy+U1pur4G65TuMmWxAMBzYGG7atZo/3mp515p8rZdsflxVD/E1FAdPLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.59.0': + resolution: {integrity: sha512-LtdeZ1l0urxte3VNi3g8cocZwv1xGM1NKHSgF/fJEEVhyQmlgGh7WFWKFd/pNuO7djfvPNtNO1+MS+FEWkgVSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.59.0': + resolution: {integrity: sha512-dBTciSsj9GTMl7p+h2gMSI0hoPn2ijfc/dUsbnWsP0RbwgPl2r0C/5zkMb3Pb+gGj17LH7f1o4qLo9aes/pAvA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.59.0': + resolution: {integrity: sha512-tXVdJ/JINsNWdponPHN0OuKHtC+HdpyoS9sd6IDPNiiEYsRki8b7tefRZ1iMnRkdbyT4SEbguWsr6o+5awvbPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.59.0': + resolution: {integrity: sha512-RRTq38i2zT5fnw6XGHjvT6w2mh6x/G3m6AZcAZ56OTDTT/lsOeYnG3SVjwmH40z5kPqF+lf+o35e6m6PpKy9Dw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.59.0': + resolution: {integrity: sha512-lD3k7glAJSaXW0D6xzu8VOZbYbosvy+0ktOVkfLEoQF5HJlMSxTQ2KNW0JO+08ccP/1ElOKktVEMI0fqRbVB4w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.59.0': + resolution: {integrity: sha512-WH5ZP1RbuHKBO/yfPRQKpNO/ijHcEDNbnmC4VPf/Bcd3+mbMAZpRiJWRa1PL5bREdIZZHo343mk3sqlc9x7Usw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.59.0': + resolution: {integrity: sha512-743wOiaI9RZY4QVGkWkfGRavD5ZJUJ6gscFjVrVu1dP8AZh9jM+a6v3NhlR+OIzHdS6DhLM96w+gcVskskz7rw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.59.0': + resolution: {integrity: sha512-xjRXQsRnrRZCcCkIEnbd2lmsQNobtwwkJxdy2bWXhZ1lIN0ouZwsBXRsoovW3yATuziAYwr9HMiQuR/Cc75NIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.59.0': + resolution: {integrity: sha512-4hNjqq/Rbr9B+StY9zMMAfm72+mtM4v80xYL5Qkb59Qd72g2vJMI0iFlPj3kf6miMsie/yJ7rt4urJT292HBgA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.59.0': + resolution: {integrity: sha512-NH579iN8EVQYsWowUB8B5vFchcylJtwPVJ7NmUAqEQHNLfhPbDT3K56KrECNAkUN4QpF4qiMgN2vsfZwVvjm7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.59.0': + resolution: {integrity: sha512-mzZy3Z5Aj1D75Aq9FVlmoRQH5ei8Ga4o/NZmlXkKyeZ5EmPrUXRR7c6BMBteV1ZuZ/356UYDuLRLjAMxTDTiBA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.59.0': + resolution: {integrity: sha512-0CpDJ1gE3jN1Gk6xms1Ie6LPfPcOtY4FAtoOmVLHQoAf8DvO2wd0DW2dIX2f7YTp5dxrr0ND8JeUEjm3DP3k5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.59.0': + resolution: {integrity: sha512-zwdKBu3pt87uW0bRcywZb0oGMS7C6n87qogwRYFUgmk44T90ZzYlPjtlFYXs/DnBFrgNCvlHwCuWKfVWLeE7kw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.59.0': + resolution: {integrity: sha512-dUUbZkKgWrmAeI/puzv4bxN8lzcYaFnQVwFTFtwO2Gp8M7lZGSE2qJjC58g518+1bltJ8mizjYwD0BGHym0l/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.24.0': + resolution: {integrity: sha512-C2uMmwK5Bc4ri4ysZ6sA8Rcu+A5zBQTp6ml2u0CLLbRZp4kMFPV3yWk8B5DK9Aw7y9bbjogIm75tUwGLFzlsYQ==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@0.24.0': + resolution: {integrity: sha512-Wgvt/1lRbDxmoNqWQKKcL+UIiqLmdJ+EWLpQa1qzoNVAfNB0PJpa82/8dH1twT/3rSs4zrP5TXPWl4juB71WuQ==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@0.24.0': + resolution: {integrity: sha512-PB1rxII7KV83+ASY4sSkXtqvpij6ME66+QCRL49uksi/ofs2Rf/UVboYr095n0Rkbl2wgvlsHGl6DHC361jQUQ==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@0.24.0': + resolution: {integrity: sha512-xcz3CxKmjTQLREtE/UShh+ruWmm9nAb7UM9zKcD65BStiuYgOakAKkPHl4YS5DztpVcDrE0+HqbOolTlRKYWmw==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@0.24.0': + resolution: {integrity: sha512-A2i6ZGBec3i20S7RaxkgHc6r3HYtD5Mn7j/mb22NkTz14u0JuudvTu6JggAnbGMcv8+dBKQI//EasxSPJLD8pw==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@0.24.0': + resolution: {integrity: sha512-0ZbGd9qRB6zs82moekaKdEvncRANq49EAwfNX62JpTS46feXUhKAuoyVDvZMj6Rywejylrmmu79Wo6faYCo4Ew==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.74.0': + resolution: {integrity: sha512-+gHd12muVI9ZLBaWLPkHt3Fj7jihFjgQ1MGtBaRL8vWrWrI0P7dLUty/cHrHS0oqPYIRgQUJsPu2CExQuMcwNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.74.0': + resolution: {integrity: sha512-xjKdoMB+H+RCOByv/7l7nfIGW9mlOisqYdcyC75UqYuQecLpReAeEYUf2CNeDEI3KtmUgxpRw/+c63y4AeF/Bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.74.0': + resolution: {integrity: sha512-iUK7wvc6sejMKsC+Pt67mntoF5weFcyEunhZfLJceU6gL419mexz5wBkSx/EnkFBExMLNtOi9fnDSc5xfK0IzQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.74.0': + resolution: {integrity: sha512-ggKc/tn5SJ1u2yG2izC6VKODfYKV8MQ2AicJlNzOjuyrC29udvOef6/JzK2r32xqCnBDLFouR1VCkjzEI0/N9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.74.0': + resolution: {integrity: sha512-u++dH/43jy9hTLbneaWlS0gla/Bp1JdwJ2zgevCl8nDFUh6qRCGMxcL0f0lb7By3A9p/LfFr+7cG4HU1hG856g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.74.0': + resolution: {integrity: sha512-Sj1zmtFDVTPeIbIz4ZfcXAbFHqCmKCXdCUlAJzvTF7I20NTH1RDpoF2PhkqNODutJzVhJYmm3oz0GwgY+tvE2g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.74.0': + resolution: {integrity: sha512-//PKyQb/tQXcHArx2f7z+oVI/eMS2Jpv+edNuAtOrgIhWdGcpHxogveAxzmF2rpH1AIHp4Hq04RF/rgJdiICnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.74.0': + resolution: {integrity: sha512-/k1Me+aX2tjuH10K62mLS0y8cLkJBHX6Ce0xPK+eWeel4bSdEGZ8dv4+hYMzg0GrSmjwy4yAYsDPeEeKBft/2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.74.0': + resolution: {integrity: sha512-3tFSjBxc5D8/zvjEuLvOqcA8ZXKD0+6NuaVO/edeamNc49MoAsbfaC9s1UiwODwgF6slGaF8yJA2TPkukd77tg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.74.0': + resolution: {integrity: sha512-9QggtPkSPXOCTu8Szis7auOK/sC7KdQaN+/TujP7YVVhzCAOhgdRfgv8uEz0r2tk5xdgus5rLYUrCDoZNtiRUw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.74.0': + resolution: {integrity: sha512-VM5VPUJ4DJIWiK+AZn8FScUqMr6OFrCAYybMYjEEi7W13ParI64MByiXTkKMqZpBmvQ9zxl9Ebq2VUOiZRJYUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.74.0': + resolution: {integrity: sha512-SaDY1gh9rOA592J54g+gu5hkOFFQBZsMmIYHs+NRHG+Uq0OxtuuCXMWQ3vu1830Eugv5uMXyjG+bv2Z9y4IXjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.74.0': + resolution: {integrity: sha512-ZATQeHZCyr6MbDveg0obD5sxLHFOghtOdC5jwVwYlvFWqtFOxctgFEG6Ef/64hYvZrWyhyCckB10AelqLopeDA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.74.0': + resolution: {integrity: sha512-+aIvJyrdeD7LwCQ2WYLMUWNmnbeDRSPb40aBYtPjD9+PTqUwgJnk+HK5yLfSMeqXrMrDhE9uTmtt2y50tvjhHw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.74.0': + resolution: {integrity: sha512-XyktaR8lhK2qWiCK0Tk8oYD+/cgn+oHA6ddRnxSSXUKkkojkV78CmShZUxQF+yrBFs0SuW+JBOPG6hecyc/iZg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.74.0': + resolution: {integrity: sha512-mzbjrPl4neaVUiJ1fUiEUxTGaSZBoiKtaoB6jmIpz9S+VOA2vDYmJpihQ82w6178V5jxziclTg8Cgj5yF6tTDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.74.0': + resolution: {integrity: sha512-vUAe9okpS2Oa5+lX67lqHMuNUvfkleRKwrUDJ/WJBsgmddvZ1mrsh2HVmuFDRzqFELhaJhFaCNOuR6a7L3rtIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.74.0': + resolution: {integrity: sha512-yyXXJyYYSXL4I8K8jAWjJs+J3fa9gH2JmEbo4f5adm+1tNC9itseicBNuwK7BDHvqQ5J534s+yDULu89vYL2ZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.74.0': + resolution: {integrity: sha512-VTC9IYTIMrVUk/i6Ms1ohzzDKZFkWn0KU2OBbPBzgmVZ2V30165T/zK4LztTr0Xgp9fZ1qQZ1rsZAu/rEmySlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -800,6 +1080,63 @@ packages: cpu: [x64] os: [win32] + '@tsrx/oxc-darwin-arm64@0.9.0': + resolution: {integrity: sha512-ojZfFudFvTxXgsoko1Am0gKHn9lGG+LGztrv/Fm86PrH4dTPIX2z1S47b1KYUCk5gjckBv/P7ydtBaqniQG0tg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@tsrx/oxc-darwin-x64@0.9.0': + resolution: {integrity: sha512-sk900gJjHy40lfrro8Rcv9s7udIkIvEAh0k5z6o1eQCpBRUp/eb2cZ4Blw5QTEw5DkJm/weGZs1O+9jUJ+N+Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@tsrx/oxc-linux-arm64-gnu@0.9.0': + resolution: {integrity: sha512-4S+RdxyEsWhTnd/r2GGjSKMeaTvQpDd850GINYTqgL1P4N/d/T7pkdd9NTpAKwt781ZGqjp7NCtD7ZhJS42g7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tsrx/oxc-linux-arm64-musl@0.9.0': + resolution: {integrity: sha512-kCemJfU0eRqLVFZWP+ShRQCJcAVOCGADHmHFwFqYg3OVdHFxnAZdABISMNjq5yPU6q5gWdX9wOZwEoCNAunrKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tsrx/oxc-linux-x64-gnu@0.9.0': + resolution: {integrity: sha512-qAIoUqM1VA6z2FXpdi25uAOknl5YKa5uWTFz5EAKYg7w/N423JHX6ACesj13hK0Noerb42xiqcGBAxJJrKvreA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tsrx/oxc-linux-x64-musl@0.9.0': + resolution: {integrity: sha512-mkWLa+vRZLdworvZqV4c67WSuzn3Ls1M5+dk6kYtv247ZLeE0TmzMauOc268cYWc9YTDFHCE5JoK8vdSjnUY3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tsrx/oxc-win32-arm64-msvc@0.9.0': + resolution: {integrity: sha512-nb4OBhiiHp3sIEuNaTLofSRNacroG6+ienWd9M9LGAZmNz4aTx6UmgbZ1vxU280Uwe/d12EDUb2adErbcBsmnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@tsrx/oxc-win32-x64-msvc@0.9.0': + resolution: {integrity: sha512-h9KX+oTFeEvRr2BPY+ggXNlDaeuxrDNVEAadNQdc3P/NsTOo9LTrPuC0Ok0ySCcCdka0IhEeHuJ0WKhZESMKFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@tsrx/oxc@0.9.0': + resolution: {integrity: sha512-6hDtHk2Wue75fI1/lupoEgKE2KHTsbkGkQiRFJj3F+E9HZE7kPpkp5BtK3ZRd8MMryM5tuqMWddL/i/KsitQTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -1142,6 +1479,36 @@ packages: resolution: {integrity: sha512-cf1TKZN+zc0lwqigeyXKzKVk5+vNRe99Or2+wVJsXLdlhJgC+gsIniYDfj/ZEBzCJ8Xm21ZG6YtMbR262CcS2w==} engines: {node: ^20.19.0 || >=22.12.0} + oxfmt@0.59.0: + resolution: {integrity: sha512-Xqk6cPZS1yMvVa7OAuenaDZUsgMDutvvbZ9/L5gSvAfW64+WN4HVhgipLj5rVERbYQt8fLs9TopyZ1rU1XEG/w==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint-tsgolint@0.24.0: + resolution: {integrity: sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw==} + hasBin: true + + oxlint@1.74.0: + resolution: {integrity: sha512-odGl2s2x5IOJoj3A0v1k0PGBXVFBZeZ2+AK/+K2MJur7Ghi3bkyX5NuLUWHKqa4js1wjep3hJeuTQJOlr+4+dA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.24.0' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -1180,6 +1547,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} @@ -1315,6 +1685,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} @@ -1999,6 +2373,140 @@ snapshots: '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.140.0': {} + + '@oxfmt/binding-android-arm-eabi@0.59.0': + optional: true + + '@oxfmt/binding-android-arm64@0.59.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.59.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.59.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.59.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.59.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.59.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.59.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.59.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.59.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.59.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.59.0': + optional: true + + '@oxlint-tsgolint/darwin-arm64@0.24.0': + optional: true + + '@oxlint-tsgolint/darwin-x64@0.24.0': + optional: true + + '@oxlint-tsgolint/linux-arm64@0.24.0': + optional: true + + '@oxlint-tsgolint/linux-x64@0.24.0': + optional: true + + '@oxlint-tsgolint/win32-arm64@0.24.0': + optional: true + + '@oxlint-tsgolint/win32-x64@0.24.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.74.0': + optional: true + + '@oxlint/binding-android-arm64@1.74.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.74.0': + optional: true + + '@oxlint/binding-darwin-x64@1.74.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.74.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.74.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.74.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.74.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.74.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.74.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.74.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.74.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.74.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.74.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.74.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.74.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.74.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.74.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.74.0': + optional: true + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -2074,6 +2582,51 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true + '@tsrx/oxc-darwin-arm64@0.9.0': + optional: true + + '@tsrx/oxc-darwin-x64@0.9.0': + optional: true + + '@tsrx/oxc-linux-arm64-gnu@0.9.0': + optional: true + + '@tsrx/oxc-linux-arm64-musl@0.9.0': + optional: true + + '@tsrx/oxc-linux-x64-gnu@0.9.0': + optional: true + + '@tsrx/oxc-linux-x64-musl@0.9.0': + optional: true + + '@tsrx/oxc-win32-arm64-msvc@0.9.0': + optional: true + + '@tsrx/oxc-win32-x64-msvc@0.9.0': + optional: true + + '@tsrx/oxc@0.9.0': + dependencies: + '@oxc-project/types': 0.140.0 + oxfmt-current: oxfmt@0.59.0 + oxlint-current: oxlint@1.74.0(oxlint-tsgolint@0.24.0) + oxlint-tsgolint: 0.24.0 + pathe: 2.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@tsrx/oxc-darwin-arm64': 0.9.0 + '@tsrx/oxc-darwin-x64': 0.9.0 + '@tsrx/oxc-linux-arm64-gnu': 0.9.0 + '@tsrx/oxc-linux-arm64-musl': 0.9.0 + '@tsrx/oxc-linux-x64-gnu': 0.9.0 + '@tsrx/oxc-linux-x64-musl': 0.9.0 + '@tsrx/oxc-win32-arm64-msvc': 0.9.0 + '@tsrx/oxc-win32-x64-msvc': 0.9.0 + transitivePeerDependencies: + - svelte + - vite-plus + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -2456,6 +3009,62 @@ snapshots: '@oxc-parser/binding-win32-ia32-msvc': 0.139.0 '@oxc-parser/binding-win32-x64-msvc': 0.139.0 + oxfmt@0.59.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.59.0 + '@oxfmt/binding-android-arm64': 0.59.0 + '@oxfmt/binding-darwin-arm64': 0.59.0 + '@oxfmt/binding-darwin-x64': 0.59.0 + '@oxfmt/binding-freebsd-x64': 0.59.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.59.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.59.0 + '@oxfmt/binding-linux-arm64-gnu': 0.59.0 + '@oxfmt/binding-linux-arm64-musl': 0.59.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.59.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.59.0 + '@oxfmt/binding-linux-riscv64-musl': 0.59.0 + '@oxfmt/binding-linux-s390x-gnu': 0.59.0 + '@oxfmt/binding-linux-x64-gnu': 0.59.0 + '@oxfmt/binding-linux-x64-musl': 0.59.0 + '@oxfmt/binding-openharmony-arm64': 0.59.0 + '@oxfmt/binding-win32-arm64-msvc': 0.59.0 + '@oxfmt/binding-win32-ia32-msvc': 0.59.0 + '@oxfmt/binding-win32-x64-msvc': 0.59.0 + + oxlint-tsgolint@0.24.0: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.24.0 + '@oxlint-tsgolint/darwin-x64': 0.24.0 + '@oxlint-tsgolint/linux-arm64': 0.24.0 + '@oxlint-tsgolint/linux-x64': 0.24.0 + '@oxlint-tsgolint/win32-arm64': 0.24.0 + '@oxlint-tsgolint/win32-x64': 0.24.0 + + oxlint@1.74.0(oxlint-tsgolint@0.24.0): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.74.0 + '@oxlint/binding-android-arm64': 1.74.0 + '@oxlint/binding-darwin-arm64': 1.74.0 + '@oxlint/binding-darwin-x64': 1.74.0 + '@oxlint/binding-freebsd-x64': 1.74.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.74.0 + '@oxlint/binding-linux-arm-musleabihf': 1.74.0 + '@oxlint/binding-linux-arm64-gnu': 1.74.0 + '@oxlint/binding-linux-arm64-musl': 1.74.0 + '@oxlint/binding-linux-ppc64-gnu': 1.74.0 + '@oxlint/binding-linux-riscv64-gnu': 1.74.0 + '@oxlint/binding-linux-riscv64-musl': 1.74.0 + '@oxlint/binding-linux-s390x-gnu': 1.74.0 + '@oxlint/binding-linux-x64-gnu': 1.74.0 + '@oxlint/binding-linux-x64-musl': 1.74.0 + '@oxlint/binding-openharmony-arm64': 1.74.0 + '@oxlint/binding-win32-arm64-msvc': 1.74.0 + '@oxlint/binding-win32-ia32-msvc': 1.74.0 + '@oxlint/binding-win32-x64-msvc': 1.74.0 + oxlint-tsgolint: 0.24.0 + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -2484,6 +3093,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} picocolors@1.1.1: {} @@ -2606,6 +3217,8 @@ snapshots: tinypool@1.1.1: {} + tinypool@2.1.0: {} + tinyrainbow@1.2.0: {} tinyspy@3.0.2: {} diff --git a/src/analyze.ts b/src/analyze.ts index f54b79d..80378bb 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -1,8 +1,43 @@ import fs from "node:fs"; +import { createRequire } from "node:module"; import { parseSync, type StaticExportEntry } from "oxc-parser"; export type { StaticExportEntry }; +/** + * `oxc-parser` cannot read TSRX syntax, so `.tsrx` route modules go through + * the official TSRX oxc integration instead. `@tsrx/oxc/parser` mirrors the + * `oxc-parser` API (same `ParseResult` shape, same static-export entries), so + * the analysis below is parser-agnostic. The package is an optional peer: + * it is loaded lazily, only when a `.tsrx` route module is actually scanned. + */ +type ParseFn = ( + filename: string, + sourceText: string, + options: { lang: string } +) => ReturnType; + +let tsrxParseSync: ParseFn | undefined; + +function loadTsrxParser(src: string): ParseFn { + if (!tsrxParseSync) { + try { + const facade = createRequire(import.meta.url)("@tsrx/oxc/parser") as { + parseSync: ParseFn; + }; + tsrxParseSync = facade.parseSync; + } catch (cause) { + throw new Error( + `Cannot analyze the TSRX route module ${src}: analyzing .tsrx exports requires ` + + `the optional peer dependency @tsrx/oxc. Install it (npm i -D @tsrx/oxc) ` + + `or keep route modules in .ts/.tsx.`, + { cause } + ); + } + } + return tsrxParseSync; +} + /** * Analyze a route module's static exports. * @@ -11,7 +46,21 @@ export type { StaticExportEntry }; * a compiler that already performs export analysis can provide it instead. */ export function analyzeModule(src: string): StaticExportEntry[] { - const result = parseSync(src, fs.readFileSync(src, "utf-8"), { lang: "tsx" }); + const source = fs.readFileSync(src, "utf-8"); + let result: ReturnType; + if (src.endsWith(".tsrx")) { + const parse = loadTsrxParser(src); + // The TSRX facade reports most syntax errors through `result.errors` but + // throws on some malformed sources; normalize both into the same shape. + try { + result = parse(src, source, { lang: "tsrx" }); + } catch (cause) { + const message = cause instanceof Error ? cause.message : String(cause); + throw new SyntaxError(`Failed to parse ${src}:\n${message}`, { cause }); + } + } else { + result = parseSync(src, source, { lang: "tsx" }); + } const error = result.errors[0]; if (error) throw new SyntaxError(`Failed to parse ${src}:\n${error.codeframe || error.message}`); diff --git a/src/vite/constants.ts b/src/vite/constants.ts index 0b6aca0..693ffa3 100644 --- a/src/vite/constants.ts +++ b/src/vite/constants.ts @@ -6,4 +6,4 @@ */ export const moduleId = "virtual:file-routes"; -export const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"]; +export const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx", "tsrx"]; diff --git a/test/analyze.spec.ts b/test/analyze.spec.ts index 9426984..a2270bf 100644 --- a/test/analyze.spec.ts +++ b/test/analyze.spec.ts @@ -8,9 +8,9 @@ import { PageFileSystemRouter } from "../src/convention.ts"; const temporaryDirectories: string[] = []; -function writeRoute(source: string) { +function writeRoute(source: string, name = "route.tsx") { const directory = fs.mkdtempSync(path.join(os.tmpdir(), "file-routes-")); - const filename = path.join(directory, "route.tsx"); + const filename = path.join(directory, name); temporaryDirectories.push(directory); fs.writeFileSync(filename, source); return filename; @@ -82,4 +82,62 @@ describe("analyzeModule", () => { expect(router.toRoute(route)?.$component?.pick).toEqual(["default", "$css"]); }); + + it("returns runtime exports from TSRX modules", () => { + const route = writeRoute( + ` + export type TypeOnly = string; + export const route = { + preload: () => {} + }; + export function GET() {} + export default function Route(props: { title?: string }) @{ + const label = props.title ?? "TSRX"; +
+ + @if (label.length > 0) { +

{label}

+ } +
+ } + `, + "route.tsrx" + ); + + const exports = analyzeModule(route); + + expect( + exports.map(entry => entry.exportName.name ?? entry.exportName.kind.toLowerCase()) + ).toEqual(["route", "GET", "default"]); + expect(exports.every(entry => !entry.isType)).toBe(true); + }); + + it("applies the page convention to TSRX modules", () => { + const route = writeRoute( + ` + export const route = { preload: () => {} }; + export default function Route() @{ +
+ } + `, + "route.tsrx" + ); + const router = new PageFileSystemRouter({ + dir: path.dirname(route), + extensions: ["tsx", "tsrx"] + }); + + const entry = router.toRoute(route); + expect(entry?.page).toBe(true); + expect(entry?.$component?.pick).toEqual(["default", "$css"]); + expect(entry?.$$route?.pick).toEqual(["route"]); + }); + + it("throws on invalid TSRX route syntax", () => { + const route = writeRoute("export default function Route() @{", "route.tsrx"); + + expect(() => analyzeModule(route)).toThrow(`Failed to parse ${route}`); + }); });