From 3d5d3fdfe1a93a3e6901997a1dacb4f439c1350e Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 30 Apr 2026 22:20:24 -0600 Subject: [PATCH 01/14] chore: treeshake-check-cli --- package.json | 2 + packages/davinci-client/package.json | 4 +- packages/davinci-client/tsconfig.json | 3 + packages/davinci-client/tsconfig.lib.json | 3 + packages/device-client/package.json | 4 +- packages/device-client/tsconfig.lib.json | 5 + packages/journey-client/package.json | 4 +- packages/journey-client/tsconfig.lib.json | 3 + packages/oidc-client/package.json | 4 +- packages/oidc-client/tsconfig.lib.json | 3 + packages/protect/package.json | 6 +- packages/protect/tsconfig.lib.json | 6 +- .../sdk-effects/iframe-manager/package.json | 6 +- .../iframe-manager/tsconfig.lib.json | 6 +- packages/sdk-effects/logger/package.json | 6 +- packages/sdk-effects/logger/tsconfig.lib.json | 6 +- packages/sdk-effects/oidc/package.json | 6 +- packages/sdk-effects/oidc/tsconfig.lib.json | 3 + .../sdk-request-middleware/package.json | 6 +- .../sdk-request-middleware/tsconfig.lib.json | 6 +- packages/sdk-effects/storage/package.json | 6 +- .../sdk-effects/storage/tsconfig.lib.json | 3 + packages/sdk-types/package.json | 6 +- packages/sdk-types/tsconfig.lib.json | 6 +- packages/sdk-utilities/package.json | 6 +- packages/sdk-utilities/tsconfig.lib.json | 3 + tools/treeshake-check/README.md | 280 ++++++++++++++++++ tools/treeshake-check/eslint.config.mjs | 25 ++ tools/treeshake-check/package.json | 29 ++ tools/treeshake-check/src/index.ts | 193 ++++++++++++ tools/treeshake-check/src/lib/analysis.ts | 122 ++++++++ tools/treeshake-check/src/lib/schemas.ts | 92 ++++++ .../src/lib/treeshake-check.ts | 173 +++++++++++ tools/treeshake-check/tsconfig.json | 13 + tools/treeshake-check/tsconfig.lib.json | 35 +++ tools/treeshake-check/tsconfig.spec.json | 41 +++ tools/treeshake-check/vitest.config.mts | 18 ++ 37 files changed, 1126 insertions(+), 17 deletions(-) create mode 100644 tools/treeshake-check/README.md create mode 100644 tools/treeshake-check/eslint.config.mjs create mode 100644 tools/treeshake-check/package.json create mode 100644 tools/treeshake-check/src/index.ts create mode 100644 tools/treeshake-check/src/lib/analysis.ts create mode 100644 tools/treeshake-check/src/lib/schemas.ts create mode 100644 tools/treeshake-check/src/lib/treeshake-check.ts create mode 100644 tools/treeshake-check/tsconfig.json create mode 100644 tools/treeshake-check/tsconfig.lib.json create mode 100644 tools/treeshake-check/tsconfig.spec.json create mode 100644 tools/treeshake-check/vitest.config.mts diff --git a/package.json b/package.json index cf61fc3292..184f9af57a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "serve": "nx serve", "test": "CI=true nx affected:test", "test:e2e": "CI=true nx affected:e2e", + "treeshake-check": "@forgerock/treeshake-check", "verdaccio": "nx local-registry", "watch": "nx vite:watch-deps" }, @@ -55,6 +56,7 @@ "@effect/cli": "catalog:effect", "@eslint/eslintrc": "^3.0.0", "@eslint/js": "~9.39.0", + "@forgerock/treeshake-check": "workspace:*", "@nx/devkit": "22.6.5", "@nx/eslint": "22.6.5", "@nx/eslint-plugin": "22.6.5", diff --git a/packages/davinci-client/package.json b/packages/davinci-client/package.json index eb3cb3a64a..5550a13459 100644 --- a/packages/davinci-client/package.json +++ b/packages/davinci-client/package.json @@ -23,7 +23,8 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@forgerock/sdk-logger": "workspace:*", @@ -38,6 +39,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:effect", + "@forgerock/treeshake-check": "workspace:*", "vitest": "catalog:vitest" }, "publishConfig": { diff --git a/packages/davinci-client/tsconfig.json b/packages/davinci-client/tsconfig.json index 141b4ebf5f..37a90ed519 100644 --- a/packages/davinci-client/tsconfig.json +++ b/packages/davinci-client/tsconfig.json @@ -29,6 +29,9 @@ { "path": "../sdk-effects/logger" }, + { + "path": "../../tools/treeshake-check" + }, { "path": "./tsconfig.lib.json" }, diff --git a/packages/davinci-client/tsconfig.lib.json b/packages/davinci-client/tsconfig.lib.json index 6c7bbdefef..66b6b93d19 100644 --- a/packages/davinci-client/tsconfig.lib.json +++ b/packages/davinci-client/tsconfig.lib.json @@ -48,6 +48,9 @@ }, { "path": "../sdk-effects/logger/tsconfig.lib.json" + }, + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" } ] } diff --git a/packages/device-client/package.json b/packages/device-client/package.json index 87ede1aaaa..e702b2db49 100644 --- a/packages/device-client/package.json +++ b/packages/device-client/package.json @@ -24,13 +24,15 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@forgerock/javascript-sdk": "4.7.0", "@reduxjs/toolkit": "catalog:" }, "devDependencies": { + "@forgerock/treeshake-check": "workspace:*", "msw": "catalog:" }, "nx": { diff --git a/packages/device-client/tsconfig.lib.json b/packages/device-client/tsconfig.lib.json index c0610b334d..f3dc59f4e7 100644 --- a/packages/device-client/tsconfig.lib.json +++ b/packages/device-client/tsconfig.lib.json @@ -19,5 +19,10 @@ "src/**/*.test.ts", "src/**/*.test.utils.ts", "src/lib/mock-data/*" + ], + "references": [ + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" + } ] } diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json index 99076ebee4..d26bbd6c1d 100644 --- a/packages/journey-client/package.json +++ b/packages/journey-client/package.json @@ -29,7 +29,8 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@forgerock/sdk-logger": "workspace:*", @@ -42,6 +43,7 @@ "tslib": "^2.3.0" }, "devDependencies": { + "@forgerock/treeshake-check": "workspace:*", "@vitest/coverage-v8": "catalog:vitest", "vite": "catalog:vite", "vitest": "catalog:vitest", diff --git a/packages/journey-client/tsconfig.lib.json b/packages/journey-client/tsconfig.lib.json index ca3f899b8d..9e37086f54 100644 --- a/packages/journey-client/tsconfig.lib.json +++ b/packages/journey-client/tsconfig.lib.json @@ -35,6 +35,9 @@ }, { "path": "../sdk-effects/logger/tsconfig.lib.json" + }, + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" } ] } diff --git a/packages/oidc-client/package.json b/packages/oidc-client/package.json index 6a71dd61ad..861a90db52 100644 --- a/packages/oidc-client/package.json +++ b/packages/oidc-client/package.json @@ -24,7 +24,8 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@forgerock/iframe-manager": "workspace:*", @@ -38,6 +39,7 @@ }, "devDependencies": { "@effect/vitest": "catalog:effect", + "@forgerock/treeshake-check": "workspace:*", "msw": "catalog:" }, "nx": { diff --git a/packages/oidc-client/tsconfig.lib.json b/packages/oidc-client/tsconfig.lib.json index d4b07d31d8..20a205b1b1 100644 --- a/packages/oidc-client/tsconfig.lib.json +++ b/packages/oidc-client/tsconfig.lib.json @@ -36,6 +36,9 @@ }, { "path": "../sdk-effects/iframe-manager/tsconfig.lib.json" + }, + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" } ], "exclude": [ diff --git a/packages/protect/package.json b/packages/protect/package.json index e820a25745..f10bd3af86 100644 --- a/packages/protect/package.json +++ b/packages/protect/package.json @@ -26,9 +26,13 @@ "scripts": { "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": {}, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" + }, "publishConfig": { "access": "public" }, diff --git a/packages/protect/tsconfig.lib.json b/packages/protect/tsconfig.lib.json index 5e2630f2ee..9c2c7e93ae 100644 --- a/packages/protect/tsconfig.lib.json +++ b/packages/protect/tsconfig.lib.json @@ -30,5 +30,9 @@ "src/**/*.types.test-d.ts", "src/**/*.utils.test-d.ts" ], - "references": [] + "references": [ + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" + } + ] } diff --git a/packages/sdk-effects/iframe-manager/package.json b/packages/sdk-effects/iframe-manager/package.json index 1ee1bebcd5..5d8a0a0fe4 100644 --- a/packages/sdk-effects/iframe-manager/package.json +++ b/packages/sdk-effects/iframe-manager/package.json @@ -28,9 +28,13 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": {}, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" + }, "nx": { "tags": ["scope:sdk-effects"] } diff --git a/packages/sdk-effects/iframe-manager/tsconfig.lib.json b/packages/sdk-effects/iframe-manager/tsconfig.lib.json index c0377d19ce..09f4a95f39 100644 --- a/packages/sdk-effects/iframe-manager/tsconfig.lib.json +++ b/packages/sdk-effects/iframe-manager/tsconfig.lib.json @@ -16,7 +16,11 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "references": [], + "references": [ + { + "path": "../../../tools/treeshake-check/tsconfig.lib.json" + } + ], "exclude": [ "vite.config.ts", "vite.config.mts", diff --git a/packages/sdk-effects/logger/package.json b/packages/sdk-effects/logger/package.json index 5a0b3d82c5..f10fd04ef3 100644 --- a/packages/sdk-effects/logger/package.json +++ b/packages/sdk-effects/logger/package.json @@ -29,9 +29,13 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": {}, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" + }, "nx": { "tags": ["scope:sdk-effects"] } diff --git a/packages/sdk-effects/logger/tsconfig.lib.json b/packages/sdk-effects/logger/tsconfig.lib.json index c0377d19ce..09f4a95f39 100644 --- a/packages/sdk-effects/logger/tsconfig.lib.json +++ b/packages/sdk-effects/logger/tsconfig.lib.json @@ -16,7 +16,11 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "references": [], + "references": [ + { + "path": "../../../tools/treeshake-check/tsconfig.lib.json" + } + ], "exclude": [ "vite.config.ts", "vite.config.mts", diff --git a/packages/sdk-effects/oidc/package.json b/packages/sdk-effects/oidc/package.json index 70f0d4afd7..bdfb1df79a 100644 --- a/packages/sdk-effects/oidc/package.json +++ b/packages/sdk-effects/oidc/package.json @@ -28,12 +28,16 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*" }, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" + }, "nx": { "tags": ["scope:sdk-effects"] } diff --git a/packages/sdk-effects/oidc/tsconfig.lib.json b/packages/sdk-effects/oidc/tsconfig.lib.json index fb8d083ca2..29f8551ee9 100644 --- a/packages/sdk-effects/oidc/tsconfig.lib.json +++ b/packages/sdk-effects/oidc/tsconfig.lib.json @@ -22,6 +22,9 @@ }, { "path": "../../sdk-types/tsconfig.lib.json" + }, + { + "path": "../../../tools/treeshake-check/tsconfig.lib.json" } ], "exclude": [ diff --git a/packages/sdk-effects/sdk-request-middleware/package.json b/packages/sdk-effects/sdk-request-middleware/package.json index bf72460770..a42f9540e5 100644 --- a/packages/sdk-effects/sdk-request-middleware/package.json +++ b/packages/sdk-effects/sdk-request-middleware/package.json @@ -28,11 +28,15 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@reduxjs/toolkit": "catalog:" }, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" + }, "nx": { "tags": ["scope:sdk-effects"] } diff --git a/packages/sdk-effects/sdk-request-middleware/tsconfig.lib.json b/packages/sdk-effects/sdk-request-middleware/tsconfig.lib.json index c0377d19ce..09f4a95f39 100644 --- a/packages/sdk-effects/sdk-request-middleware/tsconfig.lib.json +++ b/packages/sdk-effects/sdk-request-middleware/tsconfig.lib.json @@ -16,7 +16,11 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "references": [], + "references": [ + { + "path": "../../../tools/treeshake-check/tsconfig.lib.json" + } + ], "exclude": [ "vite.config.ts", "vite.config.mts", diff --git a/packages/sdk-effects/storage/package.json b/packages/sdk-effects/storage/package.json index 5f0cc23e02..b3c9ce9206 100644 --- a/packages/sdk-effects/storage/package.json +++ b/packages/sdk-effects/storage/package.json @@ -28,11 +28,15 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" }, "dependencies": { "@forgerock/sdk-types": "workspace:*" }, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" + }, "nx": { "tags": ["scope:sdk-effects"] } diff --git a/packages/sdk-effects/storage/tsconfig.lib.json b/packages/sdk-effects/storage/tsconfig.lib.json index bc8ee73537..f1a3461e91 100644 --- a/packages/sdk-effects/storage/tsconfig.lib.json +++ b/packages/sdk-effects/storage/tsconfig.lib.json @@ -21,6 +21,9 @@ "references": [ { "path": "../../sdk-types/tsconfig.lib.json" + }, + { + "path": "../../../tools/treeshake-check/tsconfig.lib.json" } ], "exclude": [ diff --git a/packages/sdk-types/package.json b/packages/sdk-types/package.json index cf67671799..5cf84310e6 100644 --- a/packages/sdk-types/package.json +++ b/packages/sdk-types/package.json @@ -28,7 +28,11 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" + }, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" }, "dependencies": {}, "nx": { diff --git a/packages/sdk-types/tsconfig.lib.json b/packages/sdk-types/tsconfig.lib.json index 1ddaa8a655..95658cb20f 100644 --- a/packages/sdk-types/tsconfig.lib.json +++ b/packages/sdk-types/tsconfig.lib.json @@ -16,7 +16,11 @@ "types": [] }, "include": ["src/**/*.ts"], - "references": [], + "references": [ + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" + } + ], "exclude": [ "vite.config.ts", "vite.config.mts", diff --git a/packages/sdk-utilities/package.json b/packages/sdk-utilities/package.json index 0f37be6be9..8f9bc8fa1b 100644 --- a/packages/sdk-utilities/package.json +++ b/packages/sdk-utilities/package.json @@ -37,7 +37,11 @@ "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", - "test:watch": "pnpm nx nxTest --watch" + "test:watch": "pnpm nx nxTest --watch", + "treeshake-check": "treeshake-check" + }, + "devDependencies": { + "@forgerock/treeshake-check": "workspace:*" }, "dependencies": { "@forgerock/sdk-types": "workspace:*" diff --git a/packages/sdk-utilities/tsconfig.lib.json b/packages/sdk-utilities/tsconfig.lib.json index e057aece5a..da64205691 100644 --- a/packages/sdk-utilities/tsconfig.lib.json +++ b/packages/sdk-utilities/tsconfig.lib.json @@ -22,6 +22,9 @@ "references": [ { "path": "../sdk-types/tsconfig.lib.json" + }, + { + "path": "../../tools/treeshake-check/tsconfig.lib.json" } ] } diff --git a/tools/treeshake-check/README.md b/tools/treeshake-check/README.md new file mode 100644 index 0000000000..262710a9fe --- /dev/null +++ b/tools/treeshake-check/README.md @@ -0,0 +1,280 @@ +# treeshake-check + +A tree-shakeability analyzer for npm packages. Tells you whether your package can be fully tree-shaken by Rollup, and when it can't, points at the specific files, exports, and likely causes preventing it. + +Built on [Effect](https://effect.website), [@effect/cli](https://www.npmjs.com/package/@effect/cli), and [Rollup](https://rollupjs.org). + +## Why this exists + +When you publish a library, consumers' bundlers (webpack, Rollup, Vite, esbuild) try to eliminate unused exports from your package — that's tree-shaking. If your package isn't shakeable, every consumer who imports a single function pulls in your entire library, inflating their bundle size. + +Tree-shakeability isn't visible from the outside. You can ship what looks like a clean ESM library and still have it be unshakeable due to a single `Object.defineProperty` call at module scope, a missing `"sideEffects": false` in `package.json`, or a transitive CJS dependency. This tool surfaces those problems. + +The technique is the same one used by Rich Harris's [agadoo](https://www.npmjs.com/package/agadoo): bundle your package as a side-effect-only import (`import "your-package"` with no bindings used) and see what Rollup couldn't eliminate. Anything that survives is what's preventing tree-shaking. + +## Installation + +In a monorepo, install once at the workspace root: + +\`\`\`bash +pnpm add -Dw treeshake-check +\`\`\` + +For a standalone project: + +\`\`\`bash +pnpm add -D treeshake-check +\`\`\` + +## Usage + +### Quickstart + +From any package directory: + +\`\`\`bash +pnpm treeshake-check +\`\`\` + +You'll get one of two outcomes: + +- **Fully tree-shakeable** — ASCII tree celebration plus any recommendations for `package.json` improvements (typically `"sideEffects": false`). +- **Has side effects** — a per-module breakdown of what survived, with diagnostic info for each file. + +### Flags + +| Flag | Alias | Description | +| ---------------- | ----- | ------------------------------------------------------------------------------- | +| `--cwd ` | `-C` | Directory containing `package.json`. Defaults to the current working directory. | +| `--entry ` | `-e` | Analyze a specific entry file directly, skipping `package.json` resolution. | +| `--json` | | Emit machine-readable JSON instead of human output. | +| `--quiet` | `-q` | Suppress all output; rely on the exit code only. | +| `--top ` | | Show only the N modules with the largest surviving byte count. | + +Plus the standard `--help`, `--version`, `--wizard`, and `--completions ` flags from `@effect/cli`. + +### Examples + +Check the current package: + +\`\`\`bash +pnpm treeshake-check +\`\`\` + +Check a different package in the workspace: + +\`\`\`bash +pnpm treeshake-check --cwd packages/my-sdk +\`\`\` + +Check a specific built file directly: + +\`\`\`bash +pnpm treeshake-check --entry dist/index.js +\`\`\` + +Show only the worst 5 offenders: + +\`\`\`bash +pnpm treeshake-check --top 5 +\`\`\` + +JSON output for CI tooling: + +\`\`\`bash +pnpm treeshake-check --json | jq '.modules[] | {id, renderedLength, suspectedCauses}' +\`\`\` + +## Output + +### Fully shakeable + +\`\`\` +tree88shakey +TREESHAKEtRe eSha +kETREESHaKetreeshAKE +TreeShakEY o0o tREeSHAKE +Es6 /T r eesHakeY +\\\/// /Thanks +\\////// +||||| +||||| +||||| +.....//||||\\.... +Awesome! Your code is 100% tree-shakeable! + +Notes: +• Add "sideEffects": false to package.json. Without it, bundlers +conservatively assume every module may have side effects, which +blocks aggressive tree-shaking by consumers. +\`\`\` + +### Has side effects + +\`\`\` +Not fully tree-shakeable: 487 of 2840 bytes survived (17.1%). + +Per-module breakdown: + +/path/to/src/utils.js +bytes: 487/1240 (39.3% survived) +exports: rendered=[configureLogger] removed=[isString, isNumber] +likely: PrototypeMutation, TopLevelSideEffect + +Recommendations: +• Add "sideEffects": false to package.json... +\`\`\` + +### Reading the output + +Each module entry tells you: + +- **Path** — the file rollup couldn't eliminate +- **bytes** — how much of the file survived versus its original size +- **exports** — `rendered` exports were kept (these are your investigation targets); `removed` exports were successfully shaken +- **likely** — heuristic labels for the kind of side effect detected: + - `TopLevelSideEffect` — a top-level statement with observable effects + - `PrototypeMutation` — `Object.defineProperty`, `.prototype.x = ...`, etc. + - `GlobalAssignment` — assignment to `window`, `globalThis`, `self`, or `global` + - `CommonJsContamination` — `require()`, `module.exports`, `__esModule` artifacts + - `UnannotatedCall` — top-level function call without `/*#__PURE__*/` + - `Unknown` — none of the above patterns matched (look at the code yourself) + +The labels are heuristic. They're a starting point for investigation, not a verdict. + +## Common fixes + +### "Add `sideEffects: false`" + +The single highest-leverage fix for most packages. Add to `package.json`: + +\`\`\`json +{ +"sideEffects": false +} +\`\`\` + +This declares to bundlers that no module in your package has observable side effects from being imported. Without it, bundlers conservatively assume every module _might_ have side effects and refuse to shake aggressively. + +If some files genuinely have side effects (CSS imports, polyfills, modules that register globals), use the array form to whitelist them: + +\`\`\`json +{ +"sideEffects": [ +"**/*.css", +"./src/polyfill.js" +] +} +\`\`\` + +### Top-level function calls + +A bare top-level call like `init()` or `Object.defineProperty(target, key, descriptor)` is treated as side-effectful by default. If you know the call is pure, annotate it: + +\`\`\`js +// Before — kept by tree-shaking +const result = computeOnce(); + +// After — eligible for tree-shaking when unused +const result = /_#**PURE**_/ computeOnce(); +\`\`\` + +### CommonJS contamination + +If `likely: CommonJsContamination` shows up, your package or one of its dependencies is shipping CJS, which can't be statically analyzed for tree-shaking. Either: + +- Add a `module` field to `package.json` pointing to an ESM build +- Set `"type": "module"` +- Replace CJS dependencies with ESM equivalents +- For unavoidable CJS deps, mark them as external in your build + +## Programmatic usage + +The CLI is a thin wrapper around library functions you can use directly: + +\`\`\`typescript +import { Effect } from "effect"; +import { NodeContext } from "@effect/platform-node"; +import { checkPackage } from "treeshake-check"; + +const program = Effect.gen(function* () { +const result = yield* checkPackage("./packages/sdk"); + +if (result.\_tag === "FullyTreeshakeable") { +return true; +} + +for (const m of result.modules) { +console.log(`${m.id}: ${m.renderedLength}/${m.originalLength} bytes`); +console.log(` causes: ${m.suspectedCauses.join(", ")}`); +} +return false; +}); + +const isShakeable = await Effect.runPromise( +program.pipe(Effect.provide(NodeContext.layer)) +); +\`\`\` + +The result types and schemas are exported from `treeshake-check/schemas`, the analyzers from `treeshake-check/analysis`. See those modules' source for the full surface. + +## CI integration + +`treeshake-check` exits with code 1 when a package isn't fully shakeable, so it composes naturally as a quality gate. + +### GitHub Actions + +\`\`\`yaml + +- name: Tree-shake check + run: pnpm -r --filter "./packages/\*" exec treeshake-check --top 5 + \`\`\` + +### As a pre-publish hook + +\`\`\`json +{ +"scripts": { +"prepublishOnly": "treeshake-check --quiet" +} +} +\`\`\` + +A regression literally cannot ship. + +### Across a monorepo + +\`\`\`bash +pnpm -r --parallel exec treeshake-check --top 3 +\`\`\` + +`-r` runs in every workspace package; `--parallel` runs them concurrently since they're independent. + +To skip packages that don't have a shakeable entry (meta-packages, internal tooling), filter: + +\`\`\`bash +pnpm -r --filter "@my-org/sdk-\*" exec treeshake-check +\`\`\` + +## How it works + +1. Reads `package.json` from the target directory and resolves the entry point (`module` preferred, falling back to `main`). +2. Constructs a synthetic Rollup entry that imports the target as a side-effect-only import: `import "/absolute/path/to/entry.js"`. +3. Runs Rollup with default tree-shaking enabled. +4. Inspects `chunk.modules` for per-module `renderedLength`, `renderedExports`, and `removedExports`. +5. Heuristically classifies surviving code by pattern (regex over the rendered output). +6. Reports per-module statistics, surviving code, and `package.json` recommendations. + +The synthetic-entry trick is key: when you import a module without using any of its exports, anything that survives bundling must be there because Rollup believes evaluating the module has observable side effects. That's exactly the question we want to answer. + +## Limitations + +- **Heuristic cause detection.** Cause labels come from regex over the rendered code. False positives and missed cases are possible. The labels are diagnostic hints, not authoritative classifications. +- **Single-entry analysis.** The tool checks one entry at a time. Packages with multiple entry points (`exports` field with subpaths) need multiple invocations, one per entry. +- **No transitive cause tracing.** When module A is impure because it imports module B which imports module C, the tool reports A as the offender. Tracing back to C requires reading the surviving code. +- **Rollup-specific.** Other bundlers' tree-shaking may differ. webpack and esbuild use different heuristics; a package that's shakeable in Rollup is _usually_ shakeable in others, but not guaranteed. + +## Prior art + +- [agadoo](https://www.npmjs.com/package/agadoo) by Rich Harris — same technique, the original implementation. This package adds richer diagnostics, structured output, and Effect-based composition for use as a library. +- [bundle-phobia](https://bundlephobia.com) — measures the post-shake size from a consumer's perspective rather than analyzing why shaking succeeds or fails. diff --git a/tools/treeshake-check/eslint.config.mjs b/tools/treeshake-check/eslint.config.mjs new file mode 100644 index 0000000000..0a23ec0779 --- /dev/null +++ b/tools/treeshake-check/eslint.config.mjs @@ -0,0 +1,25 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: [ + '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}', + '{projectRoot}/vite.config.{js,ts,mjs,mts}', + ], + }, + ], + }, + languageOptions: { + parser: await import('jsonc-eslint-parser'), + }, + }, + { + ignores: ['**/out-tsc'], + }, +]; diff --git a/tools/treeshake-check/package.json b/tools/treeshake-check/package.json new file mode 100644 index 0000000000..80cdfe39d1 --- /dev/null +++ b/tools/treeshake-check/package.json @@ -0,0 +1,29 @@ +{ + "name": "@forgerock/treeshake-check", + "version": "0.0.1", + "private": true, + "description": "check packages for treeshaking", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "treeshake-check": "./dist/index.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "dependencies": { + "@effect/cli": "^0.75.1", + "@effect/platform": "catalog:effect", + "@effect/platform-node": "catalog:effect", + "@rollup/plugin-virtual": "^3.0.2", + "effect": "catalog:effect", + "rollup": "^4.59.0" + } +} diff --git a/tools/treeshake-check/src/index.ts b/tools/treeshake-check/src/index.ts new file mode 100644 index 0000000000..2b9e1526bd --- /dev/null +++ b/tools/treeshake-check/src/index.ts @@ -0,0 +1,193 @@ +#!/usr/bin/env node +// src/index.ts +import { Command, Options } from '@effect/cli'; +import { NodeContext, NodeRuntime } from '@effect/platform-node'; +import { Console, Effect, Option } from 'effect'; +import { analyzeTreeshakeability, checkPackage } from './lib/treeshake-check.js'; +import type { TreeshakeResult } from './lib/schemas.js'; + +const tree = ` + \\\\/// /Thanks + \\\\////// + ||||| + ||||| + ||||| + .....//||||\\\\.... +Awesome! Your code is 100% tree-shakeable! +`; + +// ─── Options ───────────────────────────────────────────────────────────────── + +const cwd = Options.directory('cwd', { exists: 'yes' }).pipe( + Options.withAlias('C'), + Options.withDescription( + 'Directory containing the package.json to analyze. Defaults to the current working directory.', + ), + Options.optional, +); + +const entry = Options.file('entry', { exists: 'yes' }).pipe( + Options.withAlias('e'), + Options.withDescription( + 'Analyze a specific entry file directly, skipping package.json resolution.', + ), + Options.optional, +); + +const json = Options.boolean('json').pipe( + Options.withDescription('Emit machine-readable JSON instead of human-readable output.'), +); + +const quiet = Options.boolean('quiet').pipe( + Options.withAlias('q'), + Options.withDescription('Suppress all output; rely on exit code only.'), +); + +const top = Options.integer('top').pipe( + Options.withDescription('Show only the N modules with the largest surviving byte count.'), + Options.optional, +); + +// ─── Renderers ─────────────────────────────────────────────────────────────── + +const pct = (n: number, total: number) => + total === 0 ? '0%' : `${((n / total) * 100).toFixed(1)}%`; + +const renderJson = (result: TreeshakeResult) => Console.log(JSON.stringify(result, null, 2)); + +const renderHuman = (result: TreeshakeResult, topN: Option.Option) => + Effect.gen(function* () { + if (result._tag === 'FullyTreeshakeable') { + yield* Console.info(tree); + if (result.hints.recommendations.length > 0) { + yield* Console.info('\nNotes:'); + for (const rec of result.hints.recommendations) { + yield* Console.info(` • ${rec}`); + } + } + return; + } + + yield* Console.info( + `Not fully tree-shakeable: ${result.totalRenderedBytes} of ` + + `${result.totalOriginalBytes} bytes survived ` + + `(${pct(result.totalRenderedBytes, result.totalOriginalBytes)}).\n`, + ); + + // Sort modules worst-first; optionally truncate + const sorted = [...result.modules].sort((a, b) => b.renderedLength - a.renderedLength); + const shown = Option.match(topN, { + onNone: () => sorted, + onSome: (n) => sorted.slice(0, n), + }); + + yield* Console.info( + Option.isSome(topN) ? `Top ${shown.length} unshaken modules:` : 'Per-module breakdown:', + ); + + for (const m of shown) { + const exportInfo = + m.renderedExports.length === 0 && m.removedExports.length === 0 + ? ' exports: (none)' + : ` exports: rendered=[${m.renderedExports.join(', ')}] ` + + `removed=[${m.removedExports.join(', ')}]`; + + yield* Console.info( + `\n ${m.id}\n` + + ` bytes: ${m.renderedLength}/${m.originalLength} ` + + `(${pct(m.renderedLength, m.originalLength)} survived)\n` + + `${exportInfo}\n` + + ` likely: ${m.suspectedCauses.join(', ')}`, + ); + } + + if (Option.isSome(topN) && sorted.length > Option.getOrThrow(topN)) { + yield* Console.info(`\n ...and ${sorted.length - Option.getOrThrow(topN)} more.`); + } + + if (result.warnings.length > 0) { + yield* Console.info('\nRollup warnings:'); + for (const w of result.warnings) { + const loc = w.loc ? ` (${w.loc.file ?? '?'}:${w.loc.line}:${w.loc.column})` : ''; + yield* Console.info(` [${w.code ?? 'WARN'}] ${w.message}${loc}`); + } + } + + if (result.hints.recommendations.length > 0) { + yield* Console.info('\nRecommendations:'); + for (const rec of result.hints.recommendations) { + yield* Console.info(` • ${rec}`); + } + } + }); + +// ─── Command ───────────────────────────────────────────────────────────────── + +const command = Command.make( + 'treeshake-check', + { cwd, entry, json, quiet, top }, + ({ cwd, entry, json, quiet, top }) => + Effect.gen(function* () { + const result = yield* Option.match(entry, { + onNone: () => checkPackage(Option.getOrUndefined(cwd)), + onSome: (entryPath) => analyzeTreeshakeability(entryPath), + }); + + if (!quiet) { + yield* json ? renderJson(result) : renderHuman(result, top); + } + + // Non-zero exit when shaking failed, so this composes as a CI gate. + // Use process.exitCode (not process.exit) so any in-flight stdout + // writes flush before Node exits. + if (result._tag === 'HasSideEffects') { + yield* Effect.sync(() => { + process.exitCode = 1; + }); + } + }), +).pipe(Command.withDescription('Check whether a package can be fully tree-shaken by Rollup.')); + +const cli = Command.run(command, { + name: 'Treeshake Check', + version: '1.0.0', +}); + +cli(process.argv).pipe( + Effect.catchTags({ + PackageJsonNotFound: (e) => + Console.error(`error: package.json not found at ${e.path}`).pipe( + Effect.zipRight( + Effect.sync(() => { + process.exitCode = 1; + }), + ), + ), + MissingEntryPoint: (e) => + Console.error(`error: package.json at ${e.path} has no "module" or "main" entry`).pipe( + Effect.zipRight( + Effect.sync(() => { + process.exitCode = 1; + }), + ), + ), + BundleFailed: (e) => + Console.error(`error: bundling failed — ${String(e.cause)}`).pipe( + Effect.zipRight( + Effect.sync(() => { + process.exitCode = 1; + }), + ), + ), + ParseError: (e) => + Console.error(`error: invalid package.json — ${e.message}`).pipe( + Effect.zipRight( + Effect.sync(() => { + process.exitCode = 1; + }), + ), + ), + }), + Effect.provide(NodeContext.layer), + NodeRuntime.runMain as any, +); diff --git a/tools/treeshake-check/src/lib/analysis.ts b/tools/treeshake-check/src/lib/analysis.ts new file mode 100644 index 0000000000..2ebf74fc06 --- /dev/null +++ b/tools/treeshake-check/src/lib/analysis.ts @@ -0,0 +1,122 @@ +// src/lib/analysis.ts +import type { ModuleAnalysis, PackageJson, PackageJsonHints, SuspectedCause } from './schemas.js'; + +/** + * Heuristic detection of common patterns that prevent tree-shaking. + * + * This is regex-based and approximate — a rigorous implementation would + * AST-walk the surviving code. But these patterns catch the vast majority + * of real-world cases, and labeling them "suspected" makes the uncertainty + * honest. + */ +export const detectCauses = (code: string): ReadonlyArray => { + const causes = new Set(); + + // CJS contamination — these strings shouldn't appear in clean ESM output + if (/\b(require\s*\(|module\.exports|exports\.[a-zA-Z_$]|__esModule)/.test(code)) { + causes.add('CommonJsContamination'); + } + + // Object.defineProperty / Object.assign / prototype mutations + if (/Object\.(defineProperty|defineProperties|assign|setPrototypeOf|freeze)\s*\(/.test(code)) { + causes.add('PrototypeMutation'); + } + if (/\.prototype\.[a-zA-Z_$]+\s*=/.test(code)) { + causes.add('PrototypeMutation'); + } + + // Top-level assignment to global-ish names + if (/^\s*(window|globalThis|self|global)\.[a-zA-Z_$]/m.test(code)) { + causes.add('GlobalAssignment'); + } + + // Bare top-level function call without /*#__PURE__*/ annotation + // Approximate but catches the common `someInit()` / `extend(Foo, Bar)` case + const topLevelCall = /^(?!.*\/\*#__PURE__\*\/)\s*[a-zA-Z_$][\w$]*\s*\(/m; + if (topLevelCall.test(code)) { + causes.add('UnannotatedCall'); + causes.add('TopLevelSideEffect'); + } + + if (causes.size === 0) causes.add('Unknown'); + return Array.from(causes); +}; + +/** + * Build a single ModuleAnalysis from rollup's per-module rendered info. + */ +export const buildModuleAnalysis = ( + id: string, + m: { + originalLength: number; + renderedLength: number; + renderedExports: ReadonlyArray; + removedExports: ReadonlyArray; + code: string | null; + }, +): ModuleAnalysis => ({ + id, + originalLength: m.originalLength, + renderedLength: m.renderedLength, + shakingRatio: m.originalLength === 0 ? 0 : m.renderedLength / m.originalLength, + renderedExports: [...m.renderedExports], + removedExports: [...m.removedExports], + survivingCode: m.code, + suspectedCauses: m.code ? detectCauses(m.code) : ['Unknown'], +}); + +/** + * Inspect a parsed package.json and produce hints + recommendations. + * + * The single highest-leverage fix for most real-world libraries is + * declaring `"sideEffects": false`, so that recommendation comes first. + */ +export const analyzePackageJsonHints = (pkg: PackageJson): PackageJsonHints => { + const hasSideEffectsField = pkg.sideEffects !== undefined; + const hasModuleField = pkg.module !== undefined; + const hasTypeModule = pkg.type === 'module'; + + const recommendations: string[] = []; + + if (!hasSideEffectsField) { + recommendations.push( + 'Add "sideEffects": false to package.json. Without it, bundlers ' + + 'conservatively assume every module may have side effects, which ' + + 'blocks aggressive tree-shaking by consumers.', + ); + } + + if (!hasModuleField && pkg.main !== undefined) { + recommendations.push( + 'Add a "module" field pointing to an ESM build. The "main" field ' + + 'traditionally points to CommonJS, which cannot be statically ' + + 'analyzed for tree-shaking.', + ); + } + + if (!hasTypeModule && !hasModuleField) { + recommendations.push( + 'Add "type": "module" to package.json, or provide a separate ' + + '"module" entry for ESM consumers.', + ); + } + + return { + hasSideEffectsField, + sideEffectsValue: pkg.sideEffects, + hasModuleField, + hasTypeModule, + recommendations, + }; +}; + +/** + * Default hints for cases where we analyze a raw entry path without a + * package.json (e.g., when the user passes --entry directly). + */ +export const defaultHints = (): PackageJsonHints => ({ + hasSideEffectsField: false, + hasModuleField: false, + hasTypeModule: false, + recommendations: [], +}); diff --git a/tools/treeshake-check/src/lib/schemas.ts b/tools/treeshake-check/src/lib/schemas.ts new file mode 100644 index 0000000000..845c3c1a2e --- /dev/null +++ b/tools/treeshake-check/src/lib/schemas.ts @@ -0,0 +1,92 @@ +// src/lib/schemas.ts +import { Schema } from 'effect'; + +// ─── package.json (the bits we care about) ─────────────────────────────────── + +export const SideEffectsValue = Schema.Union(Schema.Boolean, Schema.Array(Schema.String)); + +export const PackageJson = Schema.Struct({ + name: Schema.optional(Schema.String), + module: Schema.optional(Schema.String), + main: Schema.optional(Schema.String), + type: Schema.optional(Schema.Literal('module', 'commonjs')), + sideEffects: Schema.optional(SideEffectsValue), + dependencies: Schema.optional(Schema.Unknown), + peerDependencies: Schema.optional(Schema.Unknown), + devDependencies: Schema.optional(Schema.Unknown), +}); +export type PackageJson = typeof PackageJson.Type; + +// JSON-string → validated PackageJson in one shot +export const PackageJsonFromString = Schema.parseJson(PackageJson); + +// ─── Per-module analysis ───────────────────────────────────────────────────── + +export const SuspectedCause = Schema.Literal( + 'TopLevelSideEffect', + 'PrototypeMutation', + 'GlobalAssignment', + 'CommonJsContamination', + 'UnannotatedCall', + 'Unknown', +); +export type SuspectedCause = typeof SuspectedCause.Type; + +export const ModuleAnalysis = Schema.Struct({ + id: Schema.String, + originalLength: Schema.Number, + renderedLength: Schema.Number, + // 0 = fully shaken, 1 = nothing shaken + shakingRatio: Schema.Number, + renderedExports: Schema.Array(Schema.String), + removedExports: Schema.Array(Schema.String), + survivingCode: Schema.NullOr(Schema.String), + suspectedCauses: Schema.Array(SuspectedCause), +}); +export type ModuleAnalysis = typeof ModuleAnalysis.Type; + +// ─── Rollup warnings (captured during build) ───────────────────────────────── + +export const RollupWarning = Schema.Struct({ + code: Schema.optional(Schema.String), + message: Schema.String, + id: Schema.optional(Schema.String), + loc: Schema.optional( + Schema.Struct({ + file: Schema.optional(Schema.String), + line: Schema.Number, + column: Schema.Number, + }), + ), +}); +export type RollupWarning = typeof RollupWarning.Type; + +// ─── package.json hints ────────────────────────────────────────────────────── + +export const PackageJsonHints = Schema.Struct({ + hasSideEffectsField: Schema.Boolean, + sideEffectsValue: Schema.optional(SideEffectsValue), + hasModuleField: Schema.Boolean, + hasTypeModule: Schema.Boolean, + recommendations: Schema.Array(Schema.String), +}); +export type PackageJsonHints = typeof PackageJsonHints.Type; + +// ─── Top-level result ──────────────────────────────────────────────────────── + +export const TreeshakeResult = Schema.Union( + Schema.Struct({ + _tag: Schema.Literal('FullyTreeshakeable'), + hints: PackageJsonHints, + }), + Schema.Struct({ + _tag: Schema.Literal('HasSideEffects'), + totalOriginalBytes: Schema.Number, + totalRenderedBytes: Schema.Number, + modules: Schema.Array(ModuleAnalysis), + warnings: Schema.Array(RollupWarning), + hints: PackageJsonHints, + unshakenCode: Schema.String, + }), +); +export type TreeshakeResult = typeof TreeshakeResult.Type; diff --git a/tools/treeshake-check/src/lib/treeshake-check.ts b/tools/treeshake-check/src/lib/treeshake-check.ts new file mode 100644 index 0000000000..a81431b7f1 --- /dev/null +++ b/tools/treeshake-check/src/lib/treeshake-check.ts @@ -0,0 +1,173 @@ +// src/lib/treeshake-check.ts +import { FileSystem, Path } from '@effect/platform'; +import { Data, Effect, Schema } from 'effect'; +import virtualPlugin from '@rollup/plugin-virtual'; +import { rollup, type RollupBuild } from 'rollup'; +import { analyzePackageJsonHints, buildModuleAnalysis, defaultHints } from './analysis.js'; +import { + PackageJsonFromString, + type PackageJson, + type RollupWarning, + type TreeshakeResult, +} from './schemas.js'; + +// Type assertion for the virtual plugin to handle TypeScript compatibility +const virtual = virtualPlugin as any; + +// ─── Errors ────────────────────────────────────────────────────────────────── + +export class PackageJsonNotFound extends Data.TaggedError('PackageJsonNotFound')<{ + readonly path: string; +}> {} + +export class MissingEntryPoint extends Data.TaggedError('MissingEntryPoint')<{ + readonly path: string; +}> {} + +export class BundleFailed extends Data.TaggedError('BundleFailed')<{ + readonly cause: unknown; +}> {} + +// ─── Read & validate package.json ──────────────────────────────────────────── + +const readPackageJson = (cwd: string) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + + const pkgPath = path.join(cwd, 'package.json'); + const exists = yield* fs.exists(pkgPath); + if (!exists) { + return yield* new PackageJsonNotFound({ path: pkgPath }); + } + + const contents = yield* fs.readFileString(pkgPath, 'utf-8'); + const pkg = yield* Schema.decodeUnknown(PackageJsonFromString)(contents); + + return { pkg, pkgPath } as const; + }); + +export const getEntryFromPackageJson = (cwd?: string) => + Effect.gen(function* () { + const { pkg, pkgPath } = yield* readPackageJson(cwd ?? process.cwd()); + const entry = pkg.module ?? pkg.main; + if (entry === undefined) { + return yield* new MissingEntryPoint({ path: pkgPath }); + } + return { entry, pkg } as const; + }); + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +const closeBundle = (bundle: RollupBuild) => + Effect.tryPromise(() => bundle.close()).pipe( + Effect.catchAll((cause) => + Effect.logWarning('Failed to close rollup bundle').pipe( + Effect.annotateLogs('cause', String(cause)), + ), + ), + ); + +/** + * Drop the synthetic virtual entry — `@rollup/plugin-virtual` prefixes its + * module IDs with `\0virtual:`. The leading null byte is sometimes stripped + * before the ID surfaces in `chunk.modules`, sometimes not, so we check the + * realistic forms plus a permissive `:treeshake` suffix as a catch-all. + */ +const isSyntheticEntry = (id: string) => + id === 'treeshake' || + id === 'virtual:treeshake' || + id === '\0virtual:treeshake' || + id.endsWith(':treeshake'); + +// ─── Rollup bundling ───────────────────────────────────────────────────────── + +/** + * Bundle the entry as the *only* import in a synthetic virtual module. + * If rollup can statically determine the entry has no observable side + * effects, every real module renders to zero bytes — that's our "fully + * shakeable" signal. Anything that survives is what's preventing + * tree-shaking. + */ +export const analyzeTreeshakeability = ( + entry: string, + pkg?: PackageJson, +): Effect.Effect => + Effect.gen(function* () { + const path = yield* Path.Path; + const resolvedEntry = path.resolve(entry); + const warnings: RollupWarning[] = []; + + const bundle = yield* Effect.acquireRelease( + Effect.tryPromise({ + try: () => + rollup({ + input: 'treeshake', + plugins: [ + virtual({ + treeshake: `import ${JSON.stringify(resolvedEntry)}`, + }), + ], + onwarn: (warning) => { + if (warning.code === 'EMPTY_BUNDLE') return; + warnings.push({ + code: warning.code, + message: warning.message, + id: warning.id, + loc: warning.loc, + }); + }, + }), + catch: (cause) => new BundleFailed({ cause }), + }), + closeBundle, + ); + + const output = yield* Effect.tryPromise({ + try: () => bundle.generate({ format: 'esm' }), + catch: (cause) => new BundleFailed({ cause }), + }); + + const chunk = output.output[0]; + const hints = pkg ? analyzePackageJsonHints(pkg) : defaultHints(); + + // Build per-module analyses, skipping the synthetic virtual entry. + const modules = Object.entries(chunk.modules) + .filter(([id]) => !isSyntheticEntry(id)) + .map(([id, m]) => buildModuleAnalysis(id, m)); + + const totalOriginalBytes = modules.reduce((s, m) => s + m.originalLength, 0); + const totalRenderedBytes = modules.reduce((s, m) => s + m.renderedLength, 0); + + // The package is fully shakeable when none of the real modules have + // surviving code. The bundle's `chunk.code` may still contain a tiny + // bit of rollup glue, but that's not the user's code. + const isFullyShakeable = modules.length === 0 || totalRenderedBytes === 0; + + if (isFullyShakeable) { + return { _tag: 'FullyTreeshakeable', hints } as const; + } + + const offenders = modules.filter((m) => m.renderedLength > 0); + + return { + _tag: 'HasSideEffects', + totalOriginalBytes, + totalRenderedBytes, + modules: modules.filter((m) => m.renderedLength > 0), // only show actual offenders + warnings, + hints, + unshakenCode: offenders.map((m) => `// ${m.id}\n${m.survivingCode ?? ''}`).join('\n\n'), + } as const; + }).pipe(Effect.scoped); + +// ─── Composed pipeline ─────────────────────────────────────────────────────── + +/** + * Full check: read package.json from `cwd`, resolve its entry, then analyze. + */ +export const checkPackage = (cwd?: string) => + Effect.gen(function* () { + const { entry, pkg } = yield* getEntryFromPackageJson(cwd); + return yield* analyzeTreeshakeability(entry, pkg); + }); diff --git a/tools/treeshake-check/tsconfig.json b/tools/treeshake-check/tsconfig.json new file mode 100644 index 0000000000..62ebbd9464 --- /dev/null +++ b/tools/treeshake-check/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/tools/treeshake-check/tsconfig.lib.json b/tools/treeshake-check/tsconfig.lib.json new file mode 100644 index 0000000000..56716595b5 --- /dev/null +++ b/tools/treeshake-check/tsconfig.lib.json @@ -0,0 +1,35 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "verbatimModuleSyntax": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] +} diff --git a/tools/treeshake-check/tsconfig.spec.json b/tools/treeshake-check/tsconfig.spec.json new file mode 100644 index 0000000000..1060f1e9a2 --- /dev/null +++ b/tools/treeshake-check/tsconfig.spec.json @@ -0,0 +1,41 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/tools/treeshake-check/vitest.config.mts b/tools/treeshake-check/vitest.config.mts new file mode 100644 index 0000000000..b330e1751a --- /dev/null +++ b/tools/treeshake-check/vitest.config.mts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/tools/treeshake-check', + test: { + name: 'treeshake-check', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + }, + }, +})); From c6201278ae91c73b84121dd77fdab094ae82f2de Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 30 Apr 2026 22:35:54 -0600 Subject: [PATCH 02/14] chore: make-cli-more-readable --- nx.json | 18 ++- .../api-report/davinci-client.api.md | 12 +- .../api-report/davinci-client.types.api.md | 12 +- pnpm-lock.yaml | 122 ++++++++++++++- tools/treeshake-check/src/index.ts | 119 +++++++++++---- tools/treeshake-check/src/lib/analysis.ts | 24 ++- tools/treeshake-check/src/lib/explanations.ts | 144 ++++++++++++++++++ tools/treeshake-check/src/lib/schemas.ts | 1 + tsconfig.json | 3 + 9 files changed, 403 insertions(+), 52 deletions(-) create mode 100644 tools/treeshake-check/src/lib/explanations.ts diff --git a/nx.json b/nx.json index 58b3b1d998..69d8ec8a18 100644 --- a/nx.json +++ b/nx.json @@ -86,6 +86,11 @@ "dependsOn": ["^test:coverage"], "outputs": ["{projectRoot}/./coverage"], "cache": true + }, + "treeshake-check": { + "inputs": ["production", "^production"], + "dependsOn": ["build"], + "cache": true } }, "sync": { @@ -103,7 +108,8 @@ "configName": "tsconfig.lib.json" } }, - "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"] + "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"], + "exclude": ["tools/treeshake-check/*"] }, { "plugin": "@nx/playwright/plugin", @@ -136,7 +142,7 @@ }, { "plugin": "@nx/js/typescript", - "include": ["packages/journey-client/**"], + "include": ["packages/journey-client/**", "tools/treeshake-check/*"], "options": { "typecheck": { "targetName": "typecheck" @@ -153,6 +159,14 @@ "testTargetName": "nxTest" }, "include": ["packages/**/**/*", "e2e/**/**/*", "tools/**/**/*"] + }, + { + "plugin": "@nx/vitest", + "options": { + "testTargetName": "vitest:test", + "ciTargetName": "test-ci", + "testMode": "watch" + } } ], "parallel": 1, diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index b2528bf664..33cb405246 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -267,13 +267,11 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; poll: (collector: PollingCollector) => Poller; getClient: () => { - status: "start"; - } | { action: string; collectors: Collectors[]; description?: string; @@ -287,6 +285,8 @@ export function davinci(input: { status: "error"; } | { status: "failure"; + } | { + status: "start"; } | { authorization?: { code?: string; @@ -297,7 +297,7 @@ export function davinci(input: { getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; getServer: () => { _links?: Links; id?: string; @@ -306,8 +306,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -323,6 +321,8 @@ export function davinci(input: { interactionId?: string; interactionToken?: string; status: "failure"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index 2321431a0a..b4897b665c 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -267,13 +267,11 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; poll: (collector: PollingCollector) => Poller; getClient: () => { - status: "start"; - } | { action: string; collectors: Collectors[]; description?: string; @@ -287,6 +285,8 @@ export function davinci(input: { status: "error"; } | { status: "failure"; + } | { + status: "start"; } | { authorization?: { code?: string; @@ -297,7 +297,7 @@ export function davinci(input: { getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; getServer: () => { _links?: Links; id?: string; @@ -306,8 +306,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -323,6 +321,8 @@ export function davinci(input: { interactionId?: string; interactionToken?: string; status: "failure"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index babd5d7de7..a3f9dfdb06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: '@evilmartians/lefthook': specifier: ^2.1.4 version: 2.1.6 + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:tools/treeshake-check '@nx/devkit': specifier: 22.6.5 version: 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) @@ -443,6 +446,9 @@ importers: '@effect/vitest': specifier: catalog:effect version: 0.27.0(effect@3.20.0)(vitest@3.2.4) + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check vitest: specifier: catalog:vitest version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) @@ -456,6 +462,9 @@ importers: specifier: 'catalog:' version: 2.10.1 devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check msw: specifier: 'catalog:' version: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) @@ -487,6 +496,9 @@ importers: specifier: ^2.3.0 version: 2.8.1 devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check '@vitest/coverage-v8': specifier: catalog:vitest version: 3.2.4(vitest@3.2.4) @@ -530,15 +542,30 @@ importers: '@effect/vitest': specifier: catalog:effect version: 0.27.0(effect@3.20.0)(vitest@3.2.4) + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check msw: specifier: 'catalog:' version: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) - packages/protect: {} + packages/protect: + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check - packages/sdk-effects/iframe-manager: {} + packages/sdk-effects/iframe-manager: + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../../tools/treeshake-check - packages/sdk-effects/logger: {} + packages/sdk-effects/logger: + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../../tools/treeshake-check packages/sdk-effects/oidc: dependencies: @@ -548,26 +575,46 @@ importers: '@forgerock/sdk-utilities': specifier: workspace:* version: link:../../sdk-utilities + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../../tools/treeshake-check packages/sdk-effects/sdk-request-middleware: dependencies: '@reduxjs/toolkit': specifier: 'catalog:' version: 2.10.1 + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../../tools/treeshake-check packages/sdk-effects/storage: dependencies: '@forgerock/sdk-types': specifier: workspace:* version: link:../../sdk-types + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../../tools/treeshake-check - packages/sdk-types: {} + packages/sdk-types: + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check packages/sdk-utilities: dependencies: '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types + devDependencies: + '@forgerock/treeshake-check': + specifier: workspace:* + version: link:../../tools/treeshake-check scratchpad: dependencies: @@ -619,6 +666,27 @@ importers: specifier: catalog:effect version: 3.20.0 + tools/treeshake-check: + dependencies: + '@effect/cli': + specifier: ^0.75.1 + version: 0.75.1(@effect/platform@0.90.10(effect@3.20.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + '@effect/platform': + specifier: catalog:effect + version: 0.90.10(effect@3.20.0) + '@effect/platform-node': + specifier: catalog:effect + version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + '@rollup/plugin-virtual': + specifier: ^3.0.2 + version: 3.0.2(rollup@4.59.0) + effect: + specifier: catalog:effect + version: 3.20.0 + rollup: + specifier: ^4.59.0 + version: 4.59.0 + tools/user-scripts: dependencies: '@effect/platform': @@ -1488,6 +1556,14 @@ packages: '@effect/printer-ansi': ^0.45.0 effect: ^3.17.8 + '@effect/cli@0.75.1': + resolution: {integrity: sha512-aDZ1OZzaFAb6NyBOrP+Bl52eICds5ERI9aa67LzloJELt5SCWeNwthtaEacBvvMkwVUcykJ+YHcGCkD1t47g8g==} + peerDependencies: + '@effect/platform': ^0.96.0 + '@effect/printer': ^0.49.0 + '@effect/printer-ansi': ^0.49.0 + effect: ^3.21.1 + '@effect/cluster@0.46.4': resolution: {integrity: sha512-81nWw5ABtRZZFmQTrWvfsUnqTg9LybIFYvmsiIL7xQ2t+g6746IZe9Tcv9bSmMdioJLgeuO4CiRg0FfDP2qCWA==} peerDependencies: @@ -2843,6 +2919,15 @@ packages: react-redux: optional: true + '@rollup/plugin-virtual@3.0.2': + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^4.59.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -9724,6 +9809,16 @@ snapshots: toml: 3.0.0 yaml: 2.8.1 + '@effect/cli@0.75.1(@effect/platform@0.90.10(effect@3.20.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': + dependencies: + '@effect/platform': 0.90.10(effect@3.20.0) + '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0) + '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0) + effect: 3.20.0 + ini: 4.1.3 + toml: 3.0.0 + yaml: 2.8.1 + '@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': dependencies: '@effect/platform': 0.90.10(effect@3.20.0) @@ -9799,12 +9894,23 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 + '@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0)': + dependencies: + '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0) + '@effect/typeclass': 0.36.0(effect@3.20.0) + effect: 3.20.0 + '@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0)': dependencies: '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) '@effect/typeclass': 0.36.0(effect@3.21.0) effect: 3.21.0 + '@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0)': + dependencies: + '@effect/typeclass': 0.36.0(effect@3.20.0) + effect: 3.20.0 + '@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0)': dependencies: '@effect/typeclass': 0.36.0(effect@3.21.0) @@ -9822,6 +9928,10 @@ snapshots: effect: 3.20.0 uuid: 11.1.1 + '@effect/typeclass@0.36.0(effect@3.20.0)': + dependencies: + effect: 3.20.0 + '@effect/typeclass@0.36.0(effect@3.21.0)': dependencies: effect: 3.21.0 @@ -11153,6 +11263,10 @@ snapshots: redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 + '@rollup/plugin-virtual@3.0.2(rollup@4.59.0)': + optionalDependencies: + rollup: 4.59.0 + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true diff --git a/tools/treeshake-check/src/index.ts b/tools/treeshake-check/src/index.ts index 2b9e1526bd..1e9e7e08e0 100644 --- a/tools/treeshake-check/src/index.ts +++ b/tools/treeshake-check/src/index.ts @@ -4,6 +4,7 @@ import { Command, Options } from '@effect/cli'; import { NodeContext, NodeRuntime } from '@effect/platform-node'; import { Console, Effect, Option } from 'effect'; import { analyzeTreeshakeability, checkPackage } from './lib/treeshake-check.js'; +import { EXPLANATIONS, primaryCause } from './lib/explanations.js'; import type { TreeshakeResult } from './lib/schemas.js'; const tree = ` @@ -48,10 +49,17 @@ const top = Options.integer('top').pipe( Options.optional, ); -// ─── Renderers ─────────────────────────────────────────────────────────────── +// ─── Rendering helpers ─────────────────────────────────────────────────────── + +const indent = (text: string, prefix = ' ') => + text + .split('\n') + .map((line) => prefix + line) + .join('\n'); -const pct = (n: number, total: number) => - total === 0 ? '0%' : `${((n / total) * 100).toFixed(1)}%`; +const SEPARATOR = ' ───────────────────────────────────────────────'; + +// ─── Renderers ─────────────────────────────────────────────────────────────── const renderJson = (result: TreeshakeResult) => Console.log(JSON.stringify(result, null, 2)); @@ -60,7 +68,7 @@ const renderHuman = (result: TreeshakeResult, topN: Option.Option) => if (result._tag === 'FullyTreeshakeable') { yield* Console.info(tree); if (result.hints.recommendations.length > 0) { - yield* Console.info('\nNotes:'); + yield* Console.info('\nRecommendations:'); for (const rec of result.hints.recommendations) { yield* Console.info(` • ${rec}`); } @@ -68,56 +76,111 @@ const renderHuman = (result: TreeshakeResult, topN: Option.Option) => return; } + // ─── Headline verdict ────────────────────────────────────────────────── + const survivedPct = (result.totalRenderedBytes / result.totalOriginalBytes) * 100; + const moduleCount = result.modules.length; + + yield* Console.info('\n This package is not tree-shakeable.\n'); + yield* Console.info( + ` When a consumer imports anything from this package, ${survivedPct.toFixed(0)}% ` + + `of its code (${result.totalRenderedBytes} of ${result.totalOriginalBytes} bytes) ` + + `gets pulled into their bundle, even if they only use a single export.\n`, + ); yield* Console.info( - `Not fully tree-shakeable: ${result.totalRenderedBytes} of ` + - `${result.totalOriginalBytes} bytes survived ` + - `(${pct(result.totalRenderedBytes, result.totalOriginalBytes)}).\n`, + ` ${moduleCount} ${moduleCount === 1 ? 'file is' : 'files are'} preventing ` + + `tree-shaking. Details below.\n`, ); - // Sort modules worst-first; optionally truncate + // ─── Per-module diagnosis ────────────────────────────────────────────── const sorted = [...result.modules].sort((a, b) => b.renderedLength - a.renderedLength); const shown = Option.match(topN, { onNone: () => sorted, onSome: (n) => sorted.slice(0, n), }); - yield* Console.info( - Option.isSome(topN) ? `Top ${shown.length} unshaken modules:` : 'Per-module breakdown:', - ); + yield* Console.info(`${SEPARATOR}\n`); - for (const m of shown) { - const exportInfo = - m.renderedExports.length === 0 && m.removedExports.length === 0 - ? ' exports: (none)' - : ` exports: rendered=[${m.renderedExports.join(', ')}] ` + - `removed=[${m.removedExports.join(', ')}]`; + for (const [i, m] of shown.entries()) { + const cause = primaryCause(m.suspectedCauses); + const explanation = EXPLANATIONS[cause]; + const filePct = (m.renderedLength / m.originalLength) * 100; + yield* Console.info(` [${i + 1}] ${m.id}\n`); + yield* Console.info(` Problem: ${explanation.summary}\n`); yield* Console.info( - `\n ${m.id}\n` + - ` bytes: ${m.renderedLength}/${m.originalLength} ` + - `(${pct(m.renderedLength, m.originalLength)} survived)\n` + - `${exportInfo}\n` + - ` likely: ${m.suspectedCauses.join(', ')}`, + ` Impact: ${m.renderedLength} of ${m.originalLength} bytes ` + + `(${filePct.toFixed(0)}%) end up in consumer bundles\n`, ); + + if (m.renderedExports.length > 0) { + yield* Console.info(` Exports affected: ${m.renderedExports.join(', ')}\n`); + } + + yield* Console.info(' Why this happens:'); + yield* Console.info(indent(explanation.why, ' ')); + yield* Console.info(''); + + yield* Console.info(' How to fix:'); + for (const f of explanation.fix) { + yield* Console.info(` • ${f}`); + } + + if (explanation.example) { + yield* Console.info('\n Example:'); + yield* Console.info(' Before:'); + yield* Console.info(indent(explanation.example.before, ' ')); + yield* Console.info('\n After:'); + yield* Console.info(indent(explanation.example.after, ' ')); + } + + // Show a snippet of the actual surviving code as evidence + if (m.survivingCode && m.survivingCode.trim().length > 0) { + const snippet = + m.survivingCode.length > 400 + ? m.survivingCode.slice(0, 400) + '\n... (truncated)' + : m.survivingCode; + yield* Console.info('\n Surviving code:'); + yield* Console.info(indent(snippet, ' ')); + } + + yield* Console.info(`\n${SEPARATOR}\n`); } + // ─── Truncation notice ───────────────────────────────────────────────── if (Option.isSome(topN) && sorted.length > Option.getOrThrow(topN)) { - yield* Console.info(`\n ...and ${sorted.length - Option.getOrThrow(topN)} more.`); + yield* Console.info( + ` (Showing top ${shown.length} of ${sorted.length}. ` + + `Run without --top to see all, or with --json for machine-readable output.)\n`, + ); + } + + // ─── Aggregate summary when all modules share a cause ────────────────── + const uniqueCauses = new Set(result.modules.map((m) => primaryCause(m.suspectedCauses))); + if (uniqueCauses.size === 1 && result.modules.length > 1) { + const [onlyCause] = Array.from(uniqueCauses); + yield* Console.info( + ` Summary: All ${result.modules.length} files have the same root cause ` + + `(${EXPLANATIONS[onlyCause].summary}). Applying the fix above resolves all of them.\n`, + ); } + // ─── Rollup warnings, if any ─────────────────────────────────────────── if (result.warnings.length > 0) { - yield* Console.info('\nRollup warnings:'); + yield* Console.info(' Rollup warnings encountered during analysis:'); for (const w of result.warnings) { const loc = w.loc ? ` (${w.loc.file ?? '?'}:${w.loc.line}:${w.loc.column})` : ''; - yield* Console.info(` [${w.code ?? 'WARN'}] ${w.message}${loc}`); + yield* Console.info(` [${w.code ?? 'WARN'}] ${w.message}${loc}`); } + yield* Console.info(''); } + // ─── Package-level recommendations ───────────────────────────────────── if (result.hints.recommendations.length > 0) { - yield* Console.info('\nRecommendations:'); + yield* Console.info(' Package-level recommendations:'); for (const rec of result.hints.recommendations) { - yield* Console.info(` • ${rec}`); + yield* Console.info(` • ${rec}`); } + yield* Console.info(''); } }); @@ -189,5 +252,5 @@ cli(process.argv).pipe( ), }), Effect.provide(NodeContext.layer), - NodeRuntime.runMain as any, + NodeRuntime.runMain, ); diff --git a/tools/treeshake-check/src/lib/analysis.ts b/tools/treeshake-check/src/lib/analysis.ts index 2ebf74fc06..995df02025 100644 --- a/tools/treeshake-check/src/lib/analysis.ts +++ b/tools/treeshake-check/src/lib/analysis.ts @@ -9,15 +9,28 @@ import type { ModuleAnalysis, PackageJson, PackageJsonHints, SuspectedCause } fr * of real-world cases, and labeling them "suspected" makes the uncertainty * honest. */ +// src/lib/analysis.ts (additions) + export const detectCauses = (code: string): ReadonlyArray => { const causes = new Set(); - // CJS contamination — these strings shouldn't appear in clean ESM output + // TypeScript enum IIFE: `(function (X) { ... })(X || (X = {}));` + // This is the most common cause for type-package authors and produces + // the highest false-negative rate in the original heuristics. + if ( + /\(function\s*\([A-Z_$][\w$]*\)\s*\{[\s\S]*?\}\)\s*\(\s*[A-Z_$][\w$]*\s*\|\|\s*\(\s*[A-Z_$][\w$]*\s*=\s*\{\s*\}\s*\)\s*\)/.test( + code, + ) + ) { + causes.add('EnumPattern'); + } + + // CJS contamination if (/\b(require\s*\(|module\.exports|exports\.[a-zA-Z_$]|__esModule)/.test(code)) { causes.add('CommonJsContamination'); } - // Object.defineProperty / Object.assign / prototype mutations + // Prototype mutations / Object.defineProperty if (/Object\.(defineProperty|defineProperties|assign|setPrototypeOf|freeze)\s*\(/.test(code)) { causes.add('PrototypeMutation'); } @@ -25,15 +38,14 @@ export const detectCauses = (code: string): ReadonlyArray => { causes.add('PrototypeMutation'); } - // Top-level assignment to global-ish names + // Global assignment if (/^\s*(window|globalThis|self|global)\.[a-zA-Z_$]/m.test(code)) { causes.add('GlobalAssignment'); } - // Bare top-level function call without /*#__PURE__*/ annotation - // Approximate but catches the common `someInit()` / `extend(Foo, Bar)` case + // Bare top-level function call without /*#__PURE__*/ const topLevelCall = /^(?!.*\/\*#__PURE__\*\/)\s*[a-zA-Z_$][\w$]*\s*\(/m; - if (topLevelCall.test(code)) { + if (topLevelCall.test(code) && !causes.has('EnumPattern')) { causes.add('UnannotatedCall'); causes.add('TopLevelSideEffect'); } diff --git a/tools/treeshake-check/src/lib/explanations.ts b/tools/treeshake-check/src/lib/explanations.ts new file mode 100644 index 0000000000..ae4487239a --- /dev/null +++ b/tools/treeshake-check/src/lib/explanations.ts @@ -0,0 +1,144 @@ +import type { SuspectedCause } from './schemas.js'; + +export interface CauseExplanation { + /** One-line description of what was detected. */ + readonly summary: string; + /** Why this prevents tree-shaking. */ + readonly why: string; + /** Concrete steps the user can take. */ + readonly fix: ReadonlyArray; + /** Optional code example showing the fix. */ + readonly example?: { before: string; after: string }; +} + +export const EXPLANATIONS: Record = { + EnumPattern: { + summary: 'TypeScript enum', + why: + 'TypeScript compiles `enum` declarations into an IIFE that mutates a ' + + 'module-scoped variable. Rollup sees the mutation and conservatively ' + + 'assumes the module has observable side effects, even when no one is ' + + 'using the enum.', + fix: [ + 'Replace `enum` with an `as const` object plus a derived type. This ' + + 'compiles to a plain object literal that Rollup can statically analyze.', + 'For published packages, also add `"sideEffects": false` to ' + + 'package.json so consumers benefit from the change.', + ], + example: { + before: 'export enum StepType {\n' + ' LOGIN = "LOGIN",\n' + ' LOGOUT = "LOGOUT",\n' + '}', + after: + 'export const StepType = {\n' + + ' LOGIN: "LOGIN",\n' + + ' LOGOUT: "LOGOUT",\n' + + '} as const;\n' + + 'export type StepType = typeof StepType[keyof typeof StepType];', + }, + }, + + PrototypeMutation: { + summary: 'prototype or property mutation at module scope', + why: + 'Calls like `Object.defineProperty`, `Object.assign`, or assignments ' + + 'to `.prototype` at the top level run when the module is imported, so ' + + 'Rollup keeps them in the bundle.', + fix: [ + 'Move the mutation inside a function that callers explicitly invoke.', + 'If the call is genuinely pure (e.g., defining a property on a ' + + 'module-local object), wrap it in a `/*#__PURE__*/` annotation.', + 'For library code that legitimately needs side effects on import ' + + "(polyfills, registrations), declare the file in `package.json`'s " + + "`sideEffects` array so it's explicitly opted in.", + ], + }, + + GlobalAssignment: { + summary: 'assignment to a global object', + why: + 'Assignments to `window`, `globalThis`, `self`, or `global` at module ' + + 'scope are observable side effects — they affect state outside the ' + + 'module — and can never be tree-shaken.', + fix: [ + 'If this is a polyfill or registration, declare the file in ' + + "`package.json`'s `sideEffects` array so consumers know it has to run.", + 'If the global assignment is opportunistic (e.g., exposing a debug API), ' + + 'consider moving it to a separately-imported entry point so the main ' + + 'entry stays shakeable.', + ], + }, + + CommonJsContamination: { + summary: 'CommonJS code in the bundle', + why: + '`require()`, `module.exports`, and `__esModule` markers indicate ' + + "CommonJS code, which Rollup can't statically analyze for tree-shaking. " + + 'This usually means a transitive dependency ships only CJS, or your ' + + 'build is producing CJS output.', + fix: [ + 'Verify your build emits ESM. Check `package.json` for a `"module"` ' + + 'field or `"type": "module"`.', + 'If a dependency is the source, look for an ESM-only alternative or ' + + 'check whether the dep has an `exports` field with an `import` condition.', + 'For unavoidable CJS deps, mark them as external in your build so they ' + + "don't get bundled into your output.", + ], + }, + + UnannotatedCall: { + summary: 'top-level function call', + why: + 'A bare function call at module scope is treated as side-effectful by ' + + "default — Rollup doesn't know whether the call mutates state, prints " + + 'to console, or registers something globally.', + fix: [ + 'If you know the call is pure, prefix it with `/*#__PURE__*/`:\n' + + ' const result = /*#__PURE__*/ computeOnce();', + 'If the call genuinely has side effects, move it inside a function ' + + 'that consumers explicitly invoke.', + ], + }, + + TopLevelSideEffect: { + summary: 'top-level statement with side effects', + why: + 'Some statement at the top of the module runs when imported and ' + + "Rollup can't prove it's safe to eliminate.", + fix: [ + 'Move side-effecting code into an exported function.', + 'For pure expressions, wrap them in `/*#__PURE__*/`.', + ], + }, + + Unknown: { + summary: 'unknown side effect', + why: + "The heuristic patterns didn't match this code, but Rollup decided to " + + 'keep it. Look at the surviving code below to identify the cause manually.', + fix: [ + 'Read the surviving code in the breakdown above to identify the side effect.', + 'Common causes the heuristic might miss: getters, decorators, ' + + 'destructuring with defaults, or class field initializers.', + ], + }, +}; + +/** + * Pick the most informative cause when a module has multiple labels. + * EnumPattern wins over UnannotatedCall, etc., so users get the most + * specific explanation rather than a generic one. + */ +export const primaryCause = (causes: ReadonlyArray): SuspectedCause => { + const priority: ReadonlyArray = [ + 'EnumPattern', + 'CommonJsContamination', + 'GlobalAssignment', + 'PrototypeMutation', + 'UnannotatedCall', + 'TopLevelSideEffect', + 'Unknown', + ]; + for (const p of priority) { + if (causes.includes(p)) return p; + } + return 'Unknown'; +}; diff --git a/tools/treeshake-check/src/lib/schemas.ts b/tools/treeshake-check/src/lib/schemas.ts index 845c3c1a2e..9c2a3ce2dc 100644 --- a/tools/treeshake-check/src/lib/schemas.ts +++ b/tools/treeshake-check/src/lib/schemas.ts @@ -28,6 +28,7 @@ export const SuspectedCause = Schema.Literal( 'GlobalAssignment', 'CommonJsContamination', 'UnannotatedCall', + 'EnumPattern', 'Unknown', ); export type SuspectedCause = typeof SuspectedCause.Type; diff --git a/tsconfig.json b/tsconfig.json index c22692ddac..344b2eca85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,6 +84,9 @@ }, { "path": "./tools/api-report" + }, + { + "path": "./tools/treeshake-check" } ] } From 6f49cddcbff95345498783fb603242d7c4907a78 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 21 Apr 2026 08:19:29 -0600 Subject: [PATCH 03/14] chore: add-lsp-plugin \r https://github.com/microsoft/typescript-go --- .nxignore | 1 + package.json | 7 +- pnpm-lock.yaml | 236 ++++++++++++++++++++++++++++----------------- tsconfig.base.json | 81 +++++++++++++++- 4 files changed, 230 insertions(+), 95 deletions(-) create mode 100644 .nxignore diff --git a/.nxignore b/.nxignore new file mode 100644 index 0000000000..7a61f2b50d --- /dev/null +++ b/.nxignore @@ -0,0 +1 @@ +.opensource/* diff --git a/package.json b/package.json index 184f9af57a..963563ad14 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "path": "./node_modules/cz-conventional-changelog" } }, - "dependencies": {}, "devDependencies": { "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.27.9", @@ -54,9 +53,11 @@ "@commitlint/config-conventional": "^20.0.0", "@commitlint/prompt": "^20.0.0", "@effect/cli": "catalog:effect", + "@effect/tsgo": "^0.5.1", "@eslint/eslintrc": "^3.0.0", "@eslint/js": "~9.39.0", "@forgerock/treeshake-check": "workspace:*", + "@evilmartians/lefthook": "^2.1.4", "@nx/devkit": "22.6.5", "@nx/eslint": "22.6.5", "@nx/eslint-plugin": "22.6.5", @@ -94,7 +95,6 @@ "eslint-plugin-playwright": "^2.0.0", "eslint-plugin-prettier": "^5.2.3", "fast-check": "^4.0.0", - "@evilmartians/lefthook": "^2.1.4", "jiti": "2.6.1", "jsdom": "27.4.0", "jsonc-eslint-parser": "^2.1.0", @@ -103,12 +103,13 @@ "pkg-pr-new": "^0.0.67", "playwright": "^1.47.2", "prettier": "^3.2.5", + "setup": "^0.0.3", "shx": "^0.4.0", "swc-loader": "0.2.7", "ts-node": "10.9.2", - "tsx": "^4.20.0", "ts-patch": "3.3.0", "tslib": "^2.5.0", + "tsx": "^4.20.0", "typedoc": "^0.27.4", "typedoc-github-theme": "0.2.1", "typedoc-plugin-rename-defaults": "^0.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3f9dfdb06..466b4262da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,7 @@ catalogs: version: 0.27.0 effect: specifier: ^3.20.0 - version: 3.20.0 + version: 3.21.0 vite: vite: specifier: ^7.3.2 @@ -80,6 +80,9 @@ importers: '@effect/cli': specifier: catalog:effect version: 0.69.2(@effect/platform@0.90.10(effect@3.21.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + '@effect/tsgo': + specifier: ^0.5.1 + version: 0.5.1 '@eslint/eslintrc': specifier: ^3.0.0 version: 3.3.5 @@ -227,6 +230,9 @@ importers: prettier: specifier: ^3.2.5 version: 3.8.3 + setup: + specifier: ^0.0.3 + version: 0.0.3 shx: specifier: ^0.4.0 version: 0.4.0 @@ -358,13 +364,13 @@ importers: version: 0.35.2 '@effect/opentelemetry': specifier: catalog:effect - version: 0.56.6(@effect/platform@0.90.10(effect@3.20.0))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.20.0) + version: 0.56.6(@effect/platform@0.90.10(effect@3.21.0))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.21.0) '@effect/platform': specifier: catalog:effect - version: 0.90.10(effect@3.20.0) + version: 0.90.10(effect@3.21.0) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) '@opentelemetry/sdk-logs': specifier: 0.207.0 version: 0.207.0(@opentelemetry/api@1.9.0) @@ -382,14 +388,14 @@ importers: version: 2.2.0(@opentelemetry/api@1.9.0) effect: specifier: catalog:effect - version: 3.20.0 + version: 3.21.0 nanoid: specifier: 5.1.9 version: 5.1.9 devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.20.0)(vitest@3.2.4) + version: 0.27.0(effect@3.21.0)(vitest@3.2.4) vitest: specifier: catalog:vitest version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) @@ -438,14 +444,14 @@ importers: version: 2.10.1 effect: specifier: catalog:effect - version: 3.20.0 + version: 3.21.0 immer: specifier: 'catalog:' version: 10.2.0 devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.20.0)(vitest@3.2.4) + version: 0.27.0(effect@3.21.0)(vitest@3.2.4) '@forgerock/treeshake-check': specifier: workspace:* version: link:../../tools/treeshake-check @@ -537,11 +543,11 @@ importers: version: 2.10.1 effect: specifier: catalog:effect - version: 3.20.0 + version: 3.21.0 devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.20.0)(vitest@3.2.4) + version: 0.27.0(effect@3.21.0)(vitest@3.2.4) '@forgerock/treeshake-check': specifier: workspace:* version: link:../../tools/treeshake-check @@ -658,31 +664,31 @@ importers: dependencies: '@effect/platform': specifier: catalog:effect - version: 0.90.10(effect@3.20.0) + version: 0.90.10(effect@3.21.0) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) effect: specifier: catalog:effect - version: 3.20.0 + version: 3.21.0 tools/treeshake-check: dependencies: '@effect/cli': specifier: ^0.75.1 - version: 0.75.1(@effect/platform@0.90.10(effect@3.20.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + version: 0.75.1(@effect/platform@0.90.10(effect@3.21.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) '@effect/platform': specifier: catalog:effect - version: 0.90.10(effect@3.20.0) + version: 0.90.10(effect@3.21.0) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) '@rollup/plugin-virtual': specifier: ^3.0.2 version: 3.0.2(rollup@4.59.0) effect: specifier: catalog:effect - version: 3.20.0 + version: 3.21.0 rollup: specifier: ^4.59.0 version: 4.59.0 @@ -691,13 +697,13 @@ importers: dependencies: '@effect/platform': specifier: catalog:effect - version: 0.90.10(effect@3.20.0) + version: 0.90.10(effect@3.21.0) '@effect/platform-node': specifier: catalog:effect - version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) + version: 0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) effect: specifier: catalog:effect - version: 3.20.0 + version: 3.21.0 vitest: specifier: catalog:vitest version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) @@ -707,7 +713,7 @@ importers: version: 0.35.2 '@effect/vitest': specifier: catalog:effect - version: 0.27.0(effect@3.20.0)(vitest@3.2.4) + version: 0.27.0(effect@3.21.0)(vitest@3.2.4) packages: @@ -1670,6 +1676,45 @@ packages: '@effect/platform': ^0.90.4 effect: ^3.17.7 + '@effect/tsgo-darwin-arm64@0.5.1': + resolution: {integrity: sha512-sJFjIPbfKTB9jLsUcMCmIZCOdTOQQqpKa2Ah7kP+8jXTqNvmIFZg4IzZQIPjHDSCKfU8f3JfOEEdhwktCdQ57Q==} + cpu: [arm64] + os: [darwin] + + '@effect/tsgo-darwin-x64@0.5.1': + resolution: {integrity: sha512-2b4WtHHddhUl846ZJOPhv3C/3JRwocwUer65vsxkb0ixS76ly3z1BOWwdL7naAsrtaUJ0n9FaN9WI4tbwxa5OA==} + cpu: [x64] + os: [darwin] + + '@effect/tsgo-linux-arm64@0.5.1': + resolution: {integrity: sha512-tgBW2rGLSewvnE61EYU31tmH6mdrrwHpNmbnYiNgU2rjs35gVYin2WZ7aM9r71hjUYbS2p9i4OcwRvSbyyVq/Q==} + cpu: [arm64] + os: [linux] + + '@effect/tsgo-linux-arm@0.5.1': + resolution: {integrity: sha512-xs7+sx71e+lhRgl6R0ZfCxFXzTwhO00BcpPs5CtfNS4EEhNQc9EfRvGYtx3D4l9+5KwPr5VWJbYJz99Y5e2H9w==} + cpu: [arm] + os: [linux] + + '@effect/tsgo-linux-x64@0.5.1': + resolution: {integrity: sha512-70dMv3/H+P3KDNWb31qPXJiJh6s78k3+J+QXN8RatKiQYrJw2HhREYL6ToVx9y5WOV7XFvC0eCIIa4/AMwQLTw==} + cpu: [x64] + os: [linux] + + '@effect/tsgo-win32-arm64@0.5.1': + resolution: {integrity: sha512-v4lSFoPLYmVFBJbtOseSd0T0Vij5Vdk6IAUKpQwMiy0gSDPJnE+M6Esj3tY9IpFD3LxcdqhTxBl73pEcf+ihYg==} + cpu: [arm64] + os: [win32] + + '@effect/tsgo-win32-x64@0.5.1': + resolution: {integrity: sha512-dfyXhmVQkxncSnujjSXsOMwzqFIBNDViXiD3Uj9DPDNLSxyg0ybBNxYJTpvJhqHxqseA9wE2aCIMu/pfpac+0Q==} + cpu: [x64] + os: [win32] + + '@effect/tsgo@0.5.1': + resolution: {integrity: sha512-INANZ/NK9akOwSQVWpQgSDLjlegrs4gui21nuQsgN7zCjCmj4m/ixUDuVgtW2C0UfqhPWWabyFWCDntu7ryCZQ==} + hasBin: true + '@effect/typeclass@0.36.0': resolution: {integrity: sha512-+8xYvX4tjD7gKwGYzOyFh90I+ptdXzoNHLQTSa8kGh/xOVZMIGYb0VgLoNHE02UsuVrB+JJJuBmKLdd5TeDTPg==} peerDependencies: @@ -3119,9 +3164,6 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} - '@standard-schema/spec@1.0.0': - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -7576,6 +7618,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + setup@0.0.3: + resolution: {integrity: sha512-NcuGT1k9V3jdwcNdZzpnO6h2WtLMieaIVRMWeQvlSVRMB6b51T3jeUBSeBzP5Mmqy50viW5y7LRaMaTm/MZ4CA==} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -9809,39 +9854,39 @@ snapshots: toml: 3.0.0 yaml: 2.8.1 - '@effect/cli@0.75.1(@effect/platform@0.90.10(effect@3.20.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': + '@effect/cli@0.75.1(@effect/platform@0.90.10(effect@3.21.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/platform': 0.90.10(effect@3.20.0) - '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0) - '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 + '@effect/platform': 0.90.10(effect@3.21.0) + '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) + '@effect/printer-ansi': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) + effect: 3.21.0 ini: 4.1.3 toml: 3.0.0 yaml: 2.8.1 - '@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': + '@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/platform': 0.90.10(effect@3.20.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - '@effect/workflow': 0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 + '@effect/platform': 0.90.10(effect@3.21.0) + '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + '@effect/workflow': 0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + effect: 3.21.0 - '@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0)': + '@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/platform': 0.90.10(effect@3.20.0) - effect: 3.20.0 + '@effect/platform': 0.90.10(effect@3.21.0) + effect: 3.21.0 uuid: 11.1.1 '@effect/language-service@0.20.1': {} '@effect/language-service@0.35.2': {} - '@effect/opentelemetry@0.56.6(@effect/platform@0.90.10(effect@3.20.0))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.20.0)': + '@effect/opentelemetry@0.56.6(@effect/platform@0.90.10(effect@3.21.0))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-web@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)(effect@3.21.0)': dependencies: - '@effect/platform': 0.90.10(effect@3.20.0) + '@effect/platform': 0.90.10(effect@3.21.0) '@opentelemetry/semantic-conventions': 1.38.0 - effect: 3.20.0 + effect: 3.21.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) @@ -9851,28 +9896,28 @@ snapshots: '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-web': 2.2.0(@opentelemetry/api@1.9.0) - '@effect/platform-node-shared@0.47.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': + '@effect/platform-node-shared@0.47.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/cluster': 0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.90.10(effect@3.20.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) + '@effect/cluster': 0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + '@effect/platform': 0.90.10(effect@3.21.0) + '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) '@parcel/watcher': 2.5.1 - effect: 3.20.0 + effect: 3.21.0 multipasta: 0.2.7 ws: 8.18.3 transitivePeerDependencies: - bufferutil - utf-8-validate - '@effect/platform-node@0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': + '@effect/platform-node@0.94.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/cluster': 0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.90.10(effect@3.20.0) - '@effect/platform-node-shared': 0.47.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 + '@effect/cluster': 0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + '@effect/platform': 0.90.10(effect@3.21.0) + '@effect/platform-node-shared': 0.47.2(@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0) + '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + '@effect/sql': 0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + effect: 3.21.0 mime: 3.0.0 undici: 7.24.4 ws: 8.18.3 @@ -9880,13 +9925,6 @@ snapshots: - bufferutil - utf-8-validate - '@effect/platform@0.90.10(effect@3.20.0)': - dependencies: - effect: 3.20.0 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.5 - multipasta: 0.2.7 - '@effect/platform@0.90.10(effect@3.21.0)': dependencies: effect: 3.21.0 @@ -9894,58 +9932,74 @@ snapshots: msgpackr: 1.11.5 multipasta: 0.2.7 - '@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0) - '@effect/typeclass': 0.36.0(effect@3.20.0) - effect: 3.20.0 - '@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0)': dependencies: '@effect/printer': 0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0) '@effect/typeclass': 0.36.0(effect@3.21.0) effect: 3.21.0 - '@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/typeclass': 0.36.0(effect@3.20.0) - effect: 3.20.0 - '@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0)': dependencies: '@effect/typeclass': 0.36.0(effect@3.21.0) effect: 3.21.0 - '@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0)': + '@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/platform': 0.90.10(effect@3.20.0) - effect: 3.20.0 + '@effect/platform': 0.90.10(effect@3.21.0) + effect: 3.21.0 - '@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0)': + '@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/experimental': 0.54.6(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.90.10(effect@3.20.0) - effect: 3.20.0 + '@effect/experimental': 0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + '@effect/platform': 0.90.10(effect@3.21.0) + effect: 3.21.0 uuid: 11.1.1 - '@effect/typeclass@0.36.0(effect@3.20.0)': - dependencies: - effect: 3.20.0 + '@effect/tsgo-darwin-arm64@0.5.1': + optional: true + + '@effect/tsgo-darwin-x64@0.5.1': + optional: true + + '@effect/tsgo-linux-arm64@0.5.1': + optional: true + + '@effect/tsgo-linux-arm@0.5.1': + optional: true + + '@effect/tsgo-linux-x64@0.5.1': + optional: true + + '@effect/tsgo-win32-arm64@0.5.1': + optional: true + + '@effect/tsgo-win32-x64@0.5.1': + optional: true + + '@effect/tsgo@0.5.1': + optionalDependencies: + '@effect/tsgo-darwin-arm64': 0.5.1 + '@effect/tsgo-darwin-x64': 0.5.1 + '@effect/tsgo-linux-arm': 0.5.1 + '@effect/tsgo-linux-arm64': 0.5.1 + '@effect/tsgo-linux-x64': 0.5.1 + '@effect/tsgo-win32-arm64': 0.5.1 + '@effect/tsgo-win32-x64': 0.5.1 '@effect/typeclass@0.36.0(effect@3.21.0)': dependencies: effect: 3.21.0 - '@effect/vitest@0.27.0(effect@3.20.0)(vitest@3.2.4)': + '@effect/vitest@0.27.0(effect@3.21.0)(vitest@3.2.4)': dependencies: - effect: 3.20.0 + effect: 3.21.0 vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) - '@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': + '@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: - '@effect/platform': 0.90.10(effect@3.20.0) - '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 + '@effect/platform': 0.90.10(effect@3.21.0) + '@effect/rpc': 0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0) + effect: 3.21.0 '@emnapi/core@1.7.0': dependencies: @@ -11256,7 +11310,7 @@ snapshots: '@reduxjs/toolkit@2.10.1': dependencies: - '@standard-schema/spec': 1.0.0 + '@standard-schema/spec': 1.1.0 '@standard-schema/utils': 0.3.0 immer: 10.2.0 redux: 5.0.1 @@ -11413,8 +11467,6 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@standard-schema/spec@1.0.0': {} - '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -16697,6 +16749,8 @@ snapshots: setprototypeof@1.2.0: {} + setup@0.0.3: {} + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 diff --git a/tsconfig.base.json b/tsconfig.base.json index fc5c643230..d3a8f8dff1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -4,6 +4,85 @@ "declaration": true, "declarationMap": true, "skipLibCheck": true, - "baseUrl": "." + "baseUrl": ".", + "plugins": [ + { + "name": "@effect/language-service", + // Controls Effect refactors. (default: true) + "refactors": true, + // Controls Effect diagnostics. (default: true) + "diagnostics": true, + // When false, suggestion-level Effect diagnostics are omitted from tsc CLI output. (default: true) + "includeSuggestionsInTsc": true, + // Controls Effect quickinfo. (default: true) + "quickinfo": true, + // Controls Effect completions. (default: true) + "completions": true, + // Enables additional debug-only Effect language service output. (default: false) + "debug": false, + // Controls Effect goto references support. (default: true) + "goto": true, + // Controls Effect rename helpers. (default: true) + "renames": true, + // When true, suggestion diagnostics do not affect the tsc exit code. (default: true) + "ignoreEffectSuggestionsInTscExitCode": true, + // When true, warning diagnostics do not affect the tsc exit code. (default: false) + "ignoreEffectWarningsInTscExitCode": false, + // When true, error diagnostics do not affect the tsc exit code. (default: false) + "ignoreEffectErrorsInTscExitCode": false, + // When true, disabled diagnostics are still processed so directives can re-enable them. (default: false) + "skipDisabledOptimization": false, + // Mermaid rendering service for layer graph links. Accepts mermaid.live, mermaid.com, or a custom URL. (default: "mermaid.live") + "mermaidProvider": "mermaid.live", + // When true, suppresses external Mermaid links in hover output. (default: false) + "noExternal": false, + // How many levels deep the layer graph extraction follows symbol references. (default: 0) + "layerGraphFollowDepth": 0, + // When true, suppresses redundant return-type inlay hints on supported Effect generator functions. (default: false) + "inlays": false, + // Package names that should prefer namespace imports. (default: []) + "namespaceImportPackages": [], + // Package names that should prefer barrel named imports. (default: []) + "barrelImportPackages": [], + // Package-level import aliases keyed by package name. (default: {}) + "importAliases": {}, + // Controls whether named reexports are followed at package top-level. (default: "ignore") + "topLevelNamedReexports": "ignore", + // Configures key pattern formulas for the deterministicKeys rule. (default: [{"target":"service","pattern":"default","skipLeadingPath":["src/"]},{"target":"custom","pattern":"default","skipLeadingPath":["src/"]}]) + "keyPatterns": [ + { + "target": "service", + "pattern": "default", + "skipLeadingPath": ["src/"] + }, + { + "target": "custom", + "pattern": "default", + "skipLeadingPath": ["src/"] + } + ], + // Enables matching constructors with @effect-identifier annotations. (default: false) + "extendedKeyDetection": false, + // Minimum number of contiguous pipeable transformations to trigger missedPipeableOpportunity. (default: 2) + "pipeableMinArgCount": 2, + // Package names allowed to have multiple versions without triggering duplicatePackage. (default: []) + "allowedDuplicatedPackages": [], + // Controls which effectFnOpportunity quickfix variants are offered. (default: ["span"]) + "effectFn": ["span"], + // Maps rule names to severity levels. Use {} to enable diagnostics with rule defaults. (default: {}) + "diagnosticSeverity": {}, + // Ordered per-file diagnostic option overrides. (default: [{"include":["src/**/*.ts"],"options":{"diagnosticSeverity":{"floatingEffect":"error"}}}]) + "overrides": [ + { + "include": ["src/**/*.ts"], + "options": { + "diagnosticSeverity": { + "floatingEffect": "error" + } + } + } + ] + } + ] } } From 0e66322f572afbc6ed73be1c135091d076eb7ad9 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:00:25 -0600 Subject: [PATCH 04/14] test(treeshake-check): add pure-function tests for analysis module --- pnpm-lock.yaml | 12 +- tools/treeshake-check/package.json | 4 + .../treeshake-check/src/lib/analysis.test.ts | 167 ++++++++++++++++++ 3 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 tools/treeshake-check/src/lib/analysis.test.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 466b4262da..46aad500aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -692,6 +692,13 @@ importers: rollup: specifier: ^4.59.0 version: 4.59.0 + devDependencies: + '@effect/vitest': + specifier: catalog:effect + version: 0.27.0(effect@3.21.0)(vitest@3.2.4) + vitest: + specifier: catalog:vitest + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) tools/user-scripts: dependencies: @@ -8353,6 +8360,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -12140,7 +12148,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -17688,7 +17696,7 @@ snapshots: dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: diff --git a/tools/treeshake-check/package.json b/tools/treeshake-check/package.json index 80cdfe39d1..fa002df2d1 100644 --- a/tools/treeshake-check/package.json +++ b/tools/treeshake-check/package.json @@ -18,6 +18,10 @@ }, "./package.json": "./package.json" }, + "devDependencies": { + "@effect/vitest": "catalog:effect", + "vitest": "catalog:vitest" + }, "dependencies": { "@effect/cli": "^0.75.1", "@effect/platform": "catalog:effect", diff --git a/tools/treeshake-check/src/lib/analysis.test.ts b/tools/treeshake-check/src/lib/analysis.test.ts new file mode 100644 index 0000000000..aacfac2463 --- /dev/null +++ b/tools/treeshake-check/src/lib/analysis.test.ts @@ -0,0 +1,167 @@ +// tools/treeshake-check/src/lib/analysis.test.ts +import { describe, it, expect } from '@effect/vitest'; +import { detectCauses, buildModuleAnalysis, analyzePackageJsonHints } from './analysis.js'; + +// ─── detectCauses ───────────────────────────────────────────────────────────── + +describe('detectCauses', () => { + it('detects TypeScript enum IIFE pattern', () => { + const code = 'var X; (function (X) { X["A"] = "A"; })(X || (X = {}));'; + expect(new Set(detectCauses(code))).toContain('EnumPattern'); + }); + + it('detects CommonJS require', () => { + const code = 'const fs = require("fs");'; + expect(new Set(detectCauses(code))).toContain('CommonJsContamination'); + }); + + it('detects module.exports contamination', () => { + const code = 'module.exports = { foo: 1 };'; + expect(new Set(detectCauses(code))).toContain('CommonJsContamination'); + }); + + it('detects Object.defineProperty as PrototypeMutation', () => { + const code = 'Object.defineProperty(exports, "__esModule", { value: true });'; + const causes = new Set(detectCauses(code)); + expect(causes).toContain('PrototypeMutation'); + }); + + it('detects .prototype assignment as PrototypeMutation', () => { + const code = 'String.prototype.foo = function() {};'; + expect(new Set(detectCauses(code))).toContain('PrototypeMutation'); + }); + + it('detects window global assignment', () => { + const code = 'window.MY_LIB = { version: "1" };'; + expect(new Set(detectCauses(code))).toContain('GlobalAssignment'); + }); + + it('detects globalThis assignment', () => { + const code = 'globalThis.MY_LIB = {};'; + expect(new Set(detectCauses(code))).toContain('GlobalAssignment'); + }); + + it('detects unannotated top-level call', () => { + const code = 'initialize();'; + const causes = new Set(detectCauses(code)); + expect(causes).toContain('UnannotatedCall'); + expect(causes).toContain('TopLevelSideEffect'); + }); + + it('does not flag PURE-annotated top-level call', () => { + const code = 'const x = /*#__PURE__*/ compute();'; + expect(new Set(detectCauses(code))).not.toContain('UnannotatedCall'); + }); + + it('returns Unknown for plain export', () => { + const code = 'export const foo = 42;'; + expect(detectCauses(code)).toEqual(['Unknown']); + }); + + it('returns Unknown for empty input', () => { + expect(detectCauses('')).toEqual(['Unknown']); + }); + + it('detects multiple causes in one file', () => { + const code = ` + var Status; + (function (Status) { Status["OK"] = "OK"; })(Status || (Status = {})); + window.MY_LIB = Status; + `; + const causes = new Set(detectCauses(code)); + expect(causes).toContain('EnumPattern'); + expect(causes).toContain('GlobalAssignment'); + }); +}); + +// ─── buildModuleAnalysis ────────────────────────────────────────────────────── + +describe('buildModuleAnalysis', () => { + it('computes shaking ratio correctly', () => { + const result = buildModuleAnalysis('test.js', { + originalLength: 1000, + renderedLength: 250, + renderedExports: ['foo'], + removedExports: ['bar', 'baz'], + code: 'export const foo = 1;', + }); + expect(result.shakingRatio).toBe(0.25); + expect(result.renderedExports).toEqual(['foo']); + expect(result.removedExports).toEqual(['bar', 'baz']); + expect(result.id).toBe('test.js'); + }); + + it('handles zero-byte modules without dividing by zero', () => { + const result = buildModuleAnalysis('empty.js', { + originalLength: 0, + renderedLength: 0, + renderedExports: [], + removedExports: [], + code: null, + }); + expect(result.shakingRatio).toBe(0); + expect(result.suspectedCauses).toEqual(['Unknown']); + }); + + it('returns Unknown when code is null', () => { + const result = buildModuleAnalysis('no-code.js', { + originalLength: 100, + renderedLength: 50, + renderedExports: [], + removedExports: [], + code: null, + }); + expect(result.suspectedCauses).toEqual(['Unknown']); + expect(result.survivingCode).toBeNull(); + }); + + it('runs detectCauses on surviving code', () => { + const result = buildModuleAnalysis('enum.js', { + originalLength: 200, + renderedLength: 100, + renderedExports: [], + removedExports: [], + code: 'var X; (function (X) { X["A"] = "A"; })(X || (X = {}));', + }); + expect(new Set(result.suspectedCauses)).toContain('EnumPattern'); + }); +}); + +// ─── analyzePackageJsonHints ────────────────────────────────────────────────── + +describe('analyzePackageJsonHints', () => { + it('flags missing sideEffects field', () => { + const hints = analyzePackageJsonHints({ name: 'foo', main: './index.js' }); + expect(hints.hasSideEffectsField).toBe(false); + expect(hints.recommendations.some((r) => r.includes('"sideEffects": false'))).toBe(true); + }); + + it('does not recommend sideEffects when already present', () => { + const hints = analyzePackageJsonHints({ name: 'foo', sideEffects: false }); + expect(hints.hasSideEffectsField).toBe(true); + expect(hints.recommendations.some((r) => r.includes('"sideEffects": false'))).toBe(false); + }); + + it('recommends module field when only main is present', () => { + const hints = analyzePackageJsonHints({ name: 'foo', main: './cjs/index.js' }); + expect(hints.hasModuleField).toBe(false); + expect(hints.recommendations.some((r) => r.includes('"module"'))).toBe(true); + }); + + it('does not recommend module field when already present', () => { + const hints = analyzePackageJsonHints({ name: 'foo', module: './esm/index.js' }); + expect(hints.hasModuleField).toBe(true); + expect(hints.recommendations.some((r) => r.includes('"module"'))).toBe(false); + }); + + it('recommends type module when no ESM signal', () => { + const hints = analyzePackageJsonHints({ name: 'foo' }); + expect(hints.hasTypeModule).toBe(false); + expect(hints.recommendations.some((r) => r.includes('"type": "module"'))).toBe(true); + }); + + it('recognizes type: module correctly', () => { + const hints = analyzePackageJsonHints({ name: 'foo', type: 'module', sideEffects: false }); + expect(hints.hasTypeModule).toBe(true); + }); +}); From 210887ecd421a8bb0a9051d6c393217938ec22bd Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:09:26 -0600 Subject: [PATCH 05/14] test(treeshake-check): register addEqualityTesters and close test coverage gaps --- .../treeshake-check/src/lib/analysis.test.ts | 24 ++++++++++++++++++- tools/treeshake-check/src/test-setup.ts | 2 ++ tools/treeshake-check/vitest.config.mts | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tools/treeshake-check/src/test-setup.ts diff --git a/tools/treeshake-check/src/lib/analysis.test.ts b/tools/treeshake-check/src/lib/analysis.test.ts index aacfac2463..d6b10cedb8 100644 --- a/tools/treeshake-check/src/lib/analysis.test.ts +++ b/tools/treeshake-check/src/lib/analysis.test.ts @@ -21,7 +21,7 @@ describe('detectCauses', () => { }); it('detects Object.defineProperty as PrototypeMutation', () => { - const code = 'Object.defineProperty(exports, "__esModule", { value: true });'; + const code = 'Object.defineProperty(MyClass.prototype, "foo", { value: 1 });'; const causes = new Set(detectCauses(code)); expect(causes).toContain('PrototypeMutation'); }); @@ -62,6 +62,13 @@ describe('detectCauses', () => { expect(detectCauses('')).toEqual(['Unknown']); }); + it('enum IIFE does not also trigger UnannotatedCall', () => { + const code = 'var X; (function (X) { X["A"] = "A"; })(X || (X = {}));'; + const causes = new Set(detectCauses(code)); + expect(causes).toContain('EnumPattern'); + expect(causes).not.toContain('UnannotatedCall'); + }); + it('detects multiple causes in one file', () => { const code = ` var Status; @@ -164,4 +171,19 @@ describe('analyzePackageJsonHints', () => { const hints = analyzePackageJsonHints({ name: 'foo', type: 'module', sideEffects: false }); expect(hints.hasTypeModule).toBe(true); }); + + it('does not recommend type module when module field is present', () => { + const hints = analyzePackageJsonHints({ name: 'foo', module: './esm/index.js' }); + expect(hints.recommendations.some((r) => r.includes('"type": "module"'))).toBe(false); + }); + + it('produces no recommendations when fully configured', () => { + const hints = analyzePackageJsonHints({ + name: 'foo', + module: './esm/index.js', + sideEffects: false, + type: 'module', + }); + expect(hints.recommendations).toHaveLength(0); + }); }); diff --git a/tools/treeshake-check/src/test-setup.ts b/tools/treeshake-check/src/test-setup.ts new file mode 100644 index 0000000000..3aac00b35b --- /dev/null +++ b/tools/treeshake-check/src/test-setup.ts @@ -0,0 +1,2 @@ +import { addEqualityTesters } from '@effect/vitest'; +addEqualityTesters(); diff --git a/tools/treeshake-check/vitest.config.mts b/tools/treeshake-check/vitest.config.mts index b330e1751a..3d25a4387d 100644 --- a/tools/treeshake-check/vitest.config.mts +++ b/tools/treeshake-check/vitest.config.mts @@ -9,6 +9,7 @@ export default defineConfig(() => ({ globals: true, environment: 'node', include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: ['./src/test-setup.ts'], reporters: ['default'], coverage: { reportsDirectory: './test-output/vitest/coverage', From da3d457d053d9caae90d857d4d041f9959ea413f Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:15:53 -0600 Subject: [PATCH 06/14] test(treeshake-check): add JS fixture packages for integration tests --- .../src/__fixtures__/clean/package.json | 6 ++ .../src/__fixtures__/enum-only/package.json | 5 ++ .../src/__fixtures__/mixed/package.json | 5 ++ .../src/lib/treeshake-check.test.ts | 76 +++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 tools/treeshake-check/src/__fixtures__/clean/package.json create mode 100644 tools/treeshake-check/src/__fixtures__/enum-only/package.json create mode 100644 tools/treeshake-check/src/__fixtures__/mixed/package.json create mode 100644 tools/treeshake-check/src/lib/treeshake-check.test.ts diff --git a/tools/treeshake-check/src/__fixtures__/clean/package.json b/tools/treeshake-check/src/__fixtures__/clean/package.json new file mode 100644 index 0000000000..66e3ad1369 --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/clean/package.json @@ -0,0 +1,6 @@ +{ + "name": "@fixtures/clean", + "version": "0.0.1", + "module": "./dist/index.js", + "sideEffects": false +} diff --git a/tools/treeshake-check/src/__fixtures__/enum-only/package.json b/tools/treeshake-check/src/__fixtures__/enum-only/package.json new file mode 100644 index 0000000000..354b0565c2 --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/enum-only/package.json @@ -0,0 +1,5 @@ +{ + "name": "@fixtures/enum-only", + "version": "0.0.1", + "module": "./dist/index.js" +} diff --git a/tools/treeshake-check/src/__fixtures__/mixed/package.json b/tools/treeshake-check/src/__fixtures__/mixed/package.json new file mode 100644 index 0000000000..16fc03e2b5 --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/mixed/package.json @@ -0,0 +1,5 @@ +{ + "name": "@fixtures/mixed", + "version": "0.0.1", + "module": "./dist/index.js" +} diff --git a/tools/treeshake-check/src/lib/treeshake-check.test.ts b/tools/treeshake-check/src/lib/treeshake-check.test.ts new file mode 100644 index 0000000000..fe25a8d3d6 --- /dev/null +++ b/tools/treeshake-check/src/lib/treeshake-check.test.ts @@ -0,0 +1,76 @@ +// tools/treeshake-check/src/lib/treeshake-check.test.ts +import { describe, expect, it, layer } from '@effect/vitest'; +import { Effect } from 'effect'; +import { NodeContext } from '@effect/platform-node'; +import { FileSystem, Path } from '@effect/platform'; +import { + getEntryFromPackageJson, + PackageJsonNotFound, + MissingEntryPoint, +} from './treeshake-check.js'; + +// Writes a temporary package.json and returns the temp directory path. +// Must run inside a Scope (it.scoped) — the temp dir is deleted when scope closes. +const writeTempPackage = (contents: object) => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fs.makeTempDirectoryScoped(); + yield* fs.writeFileString(path.join(dir, 'package.json'), JSON.stringify(contents)); + return dir; + }); + +layer(NodeContext.layer)('getEntryFromPackageJson', (it) => { + it.scoped('reads module field when present', () => + Effect.gen(function* () { + const dir = yield* writeTempPackage({ + name: 'test', + module: './dist/index.js', + main: './dist/index.cjs', + }); + const result = yield* getEntryFromPackageJson(dir); + expect(result.entry).toBe('./dist/index.js'); + }), + ); + + it.scoped('falls back to main when module is absent', () => + Effect.gen(function* () { + const dir = yield* writeTempPackage({ + name: 'test', + main: './dist/index.js', + }); + const result = yield* getEntryFromPackageJson(dir); + expect(result.entry).toBe('./dist/index.js'); + }), + ); + + it.scoped('returns the full pkg object alongside the entry', () => + Effect.gen(function* () { + const dir = yield* writeTempPackage({ + name: 'my-lib', + module: './esm/index.js', + sideEffects: false, + }); + const result = yield* getEntryFromPackageJson(dir); + expect(result.pkg.name).toBe('my-lib'); + expect(result.pkg.sideEffects).toBe(false); + }), + ); + + it.scoped('fails with PackageJsonNotFound when file is missing', () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const dir = yield* fs.makeTempDirectoryScoped(); // empty — no package.json + const error = yield* Effect.flip(getEntryFromPackageJson(dir)); + expect(error).toBeInstanceOf(PackageJsonNotFound); + }), + ); + + it.scoped('fails with MissingEntryPoint when neither module nor main are present', () => + Effect.gen(function* () { + const dir = yield* writeTempPackage({ name: 'no-entry' }); + const error = yield* Effect.flip(getEntryFromPackageJson(dir)); + expect(error).toBeInstanceOf(MissingEntryPoint); + }), + ); +}); From 6655edb5d623c78d165e2f205841939631cfea0e Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:23:23 -0600 Subject: [PATCH 07/14] test(treeshake-check): annotate writeTempPackage Scope requirement in comment --- tools/treeshake-check/src/lib/treeshake-check.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/treeshake-check/src/lib/treeshake-check.test.ts b/tools/treeshake-check/src/lib/treeshake-check.test.ts index fe25a8d3d6..5189c8209a 100644 --- a/tools/treeshake-check/src/lib/treeshake-check.test.ts +++ b/tools/treeshake-check/src/lib/treeshake-check.test.ts @@ -9,8 +9,7 @@ import { MissingEntryPoint, } from './treeshake-check.js'; -// Writes a temporary package.json and returns the temp directory path. -// Must run inside a Scope (it.scoped) — the temp dir is deleted when scope closes. +// Requires Scope — only call inside it.scoped; temp dir is cleaned up when scope closes. const writeTempPackage = (contents: object) => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; From 30e69a2723f1472316849fbd9c5e10a990062395 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:31:12 -0600 Subject: [PATCH 08/14] test(treeshake-check): add rollup integration tests against JS fixtures --- .../src/treeshake-check.integration.test.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tools/treeshake-check/src/treeshake-check.integration.test.ts diff --git a/tools/treeshake-check/src/treeshake-check.integration.test.ts b/tools/treeshake-check/src/treeshake-check.integration.test.ts new file mode 100644 index 0000000000..5b76bbdbd8 --- /dev/null +++ b/tools/treeshake-check/src/treeshake-check.integration.test.ts @@ -0,0 +1,74 @@ +// tools/treeshake-check/src/treeshake-check.integration.test.ts +import { describe, expect, live } from '@effect/vitest'; +import { Effect } from 'effect'; +import { NodeContext } from '@effect/platform-node'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { analyzeTreeshakeability } from './lib/treeshake-check.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +const fixturePath = (name: string) => resolve(__dirname, '__fixtures__', name, 'dist', 'index.js'); + +const run = (effect: Effect.Effect) => + effect.pipe(Effect.provide(NodeContext.layer)); + +describe('analyzeTreeshakeability integration', () => { + live('reports clean package as fully tree-shakeable', () => + run( + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('clean')); + expect(result._tag).toBe('FullyTreeshakeable'); + }), + ), + ); + + live('reports enum-only package as having side effects', () => + run( + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); + expect(result._tag).toBe('HasSideEffects'); + if (result._tag === 'HasSideEffects') { + const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); + expect(allCauses).toContain('EnumPattern'); + } + }), + ), + ); + + live('reports mixed package with both EnumPattern and PrototypeMutation', () => + run( + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('mixed')); + expect(result._tag).toBe('HasSideEffects'); + if (result._tag === 'HasSideEffects') { + const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); + expect(allCauses).toContain('EnumPattern'); + expect(allCauses).toContain('PrototypeMutation'); + } + }), + ), + ); + + live('returns totalRenderedBytes > 0 for side-effectful packages', () => + run( + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); + expect(result._tag).toBe('HasSideEffects'); + if (result._tag === 'HasSideEffects') { + expect(result.totalRenderedBytes).toBeGreaterThan(0); + expect(result.totalOriginalBytes).toBeGreaterThan(0); + } + }), + ), + ); + + live('returns FullyTreeshakeable with no modules for clean package', () => + run( + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('clean')); + expect(result._tag).toBe('FullyTreeshakeable'); + }), + ), + ); +}); From c8940d9d50d379b826ca4baf00b27175a2d0d422 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:39:27 -0600 Subject: [PATCH 09/14] test(treeshake-check): strengthen integration tests with assert narrowing and error path Replace if-guard narrowing with vitest assert() so type narrowing doubles as a hard assertion. Add hints assertions on the clean-package test. Replace the duplicate clean-package test with a BundleFailed error-path test backed by a bad-syntax fixture. --- .../src/__fixtures__/bad-syntax/dist/index.js | 1 + .../src/treeshake-check.integration.test.ts | 40 +++++++++---------- 2 files changed, 20 insertions(+), 21 deletions(-) create mode 100644 tools/treeshake-check/src/__fixtures__/bad-syntax/dist/index.js diff --git a/tools/treeshake-check/src/__fixtures__/bad-syntax/dist/index.js b/tools/treeshake-check/src/__fixtures__/bad-syntax/dist/index.js new file mode 100644 index 0000000000..9d688c49ad --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/bad-syntax/dist/index.js @@ -0,0 +1 @@ +this is not valid javascript !!!### diff --git a/tools/treeshake-check/src/treeshake-check.integration.test.ts b/tools/treeshake-check/src/treeshake-check.integration.test.ts index 5b76bbdbd8..c1121723e6 100644 --- a/tools/treeshake-check/src/treeshake-check.integration.test.ts +++ b/tools/treeshake-check/src/treeshake-check.integration.test.ts @@ -1,10 +1,11 @@ // tools/treeshake-check/src/treeshake-check.integration.test.ts import { describe, expect, live } from '@effect/vitest'; +import { assert } from 'vitest'; import { Effect } from 'effect'; import { NodeContext } from '@effect/platform-node'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { analyzeTreeshakeability } from './lib/treeshake-check.js'; +import { analyzeTreeshakeability, BundleFailed } from './lib/treeshake-check.js'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); @@ -18,7 +19,10 @@ describe('analyzeTreeshakeability integration', () => { run( Effect.gen(function* () { const result = yield* analyzeTreeshakeability(fixturePath('clean')); - expect(result._tag).toBe('FullyTreeshakeable'); + assert(result._tag === 'FullyTreeshakeable'); + expect(result.hints.hasSideEffectsField).toBe(false); + expect(result.hints.hasModuleField).toBe(false); + expect(result.hints.recommendations).toHaveLength(0); }), ), ); @@ -27,11 +31,9 @@ describe('analyzeTreeshakeability integration', () => { run( Effect.gen(function* () { const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); - expect(result._tag).toBe('HasSideEffects'); - if (result._tag === 'HasSideEffects') { - const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); - expect(allCauses).toContain('EnumPattern'); - } + assert(result._tag === 'HasSideEffects'); + const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); + expect(allCauses).toContain('EnumPattern'); }), ), ); @@ -40,12 +42,10 @@ describe('analyzeTreeshakeability integration', () => { run( Effect.gen(function* () { const result = yield* analyzeTreeshakeability(fixturePath('mixed')); - expect(result._tag).toBe('HasSideEffects'); - if (result._tag === 'HasSideEffects') { - const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); - expect(allCauses).toContain('EnumPattern'); - expect(allCauses).toContain('PrototypeMutation'); - } + assert(result._tag === 'HasSideEffects'); + const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); + expect(allCauses).toContain('EnumPattern'); + expect(allCauses).toContain('PrototypeMutation'); }), ), ); @@ -54,20 +54,18 @@ describe('analyzeTreeshakeability integration', () => { run( Effect.gen(function* () { const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); - expect(result._tag).toBe('HasSideEffects'); - if (result._tag === 'HasSideEffects') { - expect(result.totalRenderedBytes).toBeGreaterThan(0); - expect(result.totalOriginalBytes).toBeGreaterThan(0); - } + assert(result._tag === 'HasSideEffects'); + expect(result.totalRenderedBytes).toBeGreaterThan(0); + expect(result.totalOriginalBytes).toBeGreaterThan(0); }), ), ); - live('returns FullyTreeshakeable with no modules for clean package', () => + live('fails with BundleFailed when entry file has a syntax error', () => run( Effect.gen(function* () { - const result = yield* analyzeTreeshakeability(fixturePath('clean')); - expect(result._tag).toBe('FullyTreeshakeable'); + const error = yield* Effect.flip(analyzeTreeshakeability(fixturePath('bad-syntax'))); + expect(error).toBeInstanceOf(BundleFailed); }), ), ); From a0a299bfcb23791bc4e4b0a823f71e47a9f95525 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 10:46:34 -0600 Subject: [PATCH 10/14] refactor(treeshake-check): pipe style + Schema.encode in renderJson --- tools/treeshake-check/src/index.ts | 10 ++++--- .../src/lib/treeshake-check.ts | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/tools/treeshake-check/src/index.ts b/tools/treeshake-check/src/index.ts index 1e9e7e08e0..1b01075c3b 100644 --- a/tools/treeshake-check/src/index.ts +++ b/tools/treeshake-check/src/index.ts @@ -2,10 +2,10 @@ // src/index.ts import { Command, Options } from '@effect/cli'; import { NodeContext, NodeRuntime } from '@effect/platform-node'; -import { Console, Effect, Option } from 'effect'; +import { Console, Effect, Option, Schema, pipe } from 'effect'; import { analyzeTreeshakeability, checkPackage } from './lib/treeshake-check.js'; import { EXPLANATIONS, primaryCause } from './lib/explanations.js'; -import type { TreeshakeResult } from './lib/schemas.js'; +import { TreeshakeResult } from './lib/schemas.js'; const tree = ` \\\\/// /Thanks @@ -61,7 +61,11 @@ const SEPARATOR = ' ─────────────────── // ─── Renderers ─────────────────────────────────────────────────────────────── -const renderJson = (result: TreeshakeResult) => Console.log(JSON.stringify(result, null, 2)); +const renderJson = (result: TreeshakeResult) => + pipe( + Schema.encode(TreeshakeResult)(result), + Effect.flatMap((encoded) => Console.log(JSON.stringify(encoded, null, 2))), + ); const renderHuman = (result: TreeshakeResult, topN: Option.Option) => Effect.gen(function* () { diff --git a/tools/treeshake-check/src/lib/treeshake-check.ts b/tools/treeshake-check/src/lib/treeshake-check.ts index a81431b7f1..16ae9bb50a 100644 --- a/tools/treeshake-check/src/lib/treeshake-check.ts +++ b/tools/treeshake-check/src/lib/treeshake-check.ts @@ -1,6 +1,6 @@ // src/lib/treeshake-check.ts import { FileSystem, Path } from '@effect/platform'; -import { Data, Effect, Schema } from 'effect'; +import { Data, Effect, Schema, pipe } from 'effect'; import virtualPlugin from '@rollup/plugin-virtual'; import { rollup, type RollupBuild } from 'rollup'; import { analyzePackageJsonHints, buildModuleAnalysis, defaultHints } from './analysis.js'; @@ -48,14 +48,15 @@ const readPackageJson = (cwd: string) => }); export const getEntryFromPackageJson = (cwd?: string) => - Effect.gen(function* () { - const { pkg, pkgPath } = yield* readPackageJson(cwd ?? process.cwd()); - const entry = pkg.module ?? pkg.main; - if (entry === undefined) { - return yield* new MissingEntryPoint({ path: pkgPath }); - } - return { entry, pkg } as const; - }); + pipe( + readPackageJson(cwd ?? process.cwd()), + Effect.flatMap(({ pkg, pkgPath }) => { + const entry = pkg.module ?? pkg.main; + return entry !== undefined + ? Effect.succeed({ entry, pkg } as const) + : new MissingEntryPoint({ path: pkgPath }); + }), + ); // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -167,7 +168,7 @@ export const analyzeTreeshakeability = ( * Full check: read package.json from `cwd`, resolve its entry, then analyze. */ export const checkPackage = (cwd?: string) => - Effect.gen(function* () { - const { entry, pkg } = yield* getEntryFromPackageJson(cwd); - return yield* analyzeTreeshakeability(entry, pkg); - }); + pipe( + getEntryFromPackageJson(cwd), + Effect.flatMap(({ entry, pkg }) => analyzeTreeshakeability(entry, pkg)), + ); From 46f48cfa073de3a8d756593ba6a1a27978405cd2 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 11:50:13 -0600 Subject: [PATCH 11/14] feat(treeshake-check): acorn AST-based UnannotatedCall detection Replaces the regex heuristic for top-level call detection with an acorn AST walk so import statements and variable-assigned calls no longer false-positive as UnannotatedCall. Regex remains as fallback for unparseable code. --- pnpm-lock.yaml | 24 ++++------- tools/treeshake-check/package.json | 1 + .../treeshake-check/src/lib/analysis.test.ts | 23 ++++++++++ tools/treeshake-check/src/lib/analysis.ts | 42 ++++++++++++------- 4 files changed, 60 insertions(+), 30 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46aad500aa..2412fedeff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -686,6 +686,9 @@ importers: '@rollup/plugin-virtual': specifier: ^3.0.2 version: 3.0.2(rollup@4.59.0) + acorn: + specifier: ^8.16.0 + version: 8.16.0 effect: specifier: catalog:effect version: 3.21.0 @@ -3910,11 +3913,6 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -12464,19 +12462,13 @@ snapshots: dependencies: acorn: 8.16.0 - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn-walk@8.3.4: dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} + acorn: 8.16.0 acorn@8.16.0: {} @@ -13894,8 +13886,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -15358,7 +15350,7 @@ snapshots: jsonc-eslint-parser@2.4.1: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 eslint-visitor-keys: 3.4.3 espree: 9.6.1 semver: 7.7.3 @@ -17271,7 +17263,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 24.9.2 - acorn: 8.15.0 + acorn: 8.16.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff --git a/tools/treeshake-check/package.json b/tools/treeshake-check/package.json index fa002df2d1..edb1a76a88 100644 --- a/tools/treeshake-check/package.json +++ b/tools/treeshake-check/package.json @@ -27,6 +27,7 @@ "@effect/platform": "catalog:effect", "@effect/platform-node": "catalog:effect", "@rollup/plugin-virtual": "^3.0.2", + "acorn": "^8.16.0", "effect": "catalog:effect", "rollup": "^4.59.0" } diff --git a/tools/treeshake-check/src/lib/analysis.test.ts b/tools/treeshake-check/src/lib/analysis.test.ts index d6b10cedb8..47c9e8a404 100644 --- a/tools/treeshake-check/src/lib/analysis.test.ts +++ b/tools/treeshake-check/src/lib/analysis.test.ts @@ -79,6 +79,29 @@ describe('detectCauses', () => { expect(causes).toContain('EnumPattern'); expect(causes).toContain('GlobalAssignment'); }); + + // AST precision tests — cases the regex approach got wrong + it('does NOT flag import statements as calls (AST precision)', () => { + const code = `import { foo } from 'bar';\nexport const x = 1;`; + expect(new Set(detectCauses(code))).not.toContain('UnannotatedCall'); + }); + + it('does NOT flag variable-assigned calls as UnannotatedCall (AST precision)', () => { + const code = `const x = createStore();\nexport { x };`; + expect(new Set(detectCauses(code))).not.toContain('UnannotatedCall'); + }); + + it('flags bare top-level call expression statement with no annotation (AST precision)', () => { + const code = `initializeGlobals();\nexport const x = 1;`; + const causes = new Set(detectCauses(code)); + expect(causes).toContain('UnannotatedCall'); + expect(causes).toContain('TopLevelSideEffect'); + }); + + it('does not flag PURE-annotated bare call (AST precision)', () => { + const code = `/*#__PURE__*/createRegistry();\nexport const x = 1;`; + expect(new Set(detectCauses(code))).not.toContain('UnannotatedCall'); + }); }); // ─── buildModuleAnalysis ────────────────────────────────────────────────────── diff --git a/tools/treeshake-check/src/lib/analysis.ts b/tools/treeshake-check/src/lib/analysis.ts index 995df02025..fb8ff345bb 100644 --- a/tools/treeshake-check/src/lib/analysis.ts +++ b/tools/treeshake-check/src/lib/analysis.ts @@ -1,22 +1,37 @@ // src/lib/analysis.ts +import * as acorn from 'acorn'; import type { ModuleAnalysis, PackageJson, PackageJsonHints, SuspectedCause } from './schemas.js'; -/** - * Heuristic detection of common patterns that prevent tree-shaking. - * - * This is regex-based and approximate — a rigorous implementation would - * AST-walk the surviving code. But these patterns catch the vast majority - * of real-world cases, and labeling them "suspected" makes the uncertainty - * honest. - */ -// src/lib/analysis.ts (additions) +// Walk the AST to find top-level ExpressionStatement → CallExpression nodes +// that are not preceded by a /*#__PURE__*/ annotation. Falls back to a regex +// heuristic when acorn cannot parse the code (e.g. unparseable rollup output). +const detectTopLevelCall = (code: string): boolean => { + let ast: acorn.Program; + try { + ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module' }); + } catch { + const topLevelCall = /^(?!.*\/\*#__PURE__\*\/)\s*[a-zA-Z_$][\w$]*\s*\(/m; + return topLevelCall.test(code); + } + + return ast.body.some((node) => { + if (node.type !== 'ExpressionStatement') return false; + const expr = (node as acorn.ExpressionStatement).expression; + if (expr.type !== 'CallExpression') return false; + + // Check for a /*#__PURE__*/ annotation immediately before this node + const preceding = code.slice(0, node.start); + const annotationIdx = preceding.lastIndexOf('/*#__PURE__*/'); + if (annotationIdx === -1) return true; + const between = preceding.slice(annotationIdx + '/*#__PURE__*/'.length); + return !/^\s*$/.test(between); + }); +}; export const detectCauses = (code: string): ReadonlyArray => { const causes = new Set(); // TypeScript enum IIFE: `(function (X) { ... })(X || (X = {}));` - // This is the most common cause for type-package authors and produces - // the highest false-negative rate in the original heuristics. if ( /\(function\s*\([A-Z_$][\w$]*\)\s*\{[\s\S]*?\}\)\s*\(\s*[A-Z_$][\w$]*\s*\|\|\s*\(\s*[A-Z_$][\w$]*\s*=\s*\{\s*\}\s*\)\s*\)/.test( code, @@ -43,9 +58,8 @@ export const detectCauses = (code: string): ReadonlyArray => { causes.add('GlobalAssignment'); } - // Bare top-level function call without /*#__PURE__*/ - const topLevelCall = /^(?!.*\/\*#__PURE__\*\/)\s*[a-zA-Z_$][\w$]*\s*\(/m; - if (topLevelCall.test(code) && !causes.has('EnumPattern')) { + // Bare top-level call without /*#__PURE__*/ — AST-based, regex fallback + if (!causes.has('EnumPattern') && detectTopLevelCall(code)) { causes.add('UnannotatedCall'); causes.add('TopLevelSideEffect'); } From 99ffed1315323355197177eeba4af491183c0a29 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Fri, 1 May 2026 11:58:39 -0600 Subject: [PATCH 12/14] feat(treeshake-check): resolve entry from exports field Adds resolveEntry() which reads the exports field (subpath map, flat conditions, or bare string) before falling back to module/main. Conditions priority: import > module > default. require-only exports correctly return MissingEntryPoint rather than silently using a CJS path. --- .../treeshake-check/src/lib/analysis.test.ts | 77 ++++++++++++++++++- tools/treeshake-check/src/lib/analysis.ts | 33 ++++++++ tools/treeshake-check/src/lib/schemas.ts | 10 +++ .../src/lib/treeshake-check.test.ts | 22 ++++++ .../src/lib/treeshake-check.ts | 9 ++- 5 files changed, 148 insertions(+), 3 deletions(-) diff --git a/tools/treeshake-check/src/lib/analysis.test.ts b/tools/treeshake-check/src/lib/analysis.test.ts index 47c9e8a404..4f4236b427 100644 --- a/tools/treeshake-check/src/lib/analysis.test.ts +++ b/tools/treeshake-check/src/lib/analysis.test.ts @@ -1,6 +1,11 @@ // tools/treeshake-check/src/lib/analysis.test.ts import { describe, it, expect } from '@effect/vitest'; -import { detectCauses, buildModuleAnalysis, analyzePackageJsonHints } from './analysis.js'; +import { + detectCauses, + buildModuleAnalysis, + analyzePackageJsonHints, + resolveEntry, +} from './analysis.js'; // ─── detectCauses ───────────────────────────────────────────────────────────── @@ -210,3 +215,73 @@ describe('analyzePackageJsonHints', () => { expect(hints.recommendations).toHaveLength(0); }); }); + +// ─── resolveEntry ───────────────────────────────────────────────────────────── + +describe('resolveEntry', () => { + // exports field — subpath map with conditions + it('reads import condition from exports["."]]', () => { + expect( + resolveEntry({ + exports: { '.': { import: './dist/esm/index.js', require: './dist/cjs/index.js' } }, + }), + ).toBe('./dist/esm/index.js'); + }); + + it('reads module condition from exports["."] when import is absent', () => { + expect( + resolveEntry({ + exports: { '.': { module: './dist/esm/index.js', require: './dist/cjs/index.js' } }, + }), + ).toBe('./dist/esm/index.js'); + }); + + it('reads default condition from exports["."] when no esm-specific condition', () => { + expect(resolveEntry({ exports: { '.': { default: './dist/index.js' } } })).toBe( + './dist/index.js', + ); + }); + + // exports field — flat conditions (no subpath key) + it('reads import condition from flat exports object', () => { + expect( + resolveEntry({ exports: { import: './dist/esm/index.js', require: './dist/cjs/index.js' } }), + ).toBe('./dist/esm/index.js'); + }); + + // exports field — bare string shorthand + it('reads bare string exports field', () => { + expect(resolveEntry({ exports: './dist/index.js' })).toBe('./dist/index.js'); + }); + + // exports field takes priority over module/main + it('prefers exports over module and main', () => { + expect( + resolveEntry({ + exports: { '.': { import: './esm/index.js' } }, + module: './module/index.js', + main: './main/index.js', + }), + ).toBe('./esm/index.js'); + }); + + // fallback chain + it('falls back to module when no exports field', () => { + expect(resolveEntry({ module: './dist/index.js', main: './dist/cjs/index.js' })).toBe( + './dist/index.js', + ); + }); + + it('falls back to main when no exports or module field', () => { + expect(resolveEntry({ main: './dist/index.js' })).toBe('./dist/index.js'); + }); + + it('returns undefined when no entry can be resolved', () => { + expect(resolveEntry({ name: 'no-entry' })).toBeUndefined(); + }); + + // CJS-only exports should be skipped (require-only map with no default) + it('returns undefined for require-only exports map with no default', () => { + expect(resolveEntry({ exports: { '.': { require: './dist/cjs/index.js' } } })).toBeUndefined(); + }); +}); diff --git a/tools/treeshake-check/src/lib/analysis.ts b/tools/treeshake-check/src/lib/analysis.ts index fb8ff345bb..5cdcd82549 100644 --- a/tools/treeshake-check/src/lib/analysis.ts +++ b/tools/treeshake-check/src/lib/analysis.ts @@ -28,6 +28,39 @@ const detectTopLevelCall = (code: string): boolean => { }); }; +/** + * Resolve the ESM entry point from a package.json. + * + * Priority: exports["." | flat] → module → main + * Within the exports conditions, priority is: import → module → default + * Returns undefined when no usable ESM entry can be found (e.g. require-only). + */ +export const resolveEntry = (pkg: PackageJson): string | undefined => { + const { exports } = pkg; + + if (exports !== undefined) { + if (typeof exports === 'string') return exports; + + // exports is a record — determine whether it's a subpath map or flat conditions + const dotEntry = (exports as Record)['.']; + const conditions: unknown = dotEntry !== undefined ? dotEntry : exports; + + if (typeof conditions === 'string') return conditions; + + if (conditions !== null && typeof conditions === 'object') { + const c = conditions as Record; + const candidate = c['import'] ?? c['module'] ?? c['default']; + if (typeof candidate === 'string') return candidate; + } + + // No usable ESM condition found in exports — don't fall through to module/main. + // Falling through would return a CJS path dressed as ESM. + return undefined; + } + + return pkg.module ?? pkg.main; +}; + export const detectCauses = (code: string): ReadonlyArray => { const causes = new Set(); diff --git a/tools/treeshake-check/src/lib/schemas.ts b/tools/treeshake-check/src/lib/schemas.ts index 9c2a3ce2dc..5836aa2d40 100644 --- a/tools/treeshake-check/src/lib/schemas.ts +++ b/tools/treeshake-check/src/lib/schemas.ts @@ -5,10 +5,20 @@ import { Schema } from 'effect'; export const SideEffectsValue = Schema.Union(Schema.Boolean, Schema.Array(Schema.String)); +// Conditions object: { import?: string, module?: string, require?: string, default?: string, ... } +export const ExportsConditions = Schema.Record({ + key: Schema.String, + value: Schema.Union(Schema.String, Schema.Unknown), +}); + +// exports field: string | conditions | { ".": conditions | string } +export const ExportsField = Schema.Union(Schema.String, ExportsConditions); + export const PackageJson = Schema.Struct({ name: Schema.optional(Schema.String), module: Schema.optional(Schema.String), main: Schema.optional(Schema.String), + exports: Schema.optional(ExportsField), type: Schema.optional(Schema.Literal('module', 'commonjs')), sideEffects: Schema.optional(SideEffectsValue), dependencies: Schema.optional(Schema.Unknown), diff --git a/tools/treeshake-check/src/lib/treeshake-check.test.ts b/tools/treeshake-check/src/lib/treeshake-check.test.ts index 5189c8209a..7380df7c9e 100644 --- a/tools/treeshake-check/src/lib/treeshake-check.test.ts +++ b/tools/treeshake-check/src/lib/treeshake-check.test.ts @@ -65,6 +65,17 @@ layer(NodeContext.layer)('getEntryFromPackageJson', (it) => { }), ); + it.scoped('reads import condition from exports field', () => + Effect.gen(function* () { + const dir = yield* writeTempPackage({ + name: 'exports-pkg', + exports: { '.': { import: './dist/esm/index.js', require: './dist/cjs/index.js' } }, + }); + const result = yield* getEntryFromPackageJson(dir); + expect(result.entry).toBe('./dist/esm/index.js'); + }), + ); + it.scoped('fails with MissingEntryPoint when neither module nor main are present', () => Effect.gen(function* () { const dir = yield* writeTempPackage({ name: 'no-entry' }); @@ -72,4 +83,15 @@ layer(NodeContext.layer)('getEntryFromPackageJson', (it) => { expect(error).toBeInstanceOf(MissingEntryPoint); }), ); + + it.scoped('fails with MissingEntryPoint when exports has only require condition', () => + Effect.gen(function* () { + const dir = yield* writeTempPackage({ + name: 'cjs-only', + exports: { '.': { require: './dist/cjs/index.js' } }, + }); + const error = yield* Effect.flip(getEntryFromPackageJson(dir)); + expect(error).toBeInstanceOf(MissingEntryPoint); + }), + ); }); diff --git a/tools/treeshake-check/src/lib/treeshake-check.ts b/tools/treeshake-check/src/lib/treeshake-check.ts index 16ae9bb50a..9cd440263d 100644 --- a/tools/treeshake-check/src/lib/treeshake-check.ts +++ b/tools/treeshake-check/src/lib/treeshake-check.ts @@ -3,7 +3,12 @@ import { FileSystem, Path } from '@effect/platform'; import { Data, Effect, Schema, pipe } from 'effect'; import virtualPlugin from '@rollup/plugin-virtual'; import { rollup, type RollupBuild } from 'rollup'; -import { analyzePackageJsonHints, buildModuleAnalysis, defaultHints } from './analysis.js'; +import { + analyzePackageJsonHints, + buildModuleAnalysis, + defaultHints, + resolveEntry, +} from './analysis.js'; import { PackageJsonFromString, type PackageJson, @@ -51,7 +56,7 @@ export const getEntryFromPackageJson = (cwd?: string) => pipe( readPackageJson(cwd ?? process.cwd()), Effect.flatMap(({ pkg, pkgPath }) => { - const entry = pkg.module ?? pkg.main; + const entry = resolveEntry(pkg); return entry !== undefined ? Effect.succeed({ entry, pkg } as const) : new MissingEntryPoint({ path: pkgPath }); From b6f45458b76df4e4d159806b41214cef5d64a592 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 5 May 2026 15:20:16 -0600 Subject: [PATCH 13/14] test(treeshake-check): commit fixture dist files and unblock gitignore The root .gitignore's **/dist rule was silently excluding the fixture dist files from git. In CI these files didn't exist, so rollup treated the entry as external and returned FullyTreeshakeable for enum-only and mixed, failing their HasSideEffects assertions. - Adds a fixtures-scoped .gitignore negation (consistent with how bad-syntax/dist is already tracked) - Commits the three missing dist/index.js fixture files - Excludes __fixtures__/**/dist from the eslint plugin scan - Adds the __fixtures__ pattern to nx.json eslint plugin exclude list - Migrates integration tests from `live` to `layer + it.scoped` to align with the rest of the test suite and fix type compatibility --- nx.json | 2 +- tools/treeshake-check/eslint.config.mjs | 2 +- .../src/__fixtures__/.gitignore | 2 + .../src/__fixtures__/clean/dist/index.js | 10 +++ .../src/__fixtures__/enum-only/dist/index.js | 9 ++ .../src/__fixtures__/mixed/dist/index.js | 15 ++++ .../src/treeshake-check.integration.test.ts | 87 ++++++++----------- 7 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 tools/treeshake-check/src/__fixtures__/.gitignore create mode 100644 tools/treeshake-check/src/__fixtures__/clean/dist/index.js create mode 100644 tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js create mode 100644 tools/treeshake-check/src/__fixtures__/mixed/dist/index.js diff --git a/nx.json b/nx.json index 69d8ec8a18..72d7447d3c 100644 --- a/nx.json +++ b/nx.json @@ -125,7 +125,7 @@ "targetName": "nxLint" }, "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"], - "exclude": ["tools/**/fixtures/**/*"] + "exclude": ["tools/**/fixtures/**/*", "tools/**/__fixtures__/**/*"] }, { "plugin": "@nx/vite/plugin", diff --git a/tools/treeshake-check/eslint.config.mjs b/tools/treeshake-check/eslint.config.mjs index 0a23ec0779..4dd64666b7 100644 --- a/tools/treeshake-check/eslint.config.mjs +++ b/tools/treeshake-check/eslint.config.mjs @@ -20,6 +20,6 @@ export default [ }, }, { - ignores: ['**/out-tsc'], + ignores: ['**/out-tsc', '**/__fixtures__/**/dist'], }, ]; diff --git a/tools/treeshake-check/src/__fixtures__/.gitignore b/tools/treeshake-check/src/__fixtures__/.gitignore new file mode 100644 index 0000000000..afe15553cb --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/.gitignore @@ -0,0 +1,2 @@ +!dist/ +!dist/** diff --git a/tools/treeshake-check/src/__fixtures__/clean/dist/index.js b/tools/treeshake-check/src/__fixtures__/clean/dist/index.js new file mode 100644 index 0000000000..9735c411c4 --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/clean/dist/index.js @@ -0,0 +1,10 @@ +// Pure function exports — no side effects, fully tree-shakeable +export function add(a, b) { + return a + b; +} + +export function multiply(a, b) { + return a * b; +} + +export const PI = 3.14159; diff --git a/tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js b/tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js new file mode 100644 index 0000000000..dbb2ae9e39 --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js @@ -0,0 +1,9 @@ +// TypeScript enum compiled output — classic IIFE pattern that prevents tree-shaking +var Status; +(function (Status) { + Status["Active"] = "Active"; + Status["Inactive"] = "Inactive"; + Status["Pending"] = "Pending"; +})(Status || (Status = {})); + +export { Status }; diff --git a/tools/treeshake-check/src/__fixtures__/mixed/dist/index.js b/tools/treeshake-check/src/__fixtures__/mixed/dist/index.js new file mode 100644 index 0000000000..ed3bf516fa --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/mixed/dist/index.js @@ -0,0 +1,15 @@ +// Multiple side-effect patterns in one module + +// Enum IIFE +var Color; +(function (Color) { + Color["Red"] = "Red"; + Color["Blue"] = "Blue"; +})(Color || (Color = {})); + +// Prototype mutation +String.prototype.toColor = function() { + return Color[this]; +}; + +export { Color }; diff --git a/tools/treeshake-check/src/treeshake-check.integration.test.ts b/tools/treeshake-check/src/treeshake-check.integration.test.ts index c1121723e6..134575f9e7 100644 --- a/tools/treeshake-check/src/treeshake-check.integration.test.ts +++ b/tools/treeshake-check/src/treeshake-check.integration.test.ts @@ -1,5 +1,5 @@ // tools/treeshake-check/src/treeshake-check.integration.test.ts -import { describe, expect, live } from '@effect/vitest'; +import { expect, layer } from '@effect/vitest'; import { assert } from 'vitest'; import { Effect } from 'effect'; import { NodeContext } from '@effect/platform-node'; @@ -11,62 +11,49 @@ const __dirname = fileURLToPath(new URL('.', import.meta.url)); const fixturePath = (name: string) => resolve(__dirname, '__fixtures__', name, 'dist', 'index.js'); -const run = (effect: Effect.Effect) => - effect.pipe(Effect.provide(NodeContext.layer)); - -describe('analyzeTreeshakeability integration', () => { - live('reports clean package as fully tree-shakeable', () => - run( - Effect.gen(function* () { - const result = yield* analyzeTreeshakeability(fixturePath('clean')); - assert(result._tag === 'FullyTreeshakeable'); - expect(result.hints.hasSideEffectsField).toBe(false); - expect(result.hints.hasModuleField).toBe(false); - expect(result.hints.recommendations).toHaveLength(0); - }), - ), +layer(NodeContext.layer)('analyzeTreeshakeability integration', (it) => { + it.scoped('reports clean package as fully tree-shakeable', () => + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('clean')); + assert(result._tag === 'FullyTreeshakeable'); + expect(result.hints.hasSideEffectsField).toBe(false); + expect(result.hints.hasModuleField).toBe(false); + expect(result.hints.recommendations).toHaveLength(0); + }), ); - live('reports enum-only package as having side effects', () => - run( - Effect.gen(function* () { - const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); - assert(result._tag === 'HasSideEffects'); - const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); - expect(allCauses).toContain('EnumPattern'); - }), - ), + it.scoped('reports enum-only package as having side effects', () => + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); + assert(result._tag === 'HasSideEffects'); + const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); + expect(allCauses).toContain('EnumPattern'); + }), ); - live('reports mixed package with both EnumPattern and PrototypeMutation', () => - run( - Effect.gen(function* () { - const result = yield* analyzeTreeshakeability(fixturePath('mixed')); - assert(result._tag === 'HasSideEffects'); - const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); - expect(allCauses).toContain('EnumPattern'); - expect(allCauses).toContain('PrototypeMutation'); - }), - ), + it.scoped('reports mixed package with both EnumPattern and PrototypeMutation', () => + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('mixed')); + assert(result._tag === 'HasSideEffects'); + const allCauses = new Set(result.modules.flatMap((m) => m.suspectedCauses)); + expect(allCauses).toContain('EnumPattern'); + expect(allCauses).toContain('PrototypeMutation'); + }), ); - live('returns totalRenderedBytes > 0 for side-effectful packages', () => - run( - Effect.gen(function* () { - const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); - assert(result._tag === 'HasSideEffects'); - expect(result.totalRenderedBytes).toBeGreaterThan(0); - expect(result.totalOriginalBytes).toBeGreaterThan(0); - }), - ), + it.scoped('returns totalRenderedBytes > 0 for side-effectful packages', () => + Effect.gen(function* () { + const result = yield* analyzeTreeshakeability(fixturePath('enum-only')); + assert(result._tag === 'HasSideEffects'); + expect(result.totalRenderedBytes).toBeGreaterThan(0); + expect(result.totalOriginalBytes).toBeGreaterThan(0); + }), ); - live('fails with BundleFailed when entry file has a syntax error', () => - run( - Effect.gen(function* () { - const error = yield* Effect.flip(analyzeTreeshakeability(fixturePath('bad-syntax'))); - expect(error).toBeInstanceOf(BundleFailed); - }), - ), + it.scoped('fails with BundleFailed when entry file has a syntax error', () => + Effect.gen(function* () { + const error = yield* Effect.flip(analyzeTreeshakeability(fixturePath('bad-syntax'))); + expect(error).toBeInstanceOf(BundleFailed); + }), ); }); From 8a8cefeadea7142e57f2b2ece627d3a5431469db Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 5 May 2026 15:32:54 -0600 Subject: [PATCH 14/14] test(treeshake-check): move fixture JS out of dist/ and fix CI failures Fixture JS files are hand-authored test data, not build output. Moving them from dist/index.js to index.js avoids conflicts with the root **/dist gitignore rule, which was silently excluding them from git. In CI the files were missing, causing rollup to treat them as external and return FullyTreeshakeable for enum-only and mixed, failing the HasSideEffects assertions. Also migrates integration tests from `live` to `layer + it.scoped` to align with the rest of the test suite. --- .prettierignore | 1 + package.json | 3 +- .../api-report/davinci-client.api.md | 2 +- .../api-report/davinci-client.types.api.md | 2 +- pnpm-lock.yaml | 121 +++++++++--------- tools/treeshake-check/eslint.config.mjs | 2 +- .../src/__fixtures__/.gitignore | 2 - .../bad-syntax/{dist => }/index.js | 0 .../__fixtures__/clean/{dist => }/index.js | 0 .../enum-only/{dist => }/index.js | 6 +- .../__fixtures__/mixed/{dist => }/index.js | 6 +- .../src/treeshake-check.integration.test.ts | 2 +- 12 files changed, 71 insertions(+), 76 deletions(-) delete mode 100644 tools/treeshake-check/src/__fixtures__/.gitignore rename tools/treeshake-check/src/__fixtures__/bad-syntax/{dist => }/index.js (100%) rename tools/treeshake-check/src/__fixtures__/clean/{dist => }/index.js (100%) rename tools/treeshake-check/src/__fixtures__/enum-only/{dist => }/index.js (63%) rename tools/treeshake-check/src/__fixtures__/mixed/{dist => }/index.js (67%) diff --git a/.prettierignore b/.prettierignore index 1607ccdbb2..e84621ac18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ e2e/*/dist */dist **/dist/* **/**/*.d.ts* +tools/treeshake-check/src/__fixtures__/** diff --git a/package.json b/package.json index 963563ad14..b56904c235 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,8 @@ "rollup": "^4.59.0", "undici@^7": "^7.24.0", "picomatch@>=4": "^4.0.4", - "picomatch@<3": "^2.3.2" + "picomatch@<3": "^2.3.2", + "yaml": ">=2.8.3" } } } diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index 33cb405246..289c8a5276 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -270,7 +270,7 @@ export function davinci(input: { start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; - poll: (collector: PollingCollector) => Poller; + pollStatus: (collector: PollingCollector) => Poller; getClient: () => { action: string; collectors: Collectors[]; diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index b4897b665c..05f38634dc 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -270,7 +270,7 @@ export function davinci(input: { start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; - poll: (collector: PollingCollector) => Poller; + pollStatus: (collector: PollingCollector) => Poller; getClient: () => { action: string; collectors: Collectors[]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2412fedeff..73f7ddbec0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,7 @@ overrides: undici@^7: ^7.24.0 picomatch@>=4: ^4.0.4 picomatch@<3: ^2.3.2 + yaml: '>=2.8.3' importers: @@ -121,10 +122,10 @@ importers: version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.4(jiti@2.6.1))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0)) '@nx/vite': specifier: 22.6.5 - version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))(vitest@3.2.4) '@nx/vitest': specifier: 22.6.5 - version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))(vitest@3.2.4) '@nx/web': specifier: 22.6.5 version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) @@ -271,10 +272,10 @@ importers: version: 6.5.2(typanion@3.14.0) vite: specifier: catalog:vite - version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) vitest-canvas-mock: specifier: catalog:vitest version: 1.1.3(vitest@3.2.4) @@ -398,7 +399,7 @@ importers: version: 0.27.0(effect@3.21.0)(vitest@3.2.4) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) e2e/oidc-app: dependencies: @@ -457,7 +458,7 @@ importers: version: link:../../tools/treeshake-check vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) packages/device-client: dependencies: @@ -510,10 +511,10 @@ importers: version: 3.2.4(vitest@3.2.4) vite: specifier: catalog:vite - version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) vitest-canvas-mock: specifier: catalog:vitest version: 1.1.3(vitest@3.2.4) @@ -654,7 +655,7 @@ importers: version: 4.20.6 vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) devDependencies: '@forgerock/javascript-sdk': specifier: 4.9.0 @@ -701,7 +702,7 @@ importers: version: 0.27.0(effect@3.21.0)(vitest@3.2.4) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) tools/user-scripts: dependencies: @@ -716,7 +717,7 @@ importers: version: 3.21.0 vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) devDependencies: '@effect/language-service': specifier: catalog:effect @@ -8424,7 +8425,7 @@ packages: sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 - yaml: ^2.4.2 + yaml: '>=2.8.3' peerDependenciesMeta: '@types/node': optional: true @@ -8634,12 +8635,8 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} engines: {node: '>= 14.6'} hasBin: true @@ -9858,7 +9855,7 @@ snapshots: effect: 3.21.0 ini: 4.1.3 toml: 3.0.0 - yaml: 2.8.1 + yaml: 2.8.4 '@effect/cli@0.75.1(@effect/platform@0.90.10(effect@3.21.0))(@effect/printer-ansi@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(@effect/printer@0.45.0(@effect/typeclass@0.36.0(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: @@ -9868,7 +9865,7 @@ snapshots: effect: 3.21.0 ini: 4.1.3 toml: 3.0.0 - yaml: 2.8.1 + yaml: 2.8.4 '@effect/cluster@0.46.4(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: @@ -9999,7 +9996,7 @@ snapshots: '@effect/vitest@0.27.0(effect@3.21.0)(vitest@3.2.4)': dependencies: effect: 3.21.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) '@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.21.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.21.0))(effect@3.21.0))(effect@3.21.0)': dependencies: @@ -10952,11 +10949,11 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + '@nx/vite@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))(vitest@3.2.4)': dependencies: '@nx/devkit': 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) - '@nx/vitest': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@nx/vitest': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))(vitest@3.2.4) '@phenomnomnominal/tsquery': 6.1.4(typescript@5.8.3) ajv: 8.18.0 enquirer: 2.3.6 @@ -10964,8 +10961,8 @@ snapshots: semver: 7.7.3 tsconfig-paths: 4.2.0 tslib: 2.8.1 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10976,7 +10973,7 @@ snapshots: - typescript - verdaccio - '@nx/vitest@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + '@nx/vitest@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))(vitest@3.2.4)': dependencies: '@nx/devkit': 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) @@ -10984,8 +10981,8 @@ snapshots: semver: 7.7.3 tslib: 2.8.1 optionalDependencies: - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -12146,7 +12143,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) transitivePeerDependencies: - supports-color @@ -12158,32 +12155,32 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.8.3) - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) - '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) - '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) '@vitest/pretty-format@3.2.4': dependencies: @@ -12214,7 +12211,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) '@vitest/utils@3.2.4': dependencies: @@ -13207,7 +13204,7 @@ snapshots: import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 - yaml: 1.10.2 + yaml: 2.8.4 cosmiconfig@9.0.0(typescript@5.8.3): dependencies: @@ -15898,7 +15895,7 @@ snapshots: tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tslib: 2.8.1 - yaml: 2.8.1 + yaml: 2.8.4 yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: @@ -17395,7 +17392,7 @@ snapshots: markdown-it: 14.1.0 minimatch: 9.0.5 typescript: 5.8.3 - yaml: 2.8.1 + yaml: 2.8.4 typescript-eslint@8.46.3(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3): dependencies: @@ -17610,13 +17607,13 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) transitivePeerDependencies: - '@types/node' - jiti @@ -17631,13 +17628,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) transitivePeerDependencies: - '@types/node' - jiti @@ -17652,7 +17649,7 @@ snapshots: - tsx - yaml - vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.4) @@ -17666,9 +17663,9 @@ snapshots: jiti: 2.6.1 terser: 5.46.2 tsx: 4.20.6 - yaml: 2.8.1 + yaml: 2.8.4 - vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): + vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.4) @@ -17682,19 +17679,19 @@ snapshots: jiti: 2.6.1 terser: 5.46.2 tsx: 4.21.0 - yaml: 2.8.1 + yaml: 2.8.4 vitest-canvas-mock@1.1.3(vitest@3.2.4): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) - vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17712,8 +17709,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) + vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.2 @@ -17733,11 +17730,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17755,8 +17752,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) + vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.2 @@ -17776,11 +17773,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17798,8 +17795,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) + vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.2 @@ -17995,9 +17992,7 @@ snapshots: yallist@4.0.0: {} - yaml@1.10.2: {} - - yaml@2.8.1: {} + yaml@2.8.4: {} yargs-parser@21.1.1: {} diff --git a/tools/treeshake-check/eslint.config.mjs b/tools/treeshake-check/eslint.config.mjs index 4dd64666b7..0a23ec0779 100644 --- a/tools/treeshake-check/eslint.config.mjs +++ b/tools/treeshake-check/eslint.config.mjs @@ -20,6 +20,6 @@ export default [ }, }, { - ignores: ['**/out-tsc', '**/__fixtures__/**/dist'], + ignores: ['**/out-tsc'], }, ]; diff --git a/tools/treeshake-check/src/__fixtures__/.gitignore b/tools/treeshake-check/src/__fixtures__/.gitignore deleted file mode 100644 index afe15553cb..0000000000 --- a/tools/treeshake-check/src/__fixtures__/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -!dist/ -!dist/** diff --git a/tools/treeshake-check/src/__fixtures__/bad-syntax/dist/index.js b/tools/treeshake-check/src/__fixtures__/bad-syntax/index.js similarity index 100% rename from tools/treeshake-check/src/__fixtures__/bad-syntax/dist/index.js rename to tools/treeshake-check/src/__fixtures__/bad-syntax/index.js diff --git a/tools/treeshake-check/src/__fixtures__/clean/dist/index.js b/tools/treeshake-check/src/__fixtures__/clean/index.js similarity index 100% rename from tools/treeshake-check/src/__fixtures__/clean/dist/index.js rename to tools/treeshake-check/src/__fixtures__/clean/index.js diff --git a/tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js b/tools/treeshake-check/src/__fixtures__/enum-only/index.js similarity index 63% rename from tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js rename to tools/treeshake-check/src/__fixtures__/enum-only/index.js index dbb2ae9e39..127dd3655a 100644 --- a/tools/treeshake-check/src/__fixtures__/enum-only/dist/index.js +++ b/tools/treeshake-check/src/__fixtures__/enum-only/index.js @@ -1,9 +1,9 @@ // TypeScript enum compiled output — classic IIFE pattern that prevents tree-shaking var Status; (function (Status) { - Status["Active"] = "Active"; - Status["Inactive"] = "Inactive"; - Status["Pending"] = "Pending"; + Status['Active'] = 'Active'; + Status['Inactive'] = 'Inactive'; + Status['Pending'] = 'Pending'; })(Status || (Status = {})); export { Status }; diff --git a/tools/treeshake-check/src/__fixtures__/mixed/dist/index.js b/tools/treeshake-check/src/__fixtures__/mixed/index.js similarity index 67% rename from tools/treeshake-check/src/__fixtures__/mixed/dist/index.js rename to tools/treeshake-check/src/__fixtures__/mixed/index.js index ed3bf516fa..4efbf2ebbe 100644 --- a/tools/treeshake-check/src/__fixtures__/mixed/dist/index.js +++ b/tools/treeshake-check/src/__fixtures__/mixed/index.js @@ -3,12 +3,12 @@ // Enum IIFE var Color; (function (Color) { - Color["Red"] = "Red"; - Color["Blue"] = "Blue"; + Color['Red'] = 'Red'; + Color['Blue'] = 'Blue'; })(Color || (Color = {})); // Prototype mutation -String.prototype.toColor = function() { +String.prototype.toColor = function () { return Color[this]; }; diff --git a/tools/treeshake-check/src/treeshake-check.integration.test.ts b/tools/treeshake-check/src/treeshake-check.integration.test.ts index 134575f9e7..520ecf1029 100644 --- a/tools/treeshake-check/src/treeshake-check.integration.test.ts +++ b/tools/treeshake-check/src/treeshake-check.integration.test.ts @@ -9,7 +9,7 @@ import { analyzeTreeshakeability, BundleFailed } from './lib/treeshake-check.js' const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturePath = (name: string) => resolve(__dirname, '__fixtures__', name, 'dist', 'index.js'); +const fixturePath = (name: string) => resolve(__dirname, '__fixtures__', name, 'index.js'); layer(NodeContext.layer)('analyzeTreeshakeability integration', (it) => { it.scoped('reports clean package as fully tree-shakeable', () =>