diff --git a/config/rslib.config.ts b/config/rslib.config.ts
index b005996..44553e2 100644
--- a/config/rslib.config.ts
+++ b/config/rslib.config.ts
@@ -27,8 +27,8 @@ export const pluginCleanTscCache: RsbuildPlugin = {
setup(api) {
api.onBeforeBuild(() => {
const tsbuildinfo = path.join(
- api.context.rootPath,
- 'tsconfig.tsbuildinfo',
+ api.context.rootPath,
+ 'tsconfig.tsbuildinfo'
);
if (fs.existsSync(tsbuildinfo)) {
fs.rmSync(tsbuildinfo);
@@ -42,8 +42,8 @@ export const esmConfig: LibConfig = {
syntax: 'es2021',
shims: {
esm: {
- __dirname: true
- }
+ __dirname: true,
+ },
},
dts: {
build: true,
@@ -51,10 +51,6 @@ export const esmConfig: LibConfig = {
plugins: [pluginCleanTscCache],
output: {
minify: nodeMinifyConfig,
- externals: {
- '@babel/traverse': 'commonjs @babel/traverse',
- '@babel/generator': 'commonjs @babel/generator',
- }
},
};
diff --git a/package.json b/package.json
index 0ff1577..c7d481c 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
"test": "rstest run",
"test:watch": "rstest watch",
"test:coverage": "rstest run --coverage",
+ "bench:yuku": "node scripts/benchmark-yuku.mjs --compare-head",
"test:core": "rstest run -c ./rstest.config.ts",
"test:core:watch": "rstest watch -c ./rstest.config.ts",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"",
@@ -64,16 +65,9 @@
"release:local": "pnpm build && changeset version && changeset publish && git add . && git commit -m \"chore: version packages\" && git push && git push --tags"
},
"dependencies": {
- "@babel/core": "^7.28.6",
- "@babel/generator": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6",
"@react-router/node": "^7.13.0",
"@remix-run/node-fetch-server": "^0.13.0",
"@rspack/plugin-react-refresh": "^2.0.2",
- "babel-dead-code-elimination": "^1.0.12",
- "esbuild": "^0.27.2",
"execa": "^9.6.1",
"fs-extra": "11.3.3",
"isbot": "5.1.34",
@@ -81,7 +75,10 @@
"jsesc": "^3.1.0",
"pathe": "^2.0.3",
"react-refresh": "^0.18.0",
- "rspack-plugin-virtual-module": "^1.0.1"
+ "rspack-plugin-virtual-module": "^1.0.1",
+ "yuku-analyzer": "0.5.38",
+ "yuku-codegen": "0.5.38",
+ "yuku-parser": "0.5.38"
},
"devDependencies": {
"@changesets/cli": "^2.29.8",
@@ -90,18 +87,14 @@
"@rsbuild/core": "2.0.15",
"@rslib/core": "^0.22.1",
"@rspack/core": "2.0.8",
- "@swc/helpers": "^0.5.23",
"@rstest/core": "^0.8.1",
"@rstest/coverage-istanbul": "^0.2.0",
- "@types/babel__core": "^7.20.5",
- "@types/babel__generator": "^7.27.0",
- "@types/babel__traverse": "^7.28.0",
+ "@swc/helpers": "^0.5.23",
"@types/fs-extra": "11.0.4",
"@types/jsesc": "^3.0.3",
"@types/node": "^25.0.10",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
- "es-module-lexer": "1.7.0",
"kill-port": "^2.0.1",
"pkg-pr-new": "^0.0.75",
"playwright": "^1.58.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 633631e..9731030 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,21 +11,6 @@ importers:
.:
dependencies:
- '@babel/core':
- specifier: ^7.28.6
- version: 7.28.6
- '@babel/generator':
- specifier: ^7.28.6
- version: 7.28.6
- '@babel/parser':
- specifier: ^7.28.6
- version: 7.28.6
- '@babel/traverse':
- specifier: ^7.28.6
- version: 7.28.6
- '@babel/types':
- specifier: ^7.28.6
- version: 7.28.6
'@react-router/node':
specifier: ^7.13.0
version: 7.13.0(react-router@7.13.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3)
@@ -34,13 +19,7 @@ importers:
version: 0.13.0
'@rspack/plugin-react-refresh':
specifier: ^2.0.2
- version: 2.0.2(@rspack/core@2.0.8(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-refresh@0.18.0)
- babel-dead-code-elimination:
- specifier: ^1.0.12
- version: 1.0.12
- esbuild:
- specifier: ^0.27.2
- version: 0.27.2
+ version: 2.0.2(@rspack/core@2.0.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23))(react-refresh@0.18.0)
execa:
specifier: ^9.6.1
version: 9.6.1
@@ -65,6 +44,15 @@ importers:
rspack-plugin-virtual-module:
specifier: ^1.0.1
version: 1.0.1
+ yuku-analyzer:
+ specifier: 0.5.38
+ version: 0.5.38
+ yuku-codegen:
+ specifier: 0.5.38
+ version: 0.5.38
+ yuku-parser:
+ specifier: 0.5.38
+ version: 0.5.38
devDependencies:
'@changesets/cli':
specifier: ^2.29.8
@@ -80,7 +68,7 @@ importers:
version: 2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
'@rslib/core':
specifier: ^0.22.1
- version: 0.22.1(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0)(typescript@5.9.3)
+ version: 0.22.1(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)(typescript@5.9.3)
'@rspack/core':
specifier: 2.0.8
version: 2.0.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23)
@@ -93,15 +81,6 @@ importers:
'@swc/helpers':
specifier: ^0.5.23
version: 0.5.23
- '@types/babel__core':
- specifier: ^7.20.5
- version: 7.20.5
- '@types/babel__generator':
- specifier: ^7.27.0
- version: 7.27.0
- '@types/babel__traverse':
- specifier: ^7.28.0
- version: 7.28.0
'@types/fs-extra':
specifier: 11.0.4
version: 11.0.4
@@ -117,9 +96,6 @@ importers:
'@types/react-dom':
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.10)
- es-module-lexer:
- specifier: 1.7.0
- version: 1.7.0
kill-port:
specifier: ^2.0.1
version: 2.0.1
@@ -5144,6 +5120,174 @@ packages:
'@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
+ '@yuku-analyzer/binding-darwin-arm64@0.5.38':
+ resolution: {integrity: sha512-ReQ6gxvR+fpPzaWXgovO/NPwUNL2MnllZlX3mEtUl3F7lC7ccLMzTJiYNSp8XBqqJtWW5N+jVQk744V5Y1IAgw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@yuku-analyzer/binding-darwin-x64@0.5.38':
+ resolution: {integrity: sha512-aG4J3j+rQ7UDR+BfY2ku3iurq2wCFJC3edbZCTztbkHkB6L7ScdwpVlJdIczDsxI6+U2J+nwB50VtW3asZlQow==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@yuku-analyzer/binding-freebsd-x64@0.5.38':
+ resolution: {integrity: sha512-3syI009LnQBNC+RdpEZZAP7P+rVe6Q2FQY3ZHlX5ejRb+eNjWqHoZbOSlqaON7WAOf1JQe5iLlEkMKZVBK5OiQ==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@yuku-analyzer/binding-linux-arm-gnu@0.5.38':
+ resolution: {integrity: sha512-DjHt+fDqYnptWXLGCbIL4QW2pDTGzmzi45qMz063h+2PoY7xhUmGCkqTm8Teh3IZmnm2J2ZIz9HJNHm+VOujmg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@yuku-analyzer/binding-linux-arm-musl@0.5.38':
+ resolution: {integrity: sha512-4FJgBfmZuUWHzx/OTiBjk6ZmniacT9CawHeOaQBwEgBYmvjBtaOOUtvBqD5lrJKR3VTyLAsW/DF0zHmJvkAPBA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@yuku-analyzer/binding-linux-arm64-gnu@0.5.38':
+ resolution: {integrity: sha512-YMfGT7QSFe9QXwbwmGasuXN62coW6u+TC1jb/NroGaOHsDB79cKeRz+rG+HDppTmNHlYRJfzkg2QucmvpGWTHg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@yuku-analyzer/binding-linux-arm64-musl@0.5.38':
+ resolution: {integrity: sha512-ujtqERprsVGIIQihk6wtsECSdE9XWe5Eij4/2aRbf/v0MilNECIqccC8cFElFNlJxOkZBNAL8UbF2IeCXU5+Tw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@yuku-analyzer/binding-linux-x64-gnu@0.5.38':
+ resolution: {integrity: sha512-hDDWq+CDLxuntgGoitQAaCw/ueDXNhGV0yIaEbZDEv5AvwGPgh09gBF5+yx/5wBdNNRACeYeFyukeJuIFAndZQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@yuku-analyzer/binding-linux-x64-musl@0.5.38':
+ resolution: {integrity: sha512-k8koEM7OacjdoAHDg0T0ZMtHoBAcRqyE8zIvYhjQnZfInyJ0t+WT4oeq/l//YMbcsF4wOg+QvuS+3OutyRcLTA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@yuku-analyzer/binding-win32-arm64@0.5.38':
+ resolution: {integrity: sha512-oLlk9JWH+E0qK+wc/jW2PN0ZdwNdGa0cMPN24hMmhiK6yPSFZNJUG6138XyynYq6iV//TkSzsYbweAjwtzmEAw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@yuku-analyzer/binding-win32-x64@0.5.38':
+ resolution: {integrity: sha512-DthuKpARL1lciV1XQod2r68YgFqZ+JM+oeZ+/umNJ166+HuaO2UwKpQ5h5IO8tC0MPvlLDO+J+er3aNMMrsVTQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@yuku-codegen/binding-darwin-arm64@0.5.38':
+ resolution: {integrity: sha512-2nOLgv6h5pDda+Ykqg2N+tcm+lYEdSIoStVxpUV2IlbNWSg7/q2iCtEl0qWo42o9H6Oivk6c6BftDJdZNsMIbg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@yuku-codegen/binding-darwin-x64@0.5.38':
+ resolution: {integrity: sha512-P1gUksBlW+q7MmOoLKwEkBcJ7sxlO8e4C00dWvuTFrrygTJZJfalN8WZ0DOrjkV06DLy4FIBn7FXdDy+yPbeaw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@yuku-codegen/binding-freebsd-x64@0.5.38':
+ resolution: {integrity: sha512-jsF0g3FYkzeuGijBfTWuPVQo6xMjCWPrQihNjNJdrhObPeMFRW/jCcPsjre20aZBLF5gbzRH9BFaEiGJtXvo0Q==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@yuku-codegen/binding-linux-arm-gnu@0.5.38':
+ resolution: {integrity: sha512-h5jGyr8fQ+zwmf3VzwyS+ltHfzm7iYXzkAy5TWOTT1x5GHGH5tVTBljPAmc/o+T19uj42aQgL5z3pzcMJ9g+MQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@yuku-codegen/binding-linux-arm-musl@0.5.38':
+ resolution: {integrity: sha512-7w5OxDdSBantw40nFOa/5lejp3IPoAgAg3u3uTW95ipL9/cCHSyq8cKtPxSGr+8LHVx0fCO4a2tEB8SFLy8PXQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@yuku-codegen/binding-linux-arm64-gnu@0.5.38':
+ resolution: {integrity: sha512-ZW4WJm4ygtzMv0VWwVvDa2TbkOAywggb6FGD+ZSYUNafGflMhlDvOsRTnNzlEsFqw3uU3RLEU795Ql0o2xrTxA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@yuku-codegen/binding-linux-arm64-musl@0.5.38':
+ resolution: {integrity: sha512-jlZRCKfElmGMFNUweknPdGiqukSi9N4XiS10jJ/ROFg8ND6fk9NOI2r+pr6qN0U2GAkfu2mgT7RFSHHtgtLCIg==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@yuku-codegen/binding-linux-x64-gnu@0.5.38':
+ resolution: {integrity: sha512-RxrCajqnaeJqNzs9RM8Jze/RSk2PQFDRBEkxUovpJPNhiDj5Q4JbBz5ruZBmi7bgwHliFohhqzdReyovRLsvEg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@yuku-codegen/binding-linux-x64-musl@0.5.38':
+ resolution: {integrity: sha512-QzUiOPUICxzM46Au7f2T0rUE7b+gxuDyOETL1Iz8o0JKYLjt04m6dqpo9Ln2tF4jN8RW7flolzDN7gdVQXGQsw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@yuku-codegen/binding-win32-arm64@0.5.38':
+ resolution: {integrity: sha512-NknjHAtzzJKawpMzmJ/XVi/BNk6rGs00GWCBclQUk8XNN0fJ/1urZ8iCibBZwlUjd6Z77GOYsovNU4a/Rh8nBQ==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@yuku-codegen/binding-win32-x64@0.5.38':
+ resolution: {integrity: sha512-OFRc/vNo/3nsX0ARyxpwZPsVzbDA71YyCMhKYqnyeD5OZek0O88PAjtYCg8YrmIuNGLWYE0fvMpsZe51AjePTg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@yuku-parser/binding-darwin-arm64@0.5.38':
+ resolution: {integrity: sha512-Y6hexHekLYsOyPXJwYmLUhbwawYrHx4YfFNB72vyej/CkMtG0RLHpzJKTqAwn6JTR2zdvLx6sV8gx47dAmjWNg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@yuku-parser/binding-darwin-x64@0.5.38':
+ resolution: {integrity: sha512-/Y/GOsBUwLgcHdxzDZ6JoO4iH2NK94wDileNz8h1hPyUEAYPUo6x3+4JXMT7MHJRyoPuHIrQ/p2JZmPdDtjguQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@yuku-parser/binding-freebsd-x64@0.5.38':
+ resolution: {integrity: sha512-OLUvZAx1g+nDg0cPk/QEkOdc6d49DLCkEhsGjqyd3uit69CngK9Fs8154pOZc/3Y2QAy4jQAs8HnnediyIc5Bw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@yuku-parser/binding-linux-arm-gnu@0.5.38':
+ resolution: {integrity: sha512-LcyGYaBuBm1VYKH0qURqKRcMkW0/PaZdfFdTyHaStLNJzYrHzJLBE/wI5dm2q6NEc56NMnFMSrRzw/BUXv4V0g==}
+ cpu: [arm]
+ os: [linux]
+
+ '@yuku-parser/binding-linux-arm-musl@0.5.38':
+ resolution: {integrity: sha512-d6fd0z96mmq85rm1w8+AUURQoW/R7JxNXx61oWusVAC+JdJmK6KUty5r1hTXiLGRAzdkZXeG3ZlnqlzjvC0wnQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@yuku-parser/binding-linux-arm64-gnu@0.5.38':
+ resolution: {integrity: sha512-fz9emPmTQsupJR0H5s/oMHf9JrIMo6qaNXVR9ljY9PFICW0+FD7TMdAwoIN7pK/vZ3AgOKWMZcDjjpoA6qaEZQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@yuku-parser/binding-linux-arm64-musl@0.5.38':
+ resolution: {integrity: sha512-jvFgjPgoUo9kOebQD4mZUyQ2xMrsuOcTuzJ2rWLApqUTpnrOoVwWyL1MKNw3CdkZUh0h/nMoIPQmQXObmSRxNw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@yuku-parser/binding-linux-x64-gnu@0.5.38':
+ resolution: {integrity: sha512-DFmydzH7fHMRlFC82dVbIcPugN8eq83B/t2Zjy3HRLnQWMQXhFvZNiv9NNinT3ccjzGeFKo/V3+N9/tGc98sGQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@yuku-parser/binding-linux-x64-musl@0.5.38':
+ resolution: {integrity: sha512-PTwAGbC5I5Fj6VI38HtI4C3BDrNgpXZdtcK9Um3i/2Tv2R1AintQhIDMyl5ir6NI7AweWl83sHAkO9xAG7cEEw==}
+ cpu: [x64]
+ os: [linux]
+
+ '@yuku-parser/binding-win32-arm64@0.5.38':
+ resolution: {integrity: sha512-3gxfBDo1G70Y1q2Ec8lAYQ2+BV3bA9i74lovmIVRmv6C55aiXfBzrJHwSvsBU/Js1r0MtzT13vdxUdx83BPsuw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@yuku-parser/binding-win32-x64@0.5.38':
+ resolution: {integrity: sha512-CDZz3v7M6+PyZQJjAJFkZURbDmZtaJeOTgrvDudAk5FzMemcfuTJBKZ0hYp3OI/u0va0kDsre3eCRIwg5eMVbA==}
+ cpu: [x64]
+ os: [win32]
+
+ '@yuku-toolchain/types@0.5.37':
+ resolution: {integrity: sha512-yaGadzsSgTqKXUFef9iUBP7tFXdkN+DWcZqU+MvixYajB3luC8HHCDfJZk/Dy/Hb8haAwJ3z0G9g7bjAG4nGJg==}
+
'@zeit/schemas@2.36.0':
resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
@@ -9475,6 +9619,15 @@ packages:
youch@4.1.0-beta.10:
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
+ yuku-analyzer@0.5.38:
+ resolution: {integrity: sha512-uxVIPMomdry2zW2qvPbo44Rj72ucom2atQp6Cf5CCcW2wuLxwdHe9eLEo6qELZutsLNC0zZT+cYswyJ75G9q0g==}
+
+ yuku-codegen@0.5.38:
+ resolution: {integrity: sha512-oaWapF6EiMec8UndXkxVrHiYrDhKEywNTKLoYHLBkbIOxopKd9jBKpLOiYu89NNszuuglGkpQ1z+iuGWYytLPQ==}
+
+ yuku-parser@0.5.38:
+ resolution: {integrity: sha512-u2+4Vv948JFl+AiXWcKNoagrmZDL1jSvwBuRDoZq4pMTO/ZYJZp3lI2PuIXcLW1eL9eGxvBNvB+X5NgWgoFb0A==}
+
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -10387,7 +10540,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.8.1
+ '@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -12107,6 +12260,17 @@ snapshots:
- '@rspack/core'
- webpack
+ '@rslib/core@0.22.1(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)(typescript@5.9.3)':
+ dependencies:
+ '@rsbuild/core': 2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ rsbuild-plugin-dts: 0.22.1(@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(typescript@5.9.3)
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@module-federation/runtime-tools'
+ - '@typescript/native-preview'
+ - core-js
+
'@rslib/core@0.22.1(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0)(typescript@5.9.3)':
dependencies:
'@rsbuild/core': 2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
@@ -12233,7 +12397,7 @@ snapshots:
optionalDependencies:
'@rspack/core': 2.0.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23)
- '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.0.8(@module-federation/runtime-tools@2.5.1)(@swc/helpers@0.5.23))(react-refresh@0.18.0)':
+ '@rspack/plugin-react-refresh@2.0.2(@rspack/core@2.0.8(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(@swc/helpers@0.5.23))(react-refresh@0.18.0)':
dependencies:
react-refresh: 0.18.0
optionalDependencies:
@@ -13236,6 +13400,107 @@ snapshots:
'@xtuc/long@4.2.2': {}
+ '@yuku-analyzer/binding-darwin-arm64@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-darwin-x64@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-freebsd-x64@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-linux-arm-gnu@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-linux-arm-musl@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-linux-arm64-gnu@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-linux-arm64-musl@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-linux-x64-gnu@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-linux-x64-musl@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-win32-arm64@0.5.38':
+ optional: true
+
+ '@yuku-analyzer/binding-win32-x64@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-darwin-arm64@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-darwin-x64@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-freebsd-x64@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm-gnu@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm-musl@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm64-gnu@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-linux-arm64-musl@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-linux-x64-gnu@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-linux-x64-musl@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-win32-arm64@0.5.38':
+ optional: true
+
+ '@yuku-codegen/binding-win32-x64@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-darwin-arm64@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-darwin-x64@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-freebsd-x64@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm-gnu@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm-musl@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm64-gnu@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-linux-arm64-musl@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-linux-x64-gnu@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-linux-x64-musl@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-win32-arm64@0.5.38':
+ optional: true
+
+ '@yuku-parser/binding-win32-x64@0.5.38':
+ optional: true
+
+ '@yuku-toolchain/types@0.5.37': {}
+
'@zeit/schemas@2.36.0': {}
accepts@1.3.8:
@@ -15277,7 +15542,7 @@ snapshots:
webidl-conversions: 8.0.1
whatwg-mimetype: 4.0.0
whatwg-url: 15.1.0
- ws: 8.19.0
+ ws: 8.21.0
xml-name-validator: 5.0.0
transitivePeerDependencies:
- '@noble/hashes'
@@ -16478,6 +16743,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ rsbuild-plugin-dts@0.22.1(@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0))(typescript@5.9.3):
+ dependencies:
+ '@ast-grep/napi': 0.37.0
+ '@rsbuild/core': 2.0.15(@module-federation/runtime-tools@2.5.1(node-fetch@2.7.0(encoding@0.1.13)))(core-js@3.47.0)
+ optionalDependencies:
+ typescript: 5.9.3
+
rsbuild-plugin-dts@0.22.1(@rsbuild/core@2.0.15(@module-federation/runtime-tools@2.5.1)(core-js@3.47.0))(typescript@5.9.3):
dependencies:
'@ast-grep/napi': 0.37.0
@@ -16785,7 +17057,7 @@ snapshots:
dependencies:
'@img/colour': 1.0.0
detect-libc: 2.1.2
- semver: 7.7.3
+ semver: 7.8.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
@@ -17804,4 +18076,52 @@ snapshots:
cookie: 1.1.1
youch-core: 0.3.3
+ yuku-analyzer@0.5.38:
+ dependencies:
+ '@yuku-toolchain/types': 0.5.37
+ optionalDependencies:
+ '@yuku-analyzer/binding-darwin-arm64': 0.5.38
+ '@yuku-analyzer/binding-darwin-x64': 0.5.38
+ '@yuku-analyzer/binding-freebsd-x64': 0.5.38
+ '@yuku-analyzer/binding-linux-arm-gnu': 0.5.38
+ '@yuku-analyzer/binding-linux-arm-musl': 0.5.38
+ '@yuku-analyzer/binding-linux-arm64-gnu': 0.5.38
+ '@yuku-analyzer/binding-linux-arm64-musl': 0.5.38
+ '@yuku-analyzer/binding-linux-x64-gnu': 0.5.38
+ '@yuku-analyzer/binding-linux-x64-musl': 0.5.38
+ '@yuku-analyzer/binding-win32-arm64': 0.5.38
+ '@yuku-analyzer/binding-win32-x64': 0.5.38
+
+ yuku-codegen@0.5.38:
+ dependencies:
+ '@yuku-toolchain/types': 0.5.37
+ optionalDependencies:
+ '@yuku-codegen/binding-darwin-arm64': 0.5.38
+ '@yuku-codegen/binding-darwin-x64': 0.5.38
+ '@yuku-codegen/binding-freebsd-x64': 0.5.38
+ '@yuku-codegen/binding-linux-arm-gnu': 0.5.38
+ '@yuku-codegen/binding-linux-arm-musl': 0.5.38
+ '@yuku-codegen/binding-linux-arm64-gnu': 0.5.38
+ '@yuku-codegen/binding-linux-arm64-musl': 0.5.38
+ '@yuku-codegen/binding-linux-x64-gnu': 0.5.38
+ '@yuku-codegen/binding-linux-x64-musl': 0.5.38
+ '@yuku-codegen/binding-win32-arm64': 0.5.38
+ '@yuku-codegen/binding-win32-x64': 0.5.38
+
+ yuku-parser@0.5.38:
+ dependencies:
+ '@yuku-toolchain/types': 0.5.37
+ optionalDependencies:
+ '@yuku-parser/binding-darwin-arm64': 0.5.38
+ '@yuku-parser/binding-darwin-x64': 0.5.38
+ '@yuku-parser/binding-freebsd-x64': 0.5.38
+ '@yuku-parser/binding-linux-arm-gnu': 0.5.38
+ '@yuku-parser/binding-linux-arm-musl': 0.5.38
+ '@yuku-parser/binding-linux-arm64-gnu': 0.5.38
+ '@yuku-parser/binding-linux-arm64-musl': 0.5.38
+ '@yuku-parser/binding-linux-x64-gnu': 0.5.38
+ '@yuku-parser/binding-linux-x64-musl': 0.5.38
+ '@yuku-parser/binding-win32-arm64': 0.5.38
+ '@yuku-parser/binding-win32-x64': 0.5.38
+
zod@3.25.76: {}
diff --git a/scripts/benchmark-yuku.mjs b/scripts/benchmark-yuku.mjs
new file mode 100644
index 0000000..eb3d21d
--- /dev/null
+++ b/scripts/benchmark-yuku.mjs
@@ -0,0 +1,307 @@
+#!/usr/bin/env node
+import { spawnSync } from 'node:child_process';
+import { mkdir, mkdtemp, readdir, symlink } from 'node:fs/promises';
+import { existsSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { createJiti } from 'jiti';
+
+const iterations = Number(process.env.BENCH_ITERATIONS ?? 250);
+const sampleCount = Number(process.env.BENCH_SAMPLES ?? 24);
+
+const exec = (cmd, args, options = {}) => {
+ const result = spawnSync(cmd, args, {
+ stdio: ['ignore', 'pipe', 'pipe'],
+ encoding: 'utf8',
+ ...options,
+ });
+ if (result.status !== 0) {
+ throw new Error(
+ [`Command failed: ${cmd} ${args.join(' ')}`, result.stdout, result.stderr]
+ .filter(Boolean)
+ .join('\n')
+ );
+ }
+ return result.stdout;
+};
+
+const createOldCheckout = async repoRoot => {
+ const dir = await mkdtemp(path.join(tmpdir(), 'rr-yuku-before-'));
+ const archive = path.join(dir, 'head.tar');
+ exec('git', ['archive', 'HEAD', '-o', archive], { cwd: repoRoot });
+ const checkout = path.join(dir, 'repo');
+ exec('mkdir', ['-p', checkout]);
+ exec('tar', ['-xf', archive, '-C', checkout]);
+ await linkNodeModules(repoRoot, checkout);
+ return checkout;
+};
+
+const linkNodeModules = async (repoRoot, checkout) => {
+ const sourceNodeModules = path.join(repoRoot, 'node_modules');
+ const targetNodeModules = path.join(checkout, 'node_modules');
+ await mkdir(targetNodeModules, { recursive: true });
+
+ for (const entry of await readdir(sourceNodeModules, {
+ withFileTypes: true,
+ })) {
+ if (entry.name === '.pnpm') {
+ continue;
+ }
+ const source = path.join(sourceNodeModules, entry.name);
+ const target = path.join(targetNodeModules, entry.name);
+ if (entry.name.startsWith('@') && entry.isDirectory()) {
+ await mkdir(target, { recursive: true });
+ for (const scoped of await readdir(source)) {
+ const scopedTarget = path.join(target, scoped);
+ if (!existsSync(scopedTarget)) {
+ await symlink(path.join(source, scoped), scopedTarget);
+ }
+ }
+ continue;
+ }
+ if (!existsSync(target)) {
+ await symlink(source, target);
+ }
+ }
+
+ const oldOnlyPackages = [
+ '@babel/core',
+ '@babel/generator',
+ '@babel/parser',
+ '@babel/traverse',
+ '@babel/types',
+ 'babel-dead-code-elimination',
+ 'es-module-lexer',
+ 'esbuild',
+ ];
+ for (const packageName of oldOnlyPackages) {
+ await linkPnpmPackage(sourceNodeModules, targetNodeModules, packageName);
+ }
+};
+
+const linkPnpmPackage = async (
+ sourceNodeModules,
+ targetNodeModules,
+ packageName
+) => {
+ const source = findPnpmPackage(sourceNodeModules, packageName);
+ if (!source) {
+ throw new Error(`Could not find ${packageName} in node_modules/.pnpm`);
+ }
+ const segments = packageName.split('/');
+ const target =
+ segments.length === 1
+ ? path.join(targetNodeModules, packageName)
+ : path.join(targetNodeModules, segments[0], segments[1]);
+ await mkdir(path.dirname(target), { recursive: true });
+ if (!existsSync(target)) {
+ await symlink(source, target);
+ }
+};
+
+const findPnpmPackage = (sourceNodeModules, packageName) => {
+ const pnpmDir = path.join(sourceNodeModules, '.pnpm');
+ const encodedName = packageName.replace('/', '+');
+ const entries = spawnSync(
+ 'find',
+ [pnpmDir, '-maxdepth', '1', '-type', 'd', '-name', `${encodedName}@*`],
+ {
+ encoding: 'utf8',
+ }
+ );
+ const dir = entries.stdout.split('\n').filter(Boolean).sort().at(-1);
+ if (!dir) {
+ return null;
+ }
+ return path.join(dir, 'node_modules', packageName);
+};
+
+const loadModules = async repoRoot => {
+ const jiti = createJiti(pathToFileURL(path.join(repoRoot, 'bench.mjs')).href);
+ return {
+ exportUtils: await jiti.import(path.join(repoRoot, 'src/export-utils.ts')),
+ compiler: await jiti.import(path.join(repoRoot, 'src/babel.ts')),
+ pluginUtils: await jiti.import(path.join(repoRoot, 'src/plugin-utils.ts')),
+ routeChunks: await jiti.import(path.join(repoRoot, 'src/route-chunks.ts')),
+ };
+};
+
+const createSamples = () =>
+ Array.from({ length: sampleCount }, (_, index) => {
+ const shared =
+ index % 3 === 0
+ ? `const shared${index} = (value: number) => value + ${index};`
+ : '';
+ return {
+ path: `/app/routes/bench-${index}.tsx`,
+ code: `
+ import { helper${index} } from "./helpers";
+ import { serverOnly${index} } from "./data.server";
+ ${shared}
+
+ type LoaderData${index} = { value: number };
+
+ export const loader = async () => {
+ return serverOnly${index}();
+ };
+
+ export const action = async () => {
+ return serverOnly${index}();
+ };
+
+ export const clientLoader = async () => {
+ const value = helper${index}(${index});
+ return ${shared ? `shared${index}(value)` : 'value'};
+ };
+
+ export const clientAction = async () => {
+ return helper${index}(${index + 1});
+ };
+
+ export function HydrateFallback() {
+ return
Loading
;
+ }
+
+ export function ErrorBoundary() {
+ return Error
;
+ }
+
+ export default function Route(props: LoaderData${index}) {
+ return {props.value};
+ }
+ `,
+ };
+ });
+
+const hrtimeMs = start => Number(process.hrtime.bigint() - start) / 1e6;
+
+const measure = async fn => {
+ const start = process.hrtime.bigint();
+ await fn();
+ return hrtimeMs(start);
+};
+
+const runForRepo = async (label, repoRoot) => {
+ const { exportUtils, compiler, pluginUtils, routeChunks } =
+ await loadModules(repoRoot);
+ const samples = createSamples();
+
+ for (let i = 0; i < 20; i++) {
+ const sample = samples[i % samples.length];
+ const code = await exportUtils.transformToEsm(sample.code, sample.path);
+ await exportUtils.getExportNames(code);
+ }
+
+ const transformed = new Map();
+ const transformMs = await measure(async () => {
+ for (let i = 0; i < iterations; i++) {
+ const sample = samples[i % samples.length];
+ const code = await exportUtils.transformToEsm(sample.code, sample.path);
+ transformed.set(sample.path, code);
+ }
+ });
+
+ const exportScanMs = await measure(async () => {
+ for (let i = 0; i < iterations; i++) {
+ const sample = samples[i % samples.length];
+ await exportUtils.getExportNames(transformed.get(sample.path));
+ }
+ });
+
+ const routeTransformMs = await measure(async () => {
+ for (let i = 0; i < iterations; i++) {
+ const sample = samples[i % samples.length];
+ const code = transformed.get(sample.path);
+ const ast = compiler.parse(code, { sourceType: 'module' });
+ pluginUtils.removeExports(ast, [
+ 'loader',
+ 'action',
+ 'middleware',
+ 'headers',
+ ]);
+ pluginUtils.transformRoute(ast);
+ pluginUtils.removeUnusedImports(ast);
+ compiler.generate(ast, { sourceMaps: true, filename: sample.path });
+ }
+ });
+
+ const routeChunkMs = await measure(async () => {
+ const cache = new Map();
+ const config = {
+ splitRouteModules: true,
+ appDirectory: '/app',
+ rootRouteFile: 'root.tsx',
+ };
+ for (let i = 0; i < iterations; i++) {
+ const sample = samples[i % samples.length];
+ const code = transformed.get(sample.path);
+ await routeChunks.detectRouteChunksIfEnabled(
+ cache,
+ config,
+ sample.path,
+ code
+ );
+ await routeChunks.getRouteChunkIfEnabled(
+ cache,
+ config,
+ sample.path,
+ 'main',
+ code
+ );
+ await routeChunks.getRouteChunkIfEnabled(
+ cache,
+ config,
+ sample.path,
+ 'clientLoader',
+ code
+ );
+ }
+ });
+
+ return {
+ label,
+ transformMs,
+ exportScanMs,
+ routeTransformMs,
+ routeChunkMs,
+ totalMs: transformMs + exportScanMs + routeTransformMs + routeChunkMs,
+ };
+};
+
+const format = value => value.toFixed(2).padStart(10);
+
+const printComparison = (before, after) => {
+ const rows = [
+ ['transform', before.transformMs, after.transformMs],
+ ['export scan', before.exportScanMs, after.exportScanMs],
+ ['route transform', before.routeTransformMs, after.routeTransformMs],
+ ['route chunks', before.routeChunkMs, after.routeChunkMs],
+ ['total', before.totalMs, after.totalMs],
+ ];
+ console.log(
+ `Benchmark: ${iterations} iterations across ${sampleCount} TSX route samples`
+ );
+ console.log(`Node: ${process.version}`);
+ console.log('');
+ console.log('metric before ms after ms speedup');
+ for (const [name, oldMs, newMs] of rows) {
+ const speedup = oldMs / newMs;
+ console.log(
+ `${name.padEnd(18)}${format(oldMs)}${format(newMs)}${`${speedup.toFixed(2)}x`.padStart(10)}`
+ );
+ }
+};
+
+const repoRoot = process.cwd();
+const compareHead = process.argv.includes('--compare-head');
+
+if (compareHead) {
+ const oldRepo = await createOldCheckout(repoRoot);
+ const before = await runForRepo('before', oldRepo);
+ const after = await runForRepo('after', repoRoot);
+ printComparison(before, after);
+} else {
+ const result = await runForRepo('current', repoRoot);
+ console.log(JSON.stringify(result, null, 2));
+}
diff --git a/src/babel.ts b/src/babel.ts
index 4c52d9c..c8559a6 100644
--- a/src/babel.ts
+++ b/src/babel.ts
@@ -1,18 +1,55 @@
-import type { types as Babel } from '@babel/core';
-import generatorPkg from '@babel/generator';
-import { type ParseResult, parse } from '@babel/parser';
-/* eslint-disable @typescript-eslint/consistent-type-imports */
-import type { NodePath } from '@babel/traverse';
-import traversePkg from '@babel/traverse';
-import * as t from '@babel/types';
+import {
+ parse as yukuParse,
+ walk,
+ type ParseOptions,
+ type ParseResult,
+} from 'yuku-parser';
+import { strip } from 'yuku-codegen';
-// Babel packages are CommonJS. Depending on the bundler/runtime interop mode,
-// their "default" may either be the exported function or a module namespace.
-// We normalize to always get the callable function.
-const traverse: typeof import('@babel/traverse').default =
- (traversePkg as any).default ?? (traversePkg as any);
-const generate: typeof import('@babel/generator').default =
- (generatorPkg as any).default ?? (generatorPkg as any);
+export type Babel = any;
+export type NodePath = T;
-export { traverse, generate, parse, t };
-export type { Babel, NodePath, ParseResult };
+export const parse = (
+ code: string,
+ options: ParseOptions = {}
+): ParseResult => {
+ const result = yukuParse(code, {
+ sourceType: options.sourceType ?? 'module',
+ lang: options.lang ?? 'tsx',
+ preserveParens: true,
+ });
+ const errors = result.diagnostics.filter(
+ diagnostic => diagnostic.severity === 'error'
+ );
+ if (errors.length > 0) {
+ throw new Error(errors.map(error => error.message).join('\n'));
+ }
+ return result;
+};
+
+export const traverse: typeof walk = walk;
+
+export const generate = (
+ ast: ParseResult | { type: 'Program' },
+ options: {
+ sourceMaps?: boolean;
+ filename?: string;
+ sourceFileName?: string;
+ } = {}
+): { code: string; map: any } => {
+ const result = 'program' in ast ? ast : { program: ast, lineStarts: [] };
+ const generated = strip(result.program as any, {
+ comments: 'some',
+ sourceMaps: options.sourceMaps
+ ? {
+ lineStarts: result.lineStarts,
+ file: options.filename,
+ sourceFileName: options.sourceFileName,
+ }
+ : undefined,
+ });
+ return { code: generated.code, map: generated.map as any };
+};
+
+export const t = {};
+export type { ParseResult };
diff --git a/src/export-utils.ts b/src/export-utils.ts
index d5f67d4..2fa300d 100644
--- a/src/export-utils.ts
+++ b/src/export-utils.ts
@@ -1,58 +1,174 @@
import { readFile } from 'node:fs/promises';
-import { extname } from 'pathe';
-import * as esbuild from 'esbuild';
-import { init, parse as parseExports } from 'es-module-lexer';
-import { JS_LOADERS } from './constants.js';
+import { langFromPath, parse } from 'yuku-parser';
+import { strip } from 'yuku-codegen';
-const getEsbuildLoader = (resourcePath: string): esbuild.Loader => {
- const ext = extname(resourcePath) as keyof typeof JS_LOADERS;
- return JS_LOADERS[ext] ?? 'js';
+type AnyNode = Record;
+
+const parseProgram = (code: string, resourcePath?: string) => {
+ const result = parse(code, {
+ sourceType: 'module',
+ lang: resourcePath ? langFromPath(resourcePath) : 'tsx',
+ preserveParens: true,
+ });
+ const errors = result.diagnostics.filter(
+ diagnostic => diagnostic.severity === 'error'
+ );
+ if (errors.length > 0) {
+ throw new Error(errors.map(error => error.message).join('\n'));
+ }
+ return result.program as AnyNode;
+};
+
+const getIdentifierNamesFromPattern = (
+ pattern: AnyNode | null | undefined,
+ names: string[] = []
+): string[] => {
+ if (!pattern) {
+ return names;
+ }
+ if (pattern.type === 'Identifier') {
+ names.push(pattern.name);
+ return names;
+ }
+ if (pattern.type === 'RestElement') {
+ return getIdentifierNamesFromPattern(pattern.argument, names);
+ }
+ if (pattern.type === 'AssignmentPattern') {
+ return getIdentifierNamesFromPattern(pattern.left, names);
+ }
+ if (pattern.type === 'ArrayPattern') {
+ for (const element of pattern.elements ?? []) {
+ getIdentifierNamesFromPattern(element, names);
+ }
+ return names;
+ }
+ if (pattern.type === 'ObjectPattern') {
+ for (const property of pattern.properties ?? []) {
+ if (property.type === 'RestElement') {
+ getIdentifierNamesFromPattern(property.argument, names);
+ } else {
+ getIdentifierNamesFromPattern(property.value, names);
+ }
+ }
+ }
+ return names;
+};
+
+const getExportedName = (node: AnyNode): string | null => {
+ if (!node) {
+ return null;
+ }
+ if (node.type === 'Identifier') {
+ return node.name;
+ }
+ if (node.type === 'Literal' || node.type === 'StringLiteral') {
+ return String(node.value);
+ }
+ return null;
+};
+
+const isTypeOnlyExport = (node: AnyNode): boolean =>
+ node.exportKind === 'type' || node.type === 'TSExportAssignment';
+
+const collectExportNames = (program: AnyNode): string[] => {
+ const exportNames = new Set();
+ for (const statement of program.body ?? []) {
+ if (statement.type === 'ExportAllDeclaration') {
+ const exported = getExportedName(statement.exported);
+ if (exported) {
+ exportNames.add(exported);
+ }
+ continue;
+ }
+
+ if (statement.type === 'ExportDefaultDeclaration') {
+ exportNames.add('default');
+ continue;
+ }
+
+ if (statement.type !== 'ExportNamedDeclaration') {
+ continue;
+ }
+ if (isTypeOnlyExport(statement)) {
+ continue;
+ }
+
+ const declaration = statement.declaration;
+ if (declaration) {
+ if (declaration.type === 'VariableDeclaration') {
+ for (const declarator of declaration.declarations ?? []) {
+ for (const name of getIdentifierNamesFromPattern(declarator.id)) {
+ exportNames.add(name);
+ }
+ }
+ } else if (
+ (declaration.type === 'FunctionDeclaration' ||
+ declaration.type === 'ClassDeclaration') &&
+ declaration.id?.name
+ ) {
+ exportNames.add(declaration.id.name);
+ }
+ continue;
+ }
+
+ for (const specifier of statement.specifiers ?? []) {
+ if (specifier.exportKind === 'type') {
+ continue;
+ }
+ const exported = getExportedName(specifier.exported);
+ if (exported) {
+ exportNames.add(exported);
+ }
+ }
+ }
+ return Array.from(exportNames);
+};
+
+const collectExportAllModules = (program: AnyNode): string[] => {
+ const modules: string[] = [];
+ for (const statement of program.body ?? []) {
+ if (statement.type !== 'ExportAllDeclaration') {
+ continue;
+ }
+ if (statement.exported) {
+ continue;
+ }
+ const source = statement.source?.value;
+ if (typeof source === 'string') {
+ modules.push(source);
+ }
+ }
+ return modules;
};
export const transformToEsm = async (
code: string,
resourcePath: string
): Promise => {
- return (
- await esbuild.transform(code, {
- jsx: 'automatic',
- format: 'esm',
- platform: 'neutral',
- loader: getEsbuildLoader(resourcePath),
- })
- ).code;
+ const result = parse(code, {
+ sourceType: 'module',
+ lang: langFromPath(resourcePath),
+ preserveParens: true,
+ });
+ const transformed = strip(result.program, { comments: 'some' });
+ if (transformed.errors.length > 0) {
+ throw new Error(transformed.errors.map(error => error.message).join('\n'));
+ }
+ return transformed.code;
};
export const getExportNames = async (code: string): Promise => {
- await init;
- const [, exportSpecifiers] = await parseExports(code);
- return Array.from(
- new Set(exportSpecifiers.map(specifier => specifier.n).filter(Boolean))
- );
+ return collectExportNames(parseProgram(code));
};
export const getExportNamesAndExportAll = async (
code: string
): Promise<{ exportNames: string[]; exportAllModules: string[] }> => {
- await init;
- const [imports, exportSpecifiers] = await parseExports(code);
- const exportNames = new Set();
- for (const specifier of exportSpecifiers) {
- if (specifier.n) {
- exportNames.add(specifier.n);
- }
- }
- const exportAllModules: string[] = [];
- for (const entry of imports) {
- if (!entry.n) {
- continue;
- }
- const statement = code.slice(entry.ss, entry.se);
- if (/^\s*export\s*\*\s*from\s*['"]/.test(statement)) {
- exportAllModules.push(entry.n);
- }
- }
- return { exportNames: Array.from(exportNames), exportAllModules };
+ const program = parseProgram(code);
+ return {
+ exportNames: collectExportNames(program),
+ exportAllModules: collectExportAllModules(program),
+ };
};
export const getRouteModuleExports = async (
diff --git a/src/index.ts b/src/index.ts
index 651343a..67ae7d9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1688,8 +1688,8 @@ export const pluginReactRouter = (
// In SPA mode, server-only route exports are invalid (except root `loader`),
// and `HydrateFallback` is only allowed on the root route.
//
- // Important: `es-module-lexer` can't parse TS/TSX directly, so we scan
- // the ESBuild-transformed JS output.
+ // Scan the Yuku-stripped output so TypeScript-only exports do not
+ // participate in route export validation.
if (args.environment.name === 'web' && !ssr && isSpaMode) {
const exportNames = await getExportNames(code);
diff --git a/src/plugin-utils.ts b/src/plugin-utils.ts
index 519c18a..4c9ab61 100644
--- a/src/plugin-utils.ts
+++ b/src/plugin-utils.ts
@@ -1,24 +1,23 @@
-import {
- deadCodeElimination,
- findReferencedIdentifiers,
-} from 'babel-dead-code-elimination';
import { normalize } from 'pathe';
import { existsSync } from 'node:fs';
-import type { Babel, NodePath, ParseResult } from './babel.js';
-import { t, traverse } from './babel.js';
+import { walk, type ParseResult } from 'yuku-parser';
import { NAMED_COMPONENT_EXPORTS, JS_EXTENSIONS } from './constants.js';
+type AnyNode = Record;
+
+const getProgram = (ast: ParseResult | AnyNode): AnyNode =>
+ (ast as ParseResult).program ?? ast;
+
export function validateDestructuredExports(
- id: Babel.ArrayPattern | Babel.ObjectPattern,
+ id: AnyNode,
exportsToRemove: string[]
): void {
if (id.type === 'ArrayPattern') {
- for (const element of id.elements) {
+ for (const element of id.elements ?? []) {
if (!element) {
continue;
}
- // [ foo ]
if (
element.type === 'Identifier' &&
exportsToRemove.includes(element.name)
@@ -26,7 +25,6 @@ export function validateDestructuredExports(
throw invalidDestructureError(element.name);
}
- // [ ...foo ]
if (
element.type === 'RestElement' &&
element.argument.type === 'Identifier' &&
@@ -35,8 +33,6 @@ export function validateDestructuredExports(
throw invalidDestructureError(element.argument.name);
}
- // [ [...] ]
- // [ {...} ]
if (element.type === 'ArrayPattern' || element.type === 'ObjectPattern') {
validateDestructuredExports(element, exportsToRemove);
}
@@ -44,16 +40,12 @@ export function validateDestructuredExports(
}
if (id.type === 'ObjectPattern') {
- for (const property of id.properties) {
+ for (const property of id.properties ?? []) {
if (!property) {
continue;
}
- if (
- property.type === 'ObjectProperty' &&
- property.key.type === 'Identifier'
- ) {
- // { foo }
+ if (property.type === 'Property') {
if (
property.value.type === 'Identifier' &&
exportsToRemove.includes(property.value.name)
@@ -61,8 +53,6 @@ export function validateDestructuredExports(
throw invalidDestructureError(property.value.name);
}
- // { foo: [...] }
- // { foo: {...} }
if (
property.value.type === 'ArrayPattern' ||
property.value.type === 'ObjectPattern'
@@ -71,7 +61,6 @@ export function validateDestructuredExports(
}
}
- // { ...foo }
if (
property.type === 'RestElement' &&
property.argument.type === 'Identifier' &&
@@ -87,14 +76,12 @@ export function invalidDestructureError(name: string): Error {
return new Error(`Cannot remove destructured export "${name}"`);
}
-export function toFunctionExpression(decl: Babel.FunctionDeclaration): any {
- return t.functionExpression(
- decl.id,
- decl.params,
- decl.body,
- decl.generator,
- decl.async
- );
+export function toFunctionExpression(decl: AnyNode): AnyNode {
+ return {
+ ...decl,
+ type: 'FunctionExpression',
+ declare: undefined,
+ };
}
export function combineURLs(baseURL: string, relativeURL: string): string {
@@ -173,278 +160,522 @@ export function generateWithProps() {
`;
}
+const removeFromArray = (array: T[], value: T): void => {
+ const index = array.indexOf(value);
+ if (index >= 0) {
+ array.splice(index, 1);
+ }
+};
+
+const getPatternIdentifierNames = (
+ pattern: AnyNode | null | undefined,
+ names = new Set()
+): Set => {
+ if (!pattern) {
+ return names;
+ }
+ if (pattern.type === 'Identifier') {
+ names.add(pattern.name);
+ return names;
+ }
+ if (pattern.type === 'RestElement') {
+ return getPatternIdentifierNames(pattern.argument, names);
+ }
+ if (pattern.type === 'AssignmentPattern') {
+ return getPatternIdentifierNames(pattern.left, names);
+ }
+ if (pattern.type === 'ArrayPattern') {
+ for (const element of pattern.elements ?? []) {
+ getPatternIdentifierNames(element, names);
+ }
+ return names;
+ }
+ if (pattern.type === 'ObjectPattern') {
+ for (const property of pattern.properties ?? []) {
+ if (property.type === 'RestElement') {
+ getPatternIdentifierNames(property.argument, names);
+ } else {
+ getPatternIdentifierNames(property.value, names);
+ }
+ }
+ }
+ return names;
+};
+
+const getDeclaredNames = (node: AnyNode): Set => {
+ const names = new Set();
+ if (node.type === 'VariableDeclaration') {
+ for (const declarator of node.declarations ?? []) {
+ getPatternIdentifierNames(declarator.id, names);
+ }
+ } else if (
+ (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') &&
+ node.id?.name
+ ) {
+ names.add(node.id.name);
+ } else if (node.type === 'ImportDeclaration') {
+ for (const specifier of node.specifiers ?? []) {
+ if (specifier.local?.name) {
+ names.add(specifier.local.name);
+ }
+ }
+ }
+ return names;
+};
+
+const isIdentifierDeclaration = (node: AnyNode, parent: AnyNode | null) => {
+ if (!parent || node.type !== 'Identifier') {
+ return false;
+ }
+ if (
+ (parent.type === 'FunctionDeclaration' ||
+ parent.type === 'FunctionExpression' ||
+ parent.type === 'ClassDeclaration' ||
+ parent.type === 'ClassExpression') &&
+ parent.id === node
+ ) {
+ return true;
+ }
+ if (parent.type === 'VariableDeclarator') {
+ return getPatternIdentifierNames(parent.id).has(node.name);
+ }
+ if (
+ (parent.type === 'ImportSpecifier' ||
+ parent.type === 'ImportDefaultSpecifier' ||
+ parent.type === 'ImportNamespaceSpecifier') &&
+ parent.local === node
+ ) {
+ return true;
+ }
+ if (
+ (parent.type === 'FunctionDeclaration' ||
+ parent.type === 'FunctionExpression' ||
+ parent.type === 'ArrowFunctionExpression') &&
+ (parent.params ?? []).some((param: AnyNode) =>
+ getPatternIdentifierNames(param).has(node.name)
+ )
+ ) {
+ return true;
+ }
+ return false;
+};
+
+const isNonReferenceIdentifier = (node: AnyNode, parent: AnyNode | null) => {
+ if (!parent || node.type !== 'Identifier') {
+ return false;
+ }
+ if (isIdentifierDeclaration(node, parent)) {
+ return true;
+ }
+ if (
+ parent.type === 'MemberExpression' &&
+ parent.property === node &&
+ !parent.computed
+ ) {
+ return true;
+ }
+ if (
+ parent.type === 'Property' &&
+ parent.key === node &&
+ !parent.computed &&
+ !parent.shorthand
+ ) {
+ return true;
+ }
+ if (
+ parent.type === 'MethodDefinition' &&
+ parent.key === node &&
+ !parent.computed
+ ) {
+ return true;
+ }
+ if (parent.type === 'LabeledStatement' || parent.type === 'BreakStatement') {
+ return true;
+ }
+ return false;
+};
+
+const isUppercaseName = (name: string): boolean => /^[A-Z]/.test(name);
+
+const collectReferencedNames = (program: AnyNode): Set => {
+ const referenced = new Set();
+ walk(program as any, {
+ Identifier(node: AnyNode, ctx: any) {
+ const parent = ctx.parent as AnyNode | null;
+ if (!isNonReferenceIdentifier(node, parent)) {
+ referenced.add(node.name);
+ }
+ },
+ JSXIdentifier(node: AnyNode, ctx: any) {
+ const parent = ctx.parent as AnyNode | null;
+ if (!parent || !isUppercaseName(node.name)) {
+ return;
+ }
+ if (
+ (parent.type === 'JSXOpeningElement' ||
+ parent.type === 'JSXClosingElement') &&
+ parent.name === node
+ ) {
+ referenced.add(node.name);
+ return;
+ }
+ if (parent.type === 'JSXMemberExpression' && parent.object === node) {
+ referenced.add(node.name);
+ }
+ },
+ ExportSpecifier(node: AnyNode) {
+ if (node.local?.name && node.exportKind !== 'type') {
+ referenced.add(node.local.name);
+ }
+ },
+ });
+ return referenced;
+};
+
+const getExportedName = (specifier: AnyNode): string | null => {
+ const exported = specifier.exported;
+ if (!exported) {
+ return null;
+ }
+ if (exported.type === 'Identifier') {
+ return exported.name;
+ }
+ if (exported.type === 'Literal') {
+ return String(exported.value);
+ }
+ return null;
+};
+
+const collectExportedLocalNames = (program: AnyNode): Set => {
+ const names = new Set();
+ for (const statement of program.body ?? []) {
+ if (statement.type === 'ExportDefaultDeclaration') {
+ if (statement.declaration?.id?.name) {
+ names.add(statement.declaration.id.name);
+ }
+ continue;
+ }
+ if (statement.type !== 'ExportNamedDeclaration') {
+ continue;
+ }
+ if (statement.declaration) {
+ for (const name of getDeclaredNames(statement.declaration)) {
+ names.add(name);
+ }
+ }
+ for (const specifier of statement.specifiers ?? []) {
+ if (specifier.local?.name && specifier.exportKind !== 'type') {
+ names.add(specifier.local.name);
+ }
+ }
+ }
+ return names;
+};
+
+const removeUnusedTopLevelDeclarations = (program: AnyNode): void => {
+ let changed = true;
+ while (changed) {
+ changed = false;
+ const referenced = collectReferencedNames(program);
+ const exported = collectExportedLocalNames(program);
+ for (const statement of [...program.body]) {
+ if (statement.type !== 'VariableDeclaration') {
+ if (
+ (statement.type === 'FunctionDeclaration' ||
+ statement.type === 'ClassDeclaration') &&
+ statement.id?.name &&
+ !referenced.has(statement.id.name) &&
+ !exported.has(statement.id.name)
+ ) {
+ removeFromArray(program.body, statement);
+ changed = true;
+ }
+ continue;
+ }
+ statement.declarations = statement.declarations.filter(
+ (declarator: AnyNode) => {
+ const names = getPatternIdentifierNames(declarator.id);
+ return Array.from(names).some(
+ name => referenced.has(name) || exported.has(name)
+ );
+ }
+ );
+ if (statement.declarations.length === 0) {
+ removeFromArray(program.body, statement);
+ changed = true;
+ }
+ }
+ }
+};
+
export const removeExports = (
- ast: ParseResult,
+ ast: ParseResult | AnyNode,
exportsToRemove: string[]
): void => {
- const previouslyReferencedIdentifiers = findReferencedIdentifiers(ast);
+ const program = getProgram(ast);
let exportsFiltered = false;
- const markedForRemoval = new Set>();
- // Keep track of identifiers referenced by removed exports,
- // e.g. export { localName as exportName }, export default function localName
const removedExportLocalNames = new Set();
- traverse(ast, {
- ExportDeclaration(path: NodePath) {
- // export { foo };
- // export { bar } from "./module";
- if (path.node.type === 'ExportNamedDeclaration') {
- if (path.node.specifiers.length) {
- //@ts-ignore
- path.node.specifiers = path.node.specifiers.filter(
- (
- specifier:
- | Babel.ExportSpecifier
- | Babel.ExportDefaultSpecifier
- | Babel.ExportNamespaceSpecifier
- ) => {
- // Filter out individual specifiers
- if (
- specifier.type === 'ExportSpecifier' &&
- specifier.exported.type === 'Identifier'
- ) {
- if (exportsToRemove.includes(specifier.exported.name)) {
- exportsFiltered = true;
- // Track the local identifier if it's different from the exported name
- if (
- specifier.local &&
- specifier.local.type === 'Identifier' &&
- specifier.local.name !== specifier.exported.name
- ) {
- removedExportLocalNames.add(specifier.local.name);
- }
- return false;
- }
- }
+ for (const statement of [...program.body]) {
+ if (statement.type === 'ExportNamedDeclaration') {
+ if (statement.specifiers?.length) {
+ statement.specifiers = statement.specifiers.filter(
+ (specifier: AnyNode) => {
+ if (specifier.type !== 'ExportSpecifier') {
return true;
}
- );
- // Remove the entire export statement if all specifiers were removed
- if (path.node.specifiers.length === 0) {
- markedForRemoval.add(path);
+ const exportedName = getExportedName(specifier);
+ if (exportedName && exportsToRemove.includes(exportedName)) {
+ exportsFiltered = true;
+ if (specifier.local?.name) {
+ removedExportLocalNames.add(specifier.local.name);
+ }
+ return false;
+ }
+ return true;
}
+ );
+ if (statement.specifiers.length === 0 && !statement.declaration) {
+ removeFromArray(program.body, statement);
}
+ }
- // export const foo = ...;
- // export const [ foo ] = ...;
- if (path.node.declaration?.type === 'VariableDeclaration') {
- const declaration = path.node.declaration;
- declaration.declarations = declaration.declarations.filter(
- (declaration: Babel.VariableDeclarator) => {
- // export const foo = ...;
- // export const foo = ..., bar = ...;
- if (
- declaration.id.type === 'Identifier' &&
- exportsToRemove.includes(declaration.id.name)
- ) {
- // Filter out individual variables
+ const declaration = statement.declaration;
+ if (declaration?.type === 'VariableDeclaration') {
+ declaration.declarations = declaration.declarations.filter(
+ (declarator: AnyNode) => {
+ if (declarator.id.type === 'Identifier') {
+ if (exportsToRemove.includes(declarator.id.name)) {
exportsFiltered = true;
+ removedExportLocalNames.add(declarator.id.name);
return false;
}
-
- // export const [ foo ] = ...;
- // export const { foo } = ...;
- if (
- declaration.id.type === 'ArrayPattern' ||
- declaration.id.type === 'ObjectPattern'
- ) {
- // NOTE: These exports cannot be safely removed, so instead we
- // validate them to ensure that any exports that are intended to
- // be removed are not present
- validateDestructuredExports(declaration.id, exportsToRemove);
- }
-
return true;
}
- );
- // Remove the entire export statement if all variables were removed
- if (declaration.declarations.length === 0) {
- markedForRemoval.add(path);
- }
- }
- // export function foo() {}
- if (path.node.declaration?.type === 'FunctionDeclaration') {
- const id = path.node.declaration.id;
- if (id && exportsToRemove.includes(id.name)) {
- markedForRemoval.add(path);
- }
- }
-
- // export class Foo() {}
- if (path.node.declaration?.type === 'ClassDeclaration') {
- const id = path.node.declaration.id;
- if (id && exportsToRemove.includes(id.name)) {
- markedForRemoval.add(path);
+ validateDestructuredExports(declarator.id, exportsToRemove);
+ return true;
}
+ );
+ if (declaration.declarations.length === 0) {
+ removeFromArray(program.body, statement);
}
}
- // export default ...;
if (
- path.node.type === 'ExportDefaultDeclaration' &&
- exportsToRemove.includes('default')
+ (declaration?.type === 'FunctionDeclaration' ||
+ declaration?.type === 'ClassDeclaration') &&
+ declaration.id?.name &&
+ exportsToRemove.includes(declaration.id.name)
) {
- markedForRemoval.add(path);
- // Track the identifier being exported as default
- if (path.node.declaration) {
- if (path.node.declaration.type === 'Identifier') {
- removedExportLocalNames.add(path.node.declaration.name);
- } else if (
- (path.node.declaration.type === 'FunctionDeclaration' ||
- path.node.declaration.type === 'ClassDeclaration') &&
- path.node.declaration.id
- ) {
- removedExportLocalNames.add(path.node.declaration.id.name);
- }
- }
- }
- },
- });
-
- // Remove top-level property assignments to removed exports. Handles
- // `clientLoader.hydrate = true`, `Component.displayName = "..."`, etc.
- traverse(ast, {
- ExpressionStatement(path: NodePath) {
- // Only handle top-level statements
- if (!path.parentPath.isProgram()) {
- return;
- }
-
- const expr = path.node.expression;
- if (expr.type !== 'AssignmentExpression') {
- return;
+ removedExportLocalNames.add(declaration.id.name);
+ removeFromArray(program.body, statement);
}
+ }
- const left = expr.left;
- if (
- left.type === 'MemberExpression' &&
- left.object.type === 'Identifier' &&
- (exportsToRemove.includes(left.object.name) ||
- removedExportLocalNames.has(left.object.name))
- ) {
- markedForRemoval.add(path as any);
+ if (
+ statement.type === 'ExportDefaultDeclaration' &&
+ exportsToRemove.includes('default')
+ ) {
+ const declaration = statement.declaration;
+ if (declaration?.type === 'Identifier') {
+ removedExportLocalNames.add(declaration.name);
+ } else if (declaration?.id?.name) {
+ removedExportLocalNames.add(declaration.id.name);
}
- },
- });
+ removeFromArray(program.body, statement);
+ }
+ }
- if (markedForRemoval.size > 0 || exportsFiltered) {
- for (const path of markedForRemoval) {
- path.remove();
+ for (const statement of [...program.body]) {
+ const expression =
+ statement.type === 'ExpressionStatement' ? statement.expression : null;
+ const left =
+ expression?.type === 'AssignmentExpression' ? expression.left : null;
+ if (
+ left?.type === 'MemberExpression' &&
+ left.object?.type === 'Identifier' &&
+ (exportsToRemove.includes(left.object.name) ||
+ removedExportLocalNames.has(left.object.name))
+ ) {
+ removeFromArray(program.body, statement);
}
+ }
- // Run dead code elimination on any newly unreferenced identifiers
- deadCodeElimination(ast, previouslyReferencedIdentifiers);
+ if (exportsFiltered || removedExportLocalNames.size > 0) {
+ removeUnusedTopLevelDeclarations(program);
}
};
-export const removeUnusedImports = (ast: ParseResult): void => {
- let scopeCrawled = false;
- traverse(ast, {
- Program(path: NodePath) {
- if (!scopeCrawled) {
- path.scope.crawl();
- scopeCrawled = true;
- }
- },
- ImportDeclaration(path: NodePath) {
- if (path.node.specifiers.length === 0) {
- return;
- }
-
- const specifierPaths = path.get('specifiers') as NodePath<
- | Babel.ImportSpecifier
- | Babel.ImportDefaultSpecifier
- | Babel.ImportNamespaceSpecifier
- >[];
-
- for (const specifierPath of specifierPaths) {
- const local = specifierPath.node.local;
- const binding = local ? path.scope.getBinding(local.name) : null;
- if (!binding || !binding.referenced) {
- specifierPath.remove();
+export const removeUnusedImports = (ast: ParseResult | AnyNode): void => {
+ const program = getProgram(ast);
+ const referenced = collectReferencedNames(program);
+ for (const statement of [...program.body]) {
+ if (statement.type !== 'ImportDeclaration') {
+ continue;
+ }
+ if ((statement.specifiers ?? []).length === 0) {
+ continue;
+ }
+ statement.specifiers = (statement.specifiers ?? []).filter(
+ (specifier: AnyNode) => {
+ if (specifier.importKind === 'type') {
+ return false;
}
+ return !specifier.local?.name || referenced.has(specifier.local.name);
}
+ );
+ if (statement.specifiers.length === 0) {
+ removeFromArray(program.body, statement);
+ }
+ }
+};
- if (path.node.specifiers.length === 0) {
- path.remove();
- }
+const identifier = (name: string): AnyNode => ({
+ type: 'Identifier',
+ start: 0,
+ end: 0,
+ name,
+ decorators: [],
+ optional: false,
+ typeAnnotation: null,
+});
+
+const literal = (value: string): AnyNode => ({
+ type: 'Literal',
+ start: 0,
+ end: 0,
+ value,
+ raw: JSON.stringify(value),
+});
+
+const callExpression = (callee: AnyNode, args: AnyNode[]): AnyNode => ({
+ type: 'CallExpression',
+ start: 0,
+ end: 0,
+ callee,
+ arguments: args,
+ optional: false,
+});
+
+const importDeclaration = (
+ specifiers: Array<{ local: string; imported: string }>,
+ source: string
+): AnyNode => ({
+ type: 'ImportDeclaration',
+ start: 0,
+ end: 0,
+ specifiers: specifiers.map(specifier => ({
+ type: 'ImportSpecifier',
+ start: 0,
+ end: 0,
+ imported: identifier(specifier.imported),
+ local: identifier(specifier.local),
+ importKind: 'value',
+ })),
+ source: literal(source),
+ attributes: [],
+ phase: null,
+ importKind: 'value',
+});
+
+const variableDeclaration = (name: string, init: AnyNode): AnyNode => ({
+ type: 'VariableDeclaration',
+ start: 0,
+ end: 0,
+ kind: 'const',
+ declare: false,
+ declarations: [
+ {
+ type: 'VariableDeclarator',
+ start: 0,
+ end: 0,
+ id: identifier(name),
+ init,
+ definite: false,
+ },
+ ],
+});
+
+const collectUsedNames = (program: AnyNode): Set => {
+ const names = new Set();
+ walk(program as any, {
+ Identifier(node: AnyNode) {
+ names.add(node.name);
},
});
+ return names;
};
-export const transformRoute = (ast: ParseResult): void => {
- const hocs: Array<[string, Babel.Identifier]> = [];
- function getHocUid(path: NodePath, hocName: string) {
- const uid = path.scope.generateUidIdentifier(hocName);
+export const transformRoute = (ast: ParseResult | AnyNode): void => {
+ const program = getProgram(ast);
+ const usedNames = collectUsedNames(program);
+ const hocs: Array<[string, string]> = [];
+
+ function getHocUid(hocName: string) {
+ let uid = `_${hocName}`;
+ let index = 2;
+ while (usedNames.has(uid)) {
+ uid = `_${hocName}${index++}`;
+ }
+ usedNames.add(uid);
hocs.push([hocName, uid]);
- return uid;
- }
-
- traverse(ast, {
- ExportDeclaration(path: NodePath) {
- if (path.isExportDefaultDeclaration()) {
- const declaration = path.get('declaration');
- // prettier-ignore
- const expr =
- declaration.isExpression() ? declaration.node :
- declaration.isFunctionDeclaration() ? toFunctionExpression(declaration.node) :
- undefined
- if (expr) {
- const uid = getHocUid(path, 'withComponentProps');
- declaration.replaceWith(t.callExpression(uid, [expr]) as any);
- }
- return;
- }
+ return identifier(uid);
+ }
- if (path.isExportNamedDeclaration()) {
- const decl = path.get('declaration');
-
- if (decl.isVariableDeclaration()) {
- // biome-ignore lint/complexity/noForEach:
- decl.get('declarations').forEach((varDeclarator: NodePath) => {
- const id = varDeclarator.get('id') as any;
- const init = varDeclarator.get('init') as any;
- const expr = init.node as any;
- if (!expr) return;
- if (!id.isIdentifier()) return;
- const { name } = id.node;
- if (!isNamedComponentExport(name)) return;
-
- const uid = getHocUid(path, `with${name}Props`);
- init.replaceWith(t.callExpression(uid, [expr]));
- });
- return;
- }
+ for (const statement of program.body ?? []) {
+ if (statement.type === 'ExportDefaultDeclaration') {
+ const declaration = statement.declaration;
+ const expr =
+ declaration?.type === 'FunctionDeclaration'
+ ? toFunctionExpression(declaration)
+ : declaration;
+ if (expr && expr.type !== 'ClassDeclaration') {
+ const uid = getHocUid('withComponentProps');
+ statement.declaration = callExpression(uid, [expr]);
+ }
+ continue;
+ }
- if (decl.isFunctionDeclaration()) {
- const { id } = decl.node;
- if (!id) return;
- const { name } = id;
- if (!isNamedComponentExport(name)) return;
-
- const uid = getHocUid(path, `with${name}Props`);
- decl.replaceWith(
- t.variableDeclaration('const', [
- t.variableDeclarator(
- t.identifier(name),
- t.callExpression(uid, [toFunctionExpression(decl.node)])
- ),
- ]) as any
- );
+ if (statement.type !== 'ExportNamedDeclaration') {
+ continue;
+ }
+ const declaration = statement.declaration;
+ if (declaration?.type === 'VariableDeclaration') {
+ for (const declarator of declaration.declarations ?? []) {
+ if (
+ declarator.id?.type !== 'Identifier' ||
+ !declarator.init ||
+ !isNamedComponentExport(declarator.id.name)
+ ) {
+ continue;
}
+ const uid = getHocUid(`with${declarator.id.name}Props`);
+ declarator.init = callExpression(uid, [declarator.init]);
}
- },
- });
+ continue;
+ }
+
+ if (
+ declaration?.type === 'FunctionDeclaration' &&
+ declaration.id?.name &&
+ isNamedComponentExport(declaration.id.name)
+ ) {
+ const name = declaration.id.name;
+ const uid = getHocUid(`with${name}Props`);
+ statement.declaration = variableDeclaration(
+ name,
+ callExpression(uid, [toFunctionExpression(declaration)])
+ );
+ }
+ }
if (hocs.length > 0) {
- ast.program.body.unshift(
- t.importDeclaration(
- hocs.map(([name, identifier]) =>
- t.importSpecifier(identifier, t.identifier(name))
- ),
- t.stringLiteral('virtual/react-router/with-props')
- ) as any
+ program.body.unshift(
+ importDeclaration(
+ hocs.map(([name, local]) => ({ imported: name, local })),
+ 'virtual/react-router/with-props'
+ )
);
}
};
diff --git a/src/route-chunks.ts b/src/route-chunks.ts
index 2339729..1b76c59 100644
--- a/src/route-chunks.ts
+++ b/src/route-chunks.ts
@@ -1,7 +1,14 @@
-import type { NodePath } from './babel.js';
-import { generate, parse, t, traverse } from './babel.js';
+import {
+ Analyzer,
+ type Module,
+ type Symbol as YukuSymbol,
+} from 'yuku-analyzer';
+import { strip } from 'yuku-codegen';
+import { walk } from 'yuku-parser';
import { normalize, relative, resolve } from 'pathe';
+type AnyNode = Record;
+
export type RouteChunkExportName =
| 'clientAction'
| 'clientLoader'
@@ -85,75 +92,114 @@ const getOrSetFromCache = (
return value;
};
-const codeToAst = (
+type AnalyzedModule = {
+ module: Module;
+ program: AnyNode;
+};
+
+const analyzeCode = (
code: string,
cache: RouteChunkCache | undefined,
cacheKey: string
-) => {
- return structuredClone(
- getOrSetFromCache(cache, `${cacheKey}::codeToAst`, code, () =>
- parse(code, { sourceType: 'module' })
- )
- );
+): AnalyzedModule => {
+ return getOrSetFromCache(cache, `${cacheKey}::analyzeCode`, code, () => {
+ const analyzer = new Analyzer();
+ const module = analyzer.addFile(cacheKey, code, {
+ lang: 'tsx',
+ sourceType: 'module',
+ preserveParens: true,
+ });
+ const errors = module.diagnostics.filter(
+ diagnostic => diagnostic.severity === 'error'
+ );
+ if (errors.length > 0) {
+ throw new Error(errors.map(error => error.message).join('\n'));
+ }
+ return { module, program: module.ast as AnyNode };
+ });
};
-const assertNodePath: (
- path: NodePath | NodePath[] | null | undefined
-) => asserts path is NodePath = path => {
- invariant(
- path && !Array.isArray(path),
- `Expected a Path, but got ${Array.isArray(path) ? 'an array' : path}`
- );
+const cloneProgram = (
+ code: string,
+ cache: RouteChunkCache | undefined,
+ cacheKey: string
+): AnyNode => structuredClone(analyzeCode(code, cache, cacheKey).program);
+
+type ExportDependencies = {
+ topLevelStatements: Set;
+ topLevelNonModuleStatements: Set;
+ importedIdentifierNames: Set;
+ exportedVariableDeclarators: Set;
};
-const isNodePathWithNode = (path: unknown): path is NodePath => {
- if (!path || typeof path !== 'object' || Array.isArray(path)) {
- return false;
- }
- if (!('node' in path)) {
- return false;
+const getTopLevelStatementForNode = (
+ module: Module,
+ node: AnyNode
+): AnyNode => {
+ let current: AnyNode = node;
+ let parent = module.parentOf(current as never) as AnyNode | null;
+ while (parent && parent.type !== 'Program') {
+ current = parent;
+ parent = module.parentOf(current as never) as AnyNode | null;
}
- return Boolean((path as { node?: unknown }).node);
+ invariant(parent?.type === 'Program', 'Expected node to be within Program');
+ return current;
};
-const assertNodePathIsStatement: (
- path: NodePath | NodePath[] | null | undefined
-) => asserts path is NodePath = path => {
- invariant(
- path && !Array.isArray(path) && t.isStatement(path.node),
- `Expected a Statement path, but got ${
- Array.isArray(path) ? 'an array' : path?.node?.type
- }`
- );
+const addTopLevelStatement = (
+ module: Module,
+ dependencies: ExportDependencies,
+ node: AnyNode
+) => {
+ const statement = getTopLevelStatementForNode(module, node);
+ dependencies.topLevelStatements.add(statement);
+ if (
+ statement.type !== 'ImportDeclaration' &&
+ !statement.type.startsWith('Export')
+ ) {
+ dependencies.topLevelNonModuleStatements.add(statement);
+ }
};
-const assertNodePathIsVariableDeclarator: (
- path: NodePath | NodePath[] | null | undefined
-) => asserts path is NodePath = path => {
- invariant(
- path && !Array.isArray(path) && t.isVariableDeclarator(path.node),
- `Expected a VariableDeclarator path, but got ${
- Array.isArray(path) ? 'an array' : path?.node?.type
- }`
- );
+const getVariableDeclaratorForNode = (
+ module: Module,
+ node: AnyNode
+): AnyNode | null => {
+ let current: AnyNode | null = node;
+ while (current) {
+ if (current.type === 'VariableDeclarator') {
+ return current;
+ }
+ current = module.parentOf(current as never) as AnyNode | null;
+ }
+ return null;
};
-const assertNodePathIsPattern: (
- path: NodePath | NodePath[] | null | undefined
-) => asserts path is NodePath = path => {
- invariant(
- path && !Array.isArray(path) && t.isPattern(path.node),
- `Expected a Pattern path, but got ${
- Array.isArray(path) ? 'an array' : path?.node?.type
- }`
- );
+const getExportedName = (exported: AnyNode): string => {
+ if (exported.type === 'Identifier') {
+ return exported.name;
+ }
+ return String(exported.value);
};
-type ExportDependencies = {
- topLevelStatements: Set;
- topLevelNonModuleStatements: Set;
- importedIdentifierNames: Set;
- exportedVariableDeclarators: Set;
+const sameNode = (left: AnyNode, right: AnyNode): boolean =>
+ left.type === right.type &&
+ left.start === right.start &&
+ left.end === right.end;
+
+const setsIntersect = (set1: Set, set2: Set) => {
+ let smallerSet = set1;
+ let largerSet = set2;
+ if (set1.size > set2.size) {
+ smallerSet = set2;
+ largerSet = set1;
+ }
+ for (const element of smallerSet) {
+ if (largerSet.has(element)) {
+ return true;
+ }
+ }
+ return false;
};
const getExportDependencies = (
@@ -166,296 +212,96 @@ const getExportDependencies = (
`${cacheKey}::getExportDependencies`,
code,
() => {
+ const { module } = analyzeCode(code, cache, cacheKey);
const exportDependencies = new Map();
- const ast = codeToAst(code, cache, cacheKey);
- function handleExport(
+ const handleExport = (
exportName: string,
- exportPath: NodePath,
- identifiersPath: NodePath = exportPath
- ) {
- const identifiers = getDependentIdentifiersForPath(identifiersPath);
- const topLevelStatements = new Set([
- exportPath.node as t.Statement,
- ...getTopLevelStatementsForPaths(identifiers),
- ]);
- const topLevelNonModuleStatements = new Set(
- Array.from(topLevelStatements).filter(
- statement =>
- !t.isImportDeclaration(statement) &&
- !t.isExportDeclaration(statement)
- )
- );
- const importedIdentifierNames = new Set();
- for (const identifier of identifiers) {
- if (
- t.isIdentifier(identifier.node) &&
- identifier.parentPath?.parentPath?.isImportDeclaration()
- ) {
- importedIdentifierNames.add(identifier.node.name);
- }
- }
- const exportedVariableDeclarators = new Set();
- for (const identifier of identifiers) {
- if (identifier.parentPath?.isVariableDeclarator()) {
- const parentPath = identifier.parentPath;
- if (parentPath.parentPath?.parentPath?.isExportNamedDeclaration()) {
- exportedVariableDeclarators.add(
- parentPath.node as t.VariableDeclarator
- );
- continue;
- }
+ exportNode: AnyNode,
+ localSymbol: YukuSymbol | null
+ ) => {
+ const dependencies: ExportDependencies = {
+ topLevelStatements: new Set(),
+ topLevelNonModuleStatements: new Set(),
+ importedIdentifierNames: new Set(),
+ exportedVariableDeclarators: new Set(),
+ };
+ const visitedSymbols = new Set();
+ const scannedStatements = new Set();
+
+ const scanStatement = (statement: AnyNode) => {
+ if (scannedStatements.has(statement)) {
+ return;
}
- const isWithinExportDestructuring = Boolean(
- identifier.findParent(path =>
- Boolean(
- path.isPattern() &&
- path.parentPath?.isVariableDeclarator() &&
- path.parentPath.parentPath?.parentPath?.isExportNamedDeclaration()
- )
- )
- );
- if (isWithinExportDestructuring) {
- let currentPath: NodePath | null = identifier;
- while (currentPath) {
- if (
- currentPath.parentPath?.isVariableDeclarator() &&
- currentPath.parentKey === 'id'
- ) {
- exportedVariableDeclarators.add(
- currentPath.parentPath.node as t.VariableDeclarator
- );
- break;
+ scannedStatements.add(statement);
+ walk(statement as any, {
+ Identifier(node: AnyNode) {
+ const reference = module.referenceOf(node as never);
+ if (reference?.symbol) {
+ visitSymbol(reference.symbol);
}
- currentPath = currentPath.parentPath;
- }
- }
- }
- exportDependencies.set(exportName, {
- topLevelStatements,
- topLevelNonModuleStatements,
- importedIdentifierNames,
- exportedVariableDeclarators,
- });
- }
+ },
+ });
+ };
- traverse(ast, {
- ExportDeclaration(exportPath) {
- const { node } = exportPath;
- if (t.isExportAllDeclaration(node)) {
- return;
- }
- if (t.isExportDefaultDeclaration(node)) {
- handleExport('default', exportPath);
+ const visitSymbol = (symbol: YukuSymbol) => {
+ if (visitedSymbols.has(symbol)) {
return;
}
- const { declaration } = node;
- if (t.isVariableDeclaration(declaration)) {
- const { declarations } = declaration;
- for (let i = 0; i < declarations.length; i++) {
- const declarator = declarations[i];
- if (t.isIdentifier(declarator.id)) {
- const declaratorPath = exportPath.get(
- `declaration.declarations.${i}`
- );
- assertNodePathIsVariableDeclarator(declaratorPath);
- handleExport(declarator.id.name, exportPath, declaratorPath);
- continue;
- }
- if (t.isPattern(declarator.id)) {
- const exportedPatternPath = exportPath.get(
- `declaration.declarations.${i}.id`
- );
- assertNodePathIsPattern(exportedPatternPath);
- const identifiers =
- getIdentifiersForPatternPath(exportedPatternPath);
- for (const identifier of identifiers) {
- if (!t.isIdentifier(identifier.node)) {
- continue;
- }
- handleExport(identifier.node.name, exportPath, identifier);
- }
- }
+ visitedSymbols.add(symbol);
+
+ for (const declaration of symbol.declarations as AnyNode[]) {
+ const statement = getTopLevelStatementForNode(module, declaration);
+ addTopLevelStatement(module, dependencies, declaration);
+ if (statement.type === 'ImportDeclaration') {
+ dependencies.importedIdentifierNames.add(symbol.name);
}
- return;
- }
- if (
- t.isFunctionDeclaration(declaration) ||
- t.isClassDeclaration(declaration)
- ) {
- invariant(
- declaration.id,
- 'Expected exported function or class declaration to have a name when not the default export'
+ const declarator = getVariableDeclaratorForNode(
+ module,
+ declaration
);
- handleExport(declaration.id.name, exportPath);
- return;
- }
- if (t.isExportNamedDeclaration(node)) {
- for (const specifier of node.specifiers) {
- if (t.isIdentifier(specifier.exported)) {
- const name = specifier.exported.name;
- const specifierPath = exportPath
- .get('specifiers')
- .find(path => path.node === specifier);
- invariant(
- specifierPath,
- `Expected to find specifier path for ${name}`
- );
- handleExport(name, exportPath, specifierPath);
- }
+ if (
+ declarator &&
+ getTopLevelStatementForNode(module, declarator).type ===
+ 'ExportNamedDeclaration'
+ ) {
+ dependencies.exportedVariableDeclarators.add(declarator);
}
- return;
+ scanStatement(statement);
}
- throw new Error('Unknown export node type');
- },
- });
- return exportDependencies;
- }
- );
-};
+ for (const reference of symbol.references as any[]) {
+ const statement = getTopLevelStatementForNode(
+ module,
+ reference.node
+ );
+ addTopLevelStatement(module, dependencies, reference.node);
+ scanStatement(statement);
+ }
+ };
-const getDependentIdentifiersForPath = (
- path: NodePath,
- state?: { visited: Set; identifiers: Set }
-): Set => {
- const { visited, identifiers } = state ?? {
- visited: new Set(),
- identifiers: new Set(),
- };
- if (visited.has(path)) {
- return identifiers;
- }
- visited.add(path);
- path.traverse({
- Identifier(pathInner) {
- if (identifiers.has(pathInner)) {
- return;
- }
- identifiers.add(pathInner);
- const binding = pathInner.scope.getBinding(pathInner.node.name);
- if (!binding) {
- return;
- }
- getDependentIdentifiersForPath(binding.path, { visited, identifiers });
- for (const reference of binding.referencePaths) {
- if (reference.isExportNamedDeclaration()) {
- continue;
- }
- getDependentIdentifiersForPath(reference, { visited, identifiers });
- }
- for (const constantViolation of binding.constantViolations) {
- getDependentIdentifiersForPath(constantViolation, {
- visited,
- identifiers,
- });
- }
- },
- });
- const topLevelStatement = getTopLevelStatementPathForPath(path);
- const withinImportStatement = topLevelStatement.isImportDeclaration();
- const withinExportStatement = topLevelStatement.isExportDeclaration();
- if (!withinImportStatement && !withinExportStatement) {
- getDependentIdentifiersForPath(topLevelStatement, { visited, identifiers });
- }
- if (
- withinExportStatement &&
- path.isIdentifier() &&
- (t.isPattern(path.parentPath.node) ||
- t.isPattern(path.parentPath.parentPath?.node))
- ) {
- const variableDeclarator = path.findParent(p => p.isVariableDeclarator());
- if (variableDeclarator) {
- assertNodePath(variableDeclarator);
- getDependentIdentifiersForPath(variableDeclarator, {
- visited,
- identifiers,
- });
- }
- }
- return identifiers;
-};
+ addTopLevelStatement(module, dependencies, exportNode);
-const getTopLevelStatementPathForPath = (path: NodePath) => {
- const ancestry = path.getAncestry();
- const topLevelStatement = ancestry[ancestry.length - 2];
- assertNodePathIsStatement(topLevelStatement);
- return topLevelStatement;
-};
+ if (localSymbol) {
+ visitSymbol(localSymbol);
+ } else {
+ const statement = getTopLevelStatementForNode(module, exportNode);
+ scanStatement(statement);
+ }
-const getTopLevelStatementsForPaths = (paths: Set) => {
- const topLevelStatements = new Set();
- for (const path of paths) {
- const topLevelStatement = getTopLevelStatementPathForPath(path);
- topLevelStatements.add(topLevelStatement.node as t.Statement);
- }
- return topLevelStatements;
-};
+ exportDependencies.set(exportName, dependencies);
+ };
-const getIdentifiersForPatternPath = (
- patternPath: NodePath,
- identifiers: Set = new Set()
-) => {
- function walk(currentPath: NodePath) {
- if (currentPath.isIdentifier()) {
- identifiers.add(currentPath);
- return;
- }
- if (currentPath.isObjectPattern()) {
- const { properties } = currentPath.node;
- for (let i = 0; i < properties.length; i++) {
- const property = properties[i];
- if (t.isObjectProperty(property)) {
- const valuePath = currentPath.get(`properties.${i}.value`);
- if (isNodePathWithNode(valuePath)) {
- walk(valuePath);
- }
- } else if (t.isRestElement(property)) {
- const argumentPath = currentPath.get(`properties.${i}.argument`);
- if (isNodePathWithNode(argumentPath)) {
- walk(argumentPath);
- }
- }
- }
- } else if (currentPath.isArrayPattern()) {
- const { elements } = currentPath.node;
- for (let i = 0; i < elements.length; i++) {
- const element = elements[i];
- if (element) {
- const elementPath = currentPath.get(`elements.${i}`);
- if (isNodePathWithNode(elementPath)) {
- walk(elementPath);
- }
+ for (const exp of module.exports as any[]) {
+ if (exp.typeOnly || exp.isStar || exp.isExportEquals) {
+ continue;
}
+ handleExport(exp.name, exp.node as AnyNode, exp.local ?? null);
}
- } else if (currentPath.isRestElement()) {
- const argumentPath = currentPath.get('argument');
- if (isNodePathWithNode(argumentPath)) {
- walk(argumentPath);
- }
- }
- }
- walk(patternPath);
- return identifiers;
-};
-
-const getExportedName = (exported: t.Identifier | t.StringLiteral) => {
- return t.isIdentifier(exported) ? exported.name : exported.value;
-};
-const setsIntersect = (set1: Set, set2: Set) => {
- let smallerSet = set1;
- let largerSet = set2;
- if (set1.size > set2.size) {
- smallerSet = set2;
- largerSet = set1;
- }
- for (const element of smallerSet) {
- if (largerSet.has(element)) {
- return true;
+ return exportDependencies;
}
- }
- return false;
+ );
};
const hasChunkableExport = (
@@ -516,18 +362,39 @@ const hasChunkableExport = (
);
};
+const generateCode = (program: AnyNode): string | undefined => {
+ if (program.body.length === 0) {
+ return undefined;
+ }
+ const result = strip(program as any, { comments: 'some' });
+ if (result.errors.length > 0) {
+ throw new Error(result.errors.map(error => error.message).join('\n'));
+ }
+ return result.code;
+};
+
+const filterImportSpecifiers = (
+ node: AnyNode,
+ shouldKeep: (importedName: string) => boolean
+) => {
+ if (node.specifiers.length === 0) {
+ return node;
+ }
+ node.specifiers = node.specifiers.filter((specifier: AnyNode) =>
+ shouldKeep(specifier.local.name)
+ );
+ return node.specifiers.length > 0 ? node : null;
+};
+
const getChunkedExport = (
code: string,
exportName: string,
- generateOptions: Record = {},
cache: RouteChunkCache | undefined,
cacheKey: string
): string | undefined => {
return getOrSetFromCache(
cache,
- `${cacheKey}::getChunkedExport::${exportName}::${JSON.stringify(
- generateOptions
- )}`,
+ `${cacheKey}::getChunkedExport::${exportName}`,
code,
() => {
if (!hasChunkableExport(code, exportName, cache, cacheKey)) {
@@ -544,75 +411,60 @@ const getChunkedExport = (
dependencies.exportedVariableDeclarators
);
- const ast = codeToAst(code, cache, cacheKey);
- ast.program.body = ast.program.body
- .filter(node =>
- topLevelStatementsArray.some(statement =>
- t.isNodesEquivalent(node, statement)
- )
+ const program = cloneProgram(code, cache, cacheKey);
+ program.body = program.body
+ .filter((node: AnyNode) =>
+ topLevelStatementsArray.some(statement => sameNode(node, statement))
)
- .map(node => {
- if (!t.isImportDeclaration(node)) {
+ .map((node: AnyNode) => {
+ if (node.type !== 'ImportDeclaration') {
return node;
}
if (dependencies.importedIdentifierNames.size === 0) {
return null;
}
- node.specifiers = node.specifiers.filter(specifier =>
- dependencies.importedIdentifierNames.has(specifier.local.name)
- );
- invariant(
- node.specifiers.length > 0,
- 'Expected import statement to have used specifiers'
+ return filterImportSpecifiers(node, importedName =>
+ dependencies.importedIdentifierNames.has(importedName)
);
- return node;
})
- .map(node => {
- if (!t.isExportDeclaration(node)) {
+ .map((node: AnyNode | null) => {
+ if (!node || !node.type.startsWith('Export')) {
return node;
}
- if (t.isExportAllDeclaration(node)) {
+ if (node.type === 'ExportAllDeclaration') {
return null;
}
- if (t.isExportDefaultDeclaration(node)) {
+ if (node.type === 'ExportDefaultDeclaration') {
return exportName === 'default' ? node : null;
}
const { declaration } = node;
- if (t.isVariableDeclaration(declaration)) {
+ if (declaration?.type === 'VariableDeclaration') {
declaration.declarations = declaration.declarations.filter(
- declarationNode =>
+ (declarationNode: AnyNode) =>
exportedVariableDeclaratorsArray.some(declarator =>
- t.isNodesEquivalent(declarationNode, declarator)
+ sameNode(declarationNode, declarator)
)
);
- if (declaration.declarations.length === 0) {
- return null;
- }
- return node;
+ return declaration.declarations.length > 0 ? node : null;
}
if (
- t.isFunctionDeclaration(node.declaration) ||
- t.isClassDeclaration(node.declaration)
+ declaration?.type === 'FunctionDeclaration' ||
+ declaration?.type === 'ClassDeclaration'
) {
- return node.declaration.id?.name === exportName ? node : null;
+ return declaration.id?.name === exportName ? node : null;
}
- if (t.isExportNamedDeclaration(node)) {
- if (node.specifiers.length === 0) {
- return null;
- }
+ if (node.type === 'ExportNamedDeclaration') {
node.specifiers = node.specifiers.filter(
- specifier => getExportedName(specifier.exported) === exportName
+ (specifier: AnyNode) =>
+ getExportedName(specifier.exported) === exportName
);
- if (node.specifiers.length === 0) {
- return null;
- }
- return node;
+ return node.specifiers.length > 0 ? node : null;
}
throw new Error('Unknown export node type');
})
- .filter(Boolean) as t.Statement[];
+ .filter(Boolean) as AnyNode[];
- return generate(ast, generateOptions).code;
+ return generateCode(program);
}
);
};
@@ -620,15 +472,12 @@ const getChunkedExport = (
const omitChunkedExports = (
code: string,
exportNames: string[],
- generateOptions: Record = {},
cache: RouteChunkCache | undefined,
cacheKey: string
): string | undefined => {
return getOrSetFromCache(
cache,
- `${cacheKey}::omitChunkedExports::${exportNames.join(',')}::${JSON.stringify(
- generateOptions
- )}`,
+ `${cacheKey}::omitChunkedExports::${exportNames.join(',')}`,
code,
() => {
const isChunkable = (exportName: string) =>
@@ -642,9 +491,8 @@ const omitChunkedExports = (
const omittedExportNames = allExportNames.filter(isOmitted);
const retainedExportNames = allExportNames.filter(isRetained);
- const omittedStatements = new Set();
- const omittedExportedVariableDeclarators =
- new Set();
+ const omittedStatements = new Set();
+ const omittedExportedVariableDeclarators = new Set();
for (const omittedExportName of omittedExportNames) {
const dependencies = exportDependencies.get(omittedExportName);
@@ -660,100 +508,73 @@ const omitChunkedExports = (
}
}
- const ast = codeToAst(code, cache, cacheKey);
const omittedStatementsArray = Array.from(omittedStatements);
const omittedExportedVariableDeclaratorsArray = Array.from(
omittedExportedVariableDeclarators
);
- ast.program.body = ast.program.body
- .filter(node =>
- omittedStatementsArray.every(
- statement => !t.isNodesEquivalent(node, statement)
- )
+
+ const program = cloneProgram(code, cache, cacheKey);
+ program.body = program.body
+ .filter((node: AnyNode) =>
+ omittedStatementsArray.every(statement => !sameNode(node, statement))
)
- .map(node => {
- if (!t.isImportDeclaration(node)) {
+ .map((node: AnyNode) => {
+ if (node.type !== 'ImportDeclaration') {
return node;
}
- if (node.specifiers.length === 0) {
- return node;
- }
- node.specifiers = node.specifiers.filter(specifier => {
- const importedName = specifier.local.name;
+ return filterImportSpecifiers(node, importedName => {
for (const retainedExportName of retainedExportNames) {
const dependencies = exportDependencies.get(retainedExportName);
- if (dependencies?.importedIdentifierNames?.has(importedName)) {
+ if (dependencies?.importedIdentifierNames.has(importedName)) {
return true;
}
}
for (const omittedExportName of omittedExportNames) {
const dependencies = exportDependencies.get(omittedExportName);
- if (dependencies?.importedIdentifierNames?.has(importedName)) {
+ if (dependencies?.importedIdentifierNames.has(importedName)) {
return false;
}
}
return true;
});
- if (node.specifiers.length === 0) {
- return null;
- }
- return node;
})
- .map(node => {
- if (!t.isExportDeclaration(node)) {
+ .map((node: AnyNode | null) => {
+ if (!node || !node.type.startsWith('Export')) {
return node;
}
- if (t.isExportAllDeclaration(node)) {
+ if (node.type === 'ExportAllDeclaration') {
return node;
}
- if (t.isExportDefaultDeclaration(node)) {
+ if (node.type === 'ExportDefaultDeclaration') {
return isOmitted('default') ? null : node;
}
- if (t.isVariableDeclaration(node.declaration)) {
+ if (node.declaration?.type === 'VariableDeclaration') {
node.declaration.declarations =
- node.declaration.declarations.filter(declarationNode =>
+ node.declaration.declarations.filter((declarationNode: AnyNode) =>
omittedExportedVariableDeclaratorsArray.every(
- declarator =>
- !t.isNodesEquivalent(declarationNode, declarator)
+ declarator => !sameNode(declarationNode, declarator)
)
);
- if (node.declaration.declarations.length === 0) {
- return null;
- }
- return node;
+ return node.declaration.declarations.length > 0 ? node : null;
}
if (
- t.isFunctionDeclaration(node.declaration) ||
- t.isClassDeclaration(node.declaration)
+ node.declaration?.type === 'FunctionDeclaration' ||
+ node.declaration?.type === 'ClassDeclaration'
) {
- const declarationId = node.declaration.id;
- invariant(
- declarationId,
- 'Expected exported function or class declaration to have a name when not the default export'
- );
- return isOmitted(declarationId.name) ? null : node;
+ return isOmitted(node.declaration.id.name) ? null : node;
}
- if (t.isExportNamedDeclaration(node)) {
- if (node.specifiers.length === 0) {
- return node;
- }
- node.specifiers = node.specifiers.filter(specifier => {
+ if (node.type === 'ExportNamedDeclaration') {
+ node.specifiers = node.specifiers.filter((specifier: AnyNode) => {
const exportedName = getExportedName(specifier.exported);
return !isOmitted(exportedName);
});
- if (node.specifiers.length === 0) {
- return null;
- }
- return node;
+ return node.specifiers.length > 0 || node.declaration ? node : null;
}
throw new Error('Unknown node type');
})
- .filter(Boolean) as t.Statement[];
+ .filter(Boolean) as AnyNode[];
- if (ast.program.body.length === 0) {
- return undefined;
- }
- return generate(ast, generateOptions).code;
+ return generateCode(program);
}
);
};
@@ -792,9 +613,9 @@ export const getRouteChunkCode: (
cacheKey: string
) => {
if (chunkName === 'main') {
- return omitChunkedExports(code, routeChunkExportNames, {}, cache, cacheKey);
+ return omitChunkedExports(code, routeChunkExportNames, cache, cacheKey);
}
- return getChunkedExport(code, chunkName, {}, cache, cacheKey);
+ return getChunkedExport(code, chunkName, cache, cacheKey);
};
export const getRouteChunkModuleId = (
diff --git a/tests/export-utils.test.ts b/tests/export-utils.test.ts
new file mode 100644
index 0000000..59ef389
--- /dev/null
+++ b/tests/export-utils.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from '@rstest/core';
+import { parse } from '../src/babel';
+import { transformToEsm } from '../src/export-utils';
+
+describe('export-utils', () => {
+ describe('transformToEsm', () => {
+ it('preserves arrow function object return parentheses', async () => {
+ const code = `
+ const items = [{ pathname: '/', data: 'Home' }];
+ export const labels = items.map((item) => ({
+ to: item.pathname,
+ label: item.data,
+ }));
+ `;
+
+ const transformed = await transformToEsm(code, 'route.tsx');
+
+ expect(transformed).toContain('=> ({');
+ expect(() => parse(transformed, { sourceType: 'module' })).not.toThrow();
+ });
+ });
+});
diff --git a/tests/remove-exports.test.ts b/tests/remove-exports.test.ts
index e907ca1..44f94d7 100644
--- a/tests/remove-exports.test.ts
+++ b/tests/remove-exports.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@rstest/core';
-import { parse, traverse } from '../src/babel';
+import { generate, parse, traverse } from '../src/babel';
import { removeExports, removeUnusedImports } from '../src/plugin-utils';
function hasTopLevelAssignment(ast: any, textIncludes: string): boolean {
@@ -73,4 +73,28 @@ describe('removeExports', () => {
expect(hasThemeImport).toBe(false);
});
+
+ it('keeps top-level declarations referenced from JSX after removing exports', () => {
+ const code = `
+ export function loader() {
+ return null;
+ }
+
+ function ProgressBar() {
+ return null;
+ }
+
+ export default function Route() {
+ return ;
+ }
+ `;
+
+ const ast = parse(code, { sourceType: 'module' });
+ removeExports(ast, ['loader']);
+
+ const result = generate(ast).code;
+
+ expect(result).toContain('function ProgressBar');
+ expect(result).toContain('