diff --git a/.nxignore b/.nxignore new file mode 100644 index 0000000000..7a61f2b50d --- /dev/null +++ b/.nxignore @@ -0,0 +1 @@ +.opensource/* 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/nx.json b/nx.json index 58b3b1d998..72d7447d3c 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", @@ -119,7 +125,7 @@ "targetName": "nxLint" }, "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"], - "exclude": ["tools/**/fixtures/**/*"] + "exclude": ["tools/**/fixtures/**/*", "tools/**/__fixtures__/**/*"] }, { "plugin": "@nx/vite/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/package.json b/package.json index cf61fc3292..b56904c235 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" }, @@ -45,7 +46,6 @@ "path": "./node_modules/cz-conventional-changelog" } }, - "dependencies": {}, "devDependencies": { "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.27.9", @@ -53,8 +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", @@ -92,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", @@ -101,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", @@ -130,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 b2528bf664..289c8a5276 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; + pollStatus: (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..05f38634dc 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; + pollStatus: (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/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/pnpm-lock.yaml b/pnpm-lock.yaml index babd5d7de7..73f7ddbec0 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 @@ -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: @@ -80,6 +81,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 @@ -89,6 +93,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))) @@ -115,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)) @@ -224,6 +231,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 @@ -262,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) @@ -355,13 +365,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) @@ -379,17 +389,17 @@ 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) + 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: @@ -435,17 +445,20 @@ 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 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: @@ -456,6 +469,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,15 +503,18 @@ 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) 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) @@ -525,20 +544,35 @@ 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 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 +582,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: @@ -601,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 @@ -611,35 +665,66 @@ 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.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.21.0) + '@effect/platform-node': + specifier: catalog:effect + 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) + acorn: + specifier: ^8.16.0 + version: 8.16.0 + effect: + specifier: catalog:effect + version: 3.21.0 + 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.4) tools/user-scripts: 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) + 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 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: @@ -1488,6 +1573,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: @@ -1594,6 +1687,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: @@ -2843,6 +2975,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] @@ -3034,9 +3175,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==} @@ -3776,11 +3914,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'} @@ -7491,6 +7624,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'} @@ -8223,6 +8359,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: @@ -8288,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 @@ -8498,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 @@ -9722,31 +9855,41 @@ 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: + '@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.4 - '@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) @@ -9756,28 +9899,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 @@ -9785,13 +9928,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 @@ -9810,32 +9946,63 @@ snapshots: '@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/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 - 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: 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.4) - '@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: @@ -10782,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 @@ -10794,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' @@ -10806,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)) @@ -10814,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' @@ -11146,13 +11313,17 @@ 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 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 @@ -11299,8 +11470,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': {} @@ -11974,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.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) transitivePeerDependencies: - supports-color @@ -11986,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: @@ -12042,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: @@ -12290,19 +12459,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: {} @@ -13041,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: @@ -13720,8 +13883,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: {} @@ -15184,7 +15347,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 @@ -15732,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: @@ -16583,6 +16746,8 @@ snapshots: setprototypeof@1.2.0: {} + setup@0.0.3: {} + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -17095,7 +17260,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 @@ -17227,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: @@ -17442,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 @@ -17463,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 @@ -17484,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) @@ -17498,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) @@ -17514,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.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@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 @@ -17544,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 @@ -17565,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 @@ -17587,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 @@ -17608,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 @@ -17630,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 @@ -17827,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/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..edb1a76a88 --- /dev/null +++ b/tools/treeshake-check/package.json @@ -0,0 +1,34 @@ +{ + "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" + }, + "devDependencies": { + "@effect/vitest": "catalog:effect", + "vitest": "catalog:vitest" + }, + "dependencies": { + "@effect/cli": "^0.75.1", + "@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/__fixtures__/bad-syntax/index.js b/tools/treeshake-check/src/__fixtures__/bad-syntax/index.js new file mode 100644 index 0000000000..9d688c49ad --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/bad-syntax/index.js @@ -0,0 +1 @@ +this is not valid javascript !!!### diff --git a/tools/treeshake-check/src/__fixtures__/clean/index.js b/tools/treeshake-check/src/__fixtures__/clean/index.js new file mode 100644 index 0000000000..9735c411c4 --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/clean/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__/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/index.js b/tools/treeshake-check/src/__fixtures__/enum-only/index.js new file mode 100644 index 0000000000..127dd3655a --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/enum-only/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__/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/index.js b/tools/treeshake-check/src/__fixtures__/mixed/index.js new file mode 100644 index 0000000000..4efbf2ebbe --- /dev/null +++ b/tools/treeshake-check/src/__fixtures__/mixed/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/__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/index.ts b/tools/treeshake-check/src/index.ts new file mode 100644 index 0000000000..1b01075c3b --- /dev/null +++ b/tools/treeshake-check/src/index.ts @@ -0,0 +1,260 @@ +#!/usr/bin/env node +// src/index.ts +import { Command, Options } from '@effect/cli'; +import { NodeContext, NodeRuntime } from '@effect/platform-node'; +import { Console, Effect, Option, Schema, pipe } from 'effect'; +import { analyzeTreeshakeability, checkPackage } from './lib/treeshake-check.js'; +import { EXPLANATIONS, primaryCause } from './lib/explanations.js'; +import { 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, +); + +// ─── Rendering helpers ─────────────────────────────────────────────────────── + +const indent = (text: string, prefix = ' ') => + text + .split('\n') + .map((line) => prefix + line) + .join('\n'); + +const SEPARATOR = ' ───────────────────────────────────────────────'; + +// ─── Renderers ─────────────────────────────────────────────────────────────── + +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* () { + if (result._tag === 'FullyTreeshakeable') { + yield* Console.info(tree); + if (result.hints.recommendations.length > 0) { + yield* Console.info('\nRecommendations:'); + for (const rec of result.hints.recommendations) { + yield* Console.info(` • ${rec}`); + } + } + 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( + ` ${moduleCount} ${moduleCount === 1 ? 'file is' : 'files are'} preventing ` + + `tree-shaking. Details below.\n`, + ); + + // ─── 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(`${SEPARATOR}\n`); + + 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( + ` 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( + ` (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(' 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(''); + } + + // ─── Package-level recommendations ───────────────────────────────────── + if (result.hints.recommendations.length > 0) { + yield* Console.info(' Package-level recommendations:'); + for (const rec of result.hints.recommendations) { + yield* Console.info(` • ${rec}`); + } + yield* Console.info(''); + } + }); + +// ─── 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, +); 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..4f4236b427 --- /dev/null +++ b/tools/treeshake-check/src/lib/analysis.test.ts @@ -0,0 +1,287 @@ +// tools/treeshake-check/src/lib/analysis.test.ts +import { describe, it, expect } from '@effect/vitest'; +import { + detectCauses, + buildModuleAnalysis, + analyzePackageJsonHints, + resolveEntry, +} 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(MyClass.prototype, "foo", { value: 1 });'; + 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('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; + (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'); + }); + + // 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 ────────────────────────────────────────────────────── + +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); + }); + + 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); + }); +}); + +// ─── 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 new file mode 100644 index 0000000000..5cdcd82549 --- /dev/null +++ b/tools/treeshake-check/src/lib/analysis.ts @@ -0,0 +1,181 @@ +// src/lib/analysis.ts +import * as acorn from 'acorn'; +import type { ModuleAnalysis, PackageJson, PackageJsonHints, SuspectedCause } from './schemas.js'; + +// 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); + }); +}; + +/** + * 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(); + + // TypeScript enum IIFE: `(function (X) { ... })(X || (X = {}));` + 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'); + } + + // Prototype mutations / Object.defineProperty + if (/Object\.(defineProperty|defineProperties|assign|setPrototypeOf|freeze)\s*\(/.test(code)) { + causes.add('PrototypeMutation'); + } + if (/\.prototype\.[a-zA-Z_$]+\s*=/.test(code)) { + causes.add('PrototypeMutation'); + } + + // Global assignment + if (/^\s*(window|globalThis|self|global)\.[a-zA-Z_$]/m.test(code)) { + causes.add('GlobalAssignment'); + } + + // Bare top-level call without /*#__PURE__*/ — AST-based, regex fallback + if (!causes.has('EnumPattern') && detectTopLevelCall(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/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 new file mode 100644 index 0000000000..5836aa2d40 --- /dev/null +++ b/tools/treeshake-check/src/lib/schemas.ts @@ -0,0 +1,103 @@ +// 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)); + +// 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), + 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', + 'EnumPattern', + '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.test.ts b/tools/treeshake-check/src/lib/treeshake-check.test.ts new file mode 100644 index 0000000000..7380df7c9e --- /dev/null +++ b/tools/treeshake-check/src/lib/treeshake-check.test.ts @@ -0,0 +1,97 @@ +// 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'; + +// 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; + 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('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' }); + const error = yield* Effect.flip(getEntryFromPackageJson(dir)); + 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 new file mode 100644 index 0000000000..9cd440263d --- /dev/null +++ b/tools/treeshake-check/src/lib/treeshake-check.ts @@ -0,0 +1,179 @@ +// src/lib/treeshake-check.ts +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, + resolveEntry, +} 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) => + pipe( + readPackageJson(cwd ?? process.cwd()), + Effect.flatMap(({ pkg, pkgPath }) => { + const entry = resolveEntry(pkg); + return entry !== undefined + ? Effect.succeed({ entry, pkg } as const) + : new MissingEntryPoint({ path: pkgPath }); + }), + ); + +// ─── 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) => + pipe( + getEntryFromPackageJson(cwd), + Effect.flatMap(({ entry, pkg }) => analyzeTreeshakeability(entry, pkg)), + ); 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/src/treeshake-check.integration.test.ts b/tools/treeshake-check/src/treeshake-check.integration.test.ts new file mode 100644 index 0000000000..520ecf1029 --- /dev/null +++ b/tools/treeshake-check/src/treeshake-check.integration.test.ts @@ -0,0 +1,59 @@ +// tools/treeshake-check/src/treeshake-check.integration.test.ts +import { expect, layer } 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, BundleFailed } from './lib/treeshake-check.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +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', () => + 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); + }), + ); + + 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'); + }), + ); + + 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'); + }), + ); + + 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); + }), + ); + + 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); + }), + ); +}); 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..3d25a4387d --- /dev/null +++ b/tools/treeshake-check/vitest.config.mts @@ -0,0 +1,19 @@ +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}'], + setupFiles: ['./src/test-setup.ts'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + }, + }, +})); 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" + } + } + } + ] + } + ] } } 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" } ] }