Skip to content

chore: declare sideEffects false on shipped packages - #482

Closed
laazyj wants to merge 1 commit into
mainfrom
chore/side-effects-false
Closed

laazyj wants to merge 1 commit into
mainfrom
chore/side-effects-false

Conversation

@laazyj

@laazyj laazyj commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Closes the last outstanding publint suggestion. Follow-up: #481.

Why

publint suggests "sideEffects" wherever a package ships an ESM build. The field tells a bundler that importing a module and not using it cannot change program behaviour, which licenses it to drop unreferenced modules — tree-shaking.

Scope

Applied to the 23 packages that ship a dist. examples and module-compat are exempt: neither declares an exports map, and neither is resolvable as a dependency, so the field would be meaningless there.

check:exports now reports zero suggestions across all 22 gated projects (verified with --skip-nx-cache).

The flag is a promise, so it was verified before being made

Declaring "sideEffects": false on a package that does have module-load effects is worse than not declaring it — the bundler drops the module and its effect, and the bug appears only in bundled builds.

Every top-level statement in packages/*/src/**/*.ts was parsed with the TypeScript compiler API and classified. core, logs, custom-resources and cdk-testing are entirely clean. Of 74 flags elsewhere, all but one resolved to inert data once the callee was read:

Flagged Verdict
Duration.minutes/hours/days(...) static minutes(a){return new Duration(a, …)} — pure
new ServicePrincipal(...) constructor assigns this.service / this.opts only
new RegExp(...), template literals, defaults objects inert
stringConstraint({...}) pure factory; builds and returns, registers nothing
Symbol.for(...) brands writes the global registry, but idempotently and unobservably

The one real violation

packages/ses/src/activation.ts built its provider Lambda's runtime at module scope:

const NODE_RUNTIME = new Runtime("nodejs24.x", RuntimeFamily.NODEJS, { supportsInlineCode: true });

CDK's Runtime constructor ends with Runtime.ALL.push(this) — so merely importing @composurecdk/ses mutated CDK's shared static. It is now built inside activateReceiptRuleSet, the one function that needs it. The magic-string form is retained deliberately (using Runtime.NODEJS_24_X would pull the package's aws-cdk-lib floor up to whatever release added the enum member); the comment moved with it.

Per-call construction is what CDK itself does in determineLatestNodeRuntime, and it is safe here: Runtime.ALL has exactly one consumer in aws-cdk-lib, LayerVersion, which compares it by identity (props.compatibleRuntimes === Runtime.ALL ? undefined : …), so extra entries can never reach synthesised output.

Detail worth knowing

Bundlers resolve sideEffects from the nearest enclosing package.json. For dist/esm/index.js that is dist/esm/package.json — the dialect marker tshy writes — not the package root. Confirmed tshy propagates the field into both markers, so this is effective rather than inert:

$ cat packages/ses/dist/esm/package.json
{ "type": "module", "sideEffects": false }

Documentation

One bullet in AGENTS.md under "Publishing & module format", alongside the sibling packaging rules (no import.meta, no instanceof, ADR-0018 prop typing). No ADR — this applies an existing decision rather than making a new one, and ADR-0007's enforcement table deliberately gains no row until a mechanism actually exists behind it.

Honest sizing

aws-cdk-lib declares no sideEffects and is CommonJS, and it dominates any consumer's bundle — so this does not meaningfully change what a CDK app can tree-shake. It is correct, free metadata that silences a linter suggestion. That assessment is why #481 proposes a six-line assertion in the existing module-compat suite rather than a custom ESLint rule: a syntactic rule would flag ~86 sites of which ~85 are inert, and would not have caught this one anyway.

Verification

npm run verify passes end to end.

🤖 Generated with Claude Code

publint suggests it wherever a package ships an ESM build, and it lets a
bundler drop modules a consumer never references. Applied to the 23
packages that ship a dist; examples and module-compat are exempt, as
neither declares an exports map nor is resolvable as a dependency.

The flag is a promise — importing a module and not using it cannot
change behaviour — so it was verified before being made. A TypeScript
AST scan of every top-level statement in packages/*/src found one
violation: SES built its provider Lambda's Runtime at module scope, and
CDK's Runtime constructor appends to the shared Runtime.ALL. It is now
built inside the function that needs it.

Document the invariant in AGENTS.md alongside the sibling packaging
rules. Nothing enforces it yet (#481).
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

Overall line coverage: 99.39% across 25 package(s).

Package Statements Branches Functions Lines
acm 🟢 97.36% 🟢 94.73% 🟢 100.00% 🟢 97.22%
apigateway 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
budgets 🟢 99.20% 🟢 95.55% 🟢 100.00% 🟢 100.00%
cdk-testing 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
cloudformation 🟢 98.57% 🟢 95.20% 🟢 100.00% 🟢 99.46%
cloudfront 🟢 99.06% 🟢 95.45% 🟢 100.00% 🟢 100.00%
cloudwatch 🟢 95.56% 🟡 89.55% 🟢 100.00% 🟢 98.23%
core 🟢 100.00% 🟢 97.43% 🟢 100.00% 🟢 100.00%
custom-resources 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
dynamodb 🟢 100.00% 🟢 93.10% 🟢 100.00% 🟢 100.00%
ec2 🟢 98.82% 🟢 97.72% 🟢 100.00% 🟢 99.58%
eslint-plugin 🟢 91.63% 🟡 83.11% 🟢 100.00% 🟢 98.22%
events 🟢 98.66% 🟢 94.59% 🟢 100.00% 🟢 100.00%
examples 🟢 97.76% 🟡 84.21% 🟢 100.00% 🟢 99.41%
iam 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
kms 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
lambda 🟢 98.04% 🟢 96.92% 🟢 100.00% 🟢 98.45%
logs 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
module-compat 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
neptune 🟢 97.14% 🟡 86.95% 🟢 100.00% 🟢 98.50%
route53 🟢 97.75% 🟢 95.68% 🟢 100.00% 🟢 99.18%
s3 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
ses 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
sns 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
sqs 🟢 100.00% 🟢 98.63% 🟢 100.00% 🟢 100.00%
Total 🟢 98.08% 🟢 94.39% 🟢 100.00% 🟢 99.39%

@laazyj

laazyj commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Not sure if this is worth it. We know this project is never bundled since it is a dev dependency only. The lazy loaded change in SES is more complex than the eager loaded module singleton it replaced

@laazyj laazyj added the blocked Blocked on another issue/PR before it can proceed label Sep 18, 2026
@laazyj

laazyj commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

Closing unmerged. The change is correct but not worth its maintenance cost.

@composurecdk/* is never bundled. A CDK app is executed by node at synth time, not bundled for a browser or a Lambda. sideEffects is read only by bundlers, so nothing in the real consumer path ever reads this field.

And upstream settles it anyway. aws-cdk-lib declares no sideEffects and is CommonJS. Even in the exotic case where someone did bundle a CDK app, the untreeshakeable bulk of aws-cdk-lib sits in the graph regardless — shaking a few KB of @composurecdk/* around it is noise.

Silencing a publint suggestion is not a reason on its own. That heuristic is aimed at libraries that ship to bundlers, which this one does not. Suppressing advice that does not apply is the same reflex that produced #465.

The cost is not zero, which is what tips it: an invariant spanning 23 packages with nothing enforcing it, a rule in AGENTS.md every contributor has to honour, and #481 proposing further work to protect a benefit of zero.

What survives

The audit was still worth running — it found one real defect, now split out as #483: @composurecdk/ses built a Runtime at module scope, and CDK's Runtime constructor appends to the shared Runtime.ALL. That is worth fixing whether or not a bundler ever sees it, and worse under the dual-package hazard where both copies append.

If this comes back

If @composurecdk/* ever ships runtime code — SECURITY.md anticipates CloudFront viewer functions and Lambda helpers — that would be bundled by esbuild in a NodejsFunction. "sideEffects" would then belong on whichever package ships it, not pre-emptively on all 23.

@laazyj laazyj closed this Sep 19, 2026
@laazyj
laazyj deleted the chore/side-effects-false branch September 19, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Blocked on another issue/PR before it can proceed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant