From 8644157a3f1e1475c661a86fba0c5dbaf1e90e1f Mon Sep 17 00:00:00 2001 From: Gerard Date: Fri, 29 May 2026 12:20:47 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(lint:pkg):=20make=20Gate=206=20ANSI-inv?= =?UTF-8?q?ariant=20so=20publint=20blocks=20fire=20in=20CI=20=E2=80=94=20q?= =?UTF-8?q?ueue=20#63?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lint:pkg wrapper captures publint stdout and matches PUBLINT_BLOCK_RE (/^(Suggestions|Warnings|Errors):$/m) to fail the gate on any advisory. In CI, publint colorizes its block headers (color-capable environment), emitting e.g. "\x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m". The leading SGR codes mean the line is not a bare "Suggestions:", so the regex never matched and the gate silently no-op'd — a CI false-NEGATIVE in which a genuine Warning/Error block would sail through undetected. Verified against raw CI logs: the gate had been a no-op in CI since publint 0.3.21 landed 2026-05-11; locally (plain-text, non-TTY) the regex matched and the gate fired correctly. Fix (belt-and-suspenders): spawn publint with NO_COLOR=1 AND strip residual ANSI SGR codes from captured stdout before the regex match, so the verdict is identical in every color environment (plain, TTY, FORCE_COLOR=1). ANSI_RE is built via String.fromCharCode(27) + RegExp() to avoid a control-char literal that oxlint's no-control-regex (Correctness) would reject. Header comment updated to record the verified CI-color root cause. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/lint-pkg.mjs | 45 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/scripts/lint-pkg.mjs b/scripts/lint-pkg.mjs index 1c1339f..b7828d0 100644 --- a/scripts/lint-pkg.mjs +++ b/scripts/lint-pkg.mjs @@ -4,13 +4,26 @@ // 1. publint + attw — treats publint suggestions/warnings/errors as fatal. // publint 0.3.18 CLI does not expose a flag to fail on suggestions // (--strict only promotes warnings → errors). This wrapper fills that gap: -// it runs publint per workspace, captures stdout, and fails the gate if any -// package emits a "Suggestions:", "Warnings:", or "Errors:" block. -// attw --pack runs after publint per package and preserves its own exit code. +// it runs publint per workspace, captures stdout, strips ANSI SGR codes, +// and fails the gate if any package emits a "Suggestions:", "Warnings:", or +// "Errors:" block. attw --pack runs after publint per package and preserves +// its own exit code. // Motivated by enforcement queue #33 and the PR #35 regression: publint // suggestions about the "git+" URL prefix silently re-drifted across 10 // packages because the gate tolerated them. // +// ANSI invariance (enforcement queue #63): publint colors its block headers +// when it detects a color-capable environment (TTY or FORCE_COLOR). In CI, +// publint emitted ANSI-wrapped headers (e.g. "\x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m"), +// so PUBLINT_BLOCK_RE — anchored on a bare "Suggestions:" line — never matched +// and the gate silently no-op'd (false-NEGATIVE: a real Warning/Error block +// would have sailed through CI undetected). Verified against raw CI logs: +// the gate had been a no-op in CI since publint 0.3.21 landed 2026-05-11, +// while locally (plain-text, non-TTY) the regex matched and the gate fired +// correctly. Fix: spawn publint with NO_COLOR=1 AND strip residual ANSI from +// captured stdout before the regex match — belt-and-suspenders so the verdict +// is identical in every color environment (plain, TTY, FORCE_COLOR=1). +// // 2. engines.node presence — closes enforcement queue #31 (drift-prevention // gate, deployed 2026-05-12). Every workspace package.json AND the root // package.json must declare a non-empty `engines.node` string. Value is NOT @@ -27,6 +40,15 @@ import {join} from 'node:path'; const PACKAGES_DIR = 'packages'; const ROOT_MANIFEST = 'package.json'; const PUBLINT_BLOCK_RE = /^(Suggestions|Warnings|Errors):$/m; +// SGR / ANSI escape sequences (CSI ... final-byte). publint wraps its block +// headers in these when color is enabled (CI default, FORCE_COLOR), which +// otherwise defeats PUBLINT_BLOCK_RE's bare-line anchors. See header note, +// enforcement queue #63. +const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[a-zA-Z]`, 'g'); + +function stripAnsi(text) { + return text.replace(ANSI_RE, ''); +} function listPackageDirs() { return readdirSync(PACKAGES_DIR) @@ -61,8 +83,14 @@ function checkEnginesNode(manifestPath, label) { return null; } -function runCaptured(cmd, args, cwd) { - const result = spawnSync(cmd, args, {cwd, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', shell: false}); +function runCaptured(cmd, args, cwd, extraEnv) { + const result = spawnSync(cmd, args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + encoding: 'utf8', + shell: false, + env: extraEnv ? {...process.env, ...extraEnv} : process.env, + }); const stdout = result.stdout ?? ''; const stderr = result.stderr ?? ''; process.stdout.write(stdout); @@ -95,8 +123,11 @@ function main() { process.stderr.write(` ${enginesFailure}\n`); } - const publint = runCaptured('npx', ['publint', 'run'], dir); - const publintBlock = PUBLINT_BLOCK_RE.exec(publint.stdout); + // NO_COLOR=1 keeps publint's output plain regardless of runner color + // settings; stripAnsi defends against any residual SGR codes so the + // PUBLINT_BLOCK_RE verdict is identical in every environment (queue #63). + const publint = runCaptured('npx', ['publint', 'run'], dir, {NO_COLOR: '1'}); + const publintBlock = PUBLINT_BLOCK_RE.exec(stripAnsi(publint.stdout)); if (publint.status !== 0) { failures.push(`${name}: publint exited ${publint.status}`); } else if (publintBlock) { From b5de5eaa6950446cac302942d8b8ea57fcc7b507 Mon Sep 17 00:00:00 2001 From: Gerard Date: Fri, 29 May 2026 12:20:58 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(pkg):=20declare=20sideEffects:false?= =?UTF-8?q?=20across=20all=2011=20packages=20=E2=80=94=20queue=20#70?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds "sideEffects": false immediately after "type": "module" in every packages/*/package.json. Clears the publint 0.3.21 Suggestion ("package appears to be consumed by bundlers but does not specify sideEffects") that the now-honest Gate 6 (queue #63 fix) flags as fatal, and lets consumers tree-shake under deep imports. Per-package side-effect audit re-run on current main (post-#87 streamRequest removal from fs-http): every package's src/index.ts is a pure re-export or a file whose top-level statements are imports, type declarations, and const/function factory declarations. console.*/document.*/window.*/ Object.defineProperty and the dialog body-scroll write are all inside function bodies (call-time, not module-evaluation). cached-adapter-store's top-level `new WeakMap()` is pure construction. No bare side-effect imports anywhere. All 11 cleared. CLAUDE.md gains a "No top-level side effects" Conventions bullet locking the convention so a future package author doesn't ship without the flag and re-fire Gate 6. Closes enforcement queue #70. Supersedes PR #88. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 1 + packages/adapter-store/package.json | 1 + packages/cached-adapter-store/package.json | 1 + packages/dialog/package.json | 1 + packages/helpers/package.json | 1 + packages/http/package.json | 1 + packages/loading/package.json | 1 + packages/router/package.json | 1 + packages/storage/package.json | 1 + packages/theme/package.json | 1 + packages/toast/package.json | 1 + packages/translation/package.json | 1 + 12 files changed, 12 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7ce4912..d5fd05d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,7 @@ Consumer territories must apply per-call timeouts at instantiation OR rely on th - **Test environment:** Browser-dependent tests use `// @vitest-environment happy-dom` file-level comments. - **Identical build config:** All packages share the same `tsdown.config.ts` structure. - **No direct axios imports in dependent packages.** Route `AxiosResponse` / `AxiosRequestConfig` / sibling types through `fs-http`'s re-exports (e.g. `Parameters[0]` for response types). Direct `import type {AxiosResponse} from 'axios'` breaks rolldown's `d.cts` emission on dual-bundle packages — caught during `fs-cached-adapter-store` scaffold 2026-05-13. +- **No top-level side effects.** Every published package declares `"sideEffects": false` in its `package.json` so bundlers can tree-shake under deep imports. The factory + barrel pattern ensures this is structurally true — every package's `src/index.ts` is either a pure re-export or a file whose top-level statements are imports, type declarations, and `const`/`function` factory declarations. The manifest entry makes it explicit and bundler-actionable. Closes enforcement queue #70 (publint 0.3.21 Suggestion, fatal-promoted by `scripts/lint-pkg.mjs`). - **Transport-surface discipline.** Every `fs-http` transport method must inherit option-honoring from the `axios.create()` instance. Adding a new transport path that uses native `fetch` (or any non-axios transport) requires a deliberate audit against the full `HttpServiceOptions` matrix — `headers`, `withCredentials`, `withXSRFToken`, `smartCredentials`, `timeout`, plus the per-call `AxiosRequestConfig` override surface. The Library-Config-Honor Surface Audit (Sapper M3 + Surveyor M3, 2026-05-15) is the standing checklist. The pre-1.0 `streamRequest` function violated this rule on four axes (queue #22 streamRequest portion + queue #64 XSRF + Surveyor M3 F-1 headers + F-2 timeout) and was removed in 0.4.0 with zero realized consumer impact. If a future streaming use case emerges, the right design is either axios's `responseType: 'stream'` mode via the standard methods (inherits all options for free) or a deliberate `createStreamHttpService` factory designed against the option-honoring matrix from the start — not a re-add of an axios-bypassing transport. ### Internal Dependency Coordination diff --git a/packages/adapter-store/package.json b/packages/adapter-store/package.json index cfc0b00..080652a 100644 --- a/packages/adapter-store/package.json +++ b/packages/adapter-store/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/cached-adapter-store/package.json b/packages/cached-adapter-store/package.json index 0f57e47..a3e196e 100644 --- a/packages/cached-adapter-store/package.json +++ b/packages/cached-adapter-store/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/dialog/package.json b/packages/dialog/package.json index 89bc59c..6a09e2a 100644 --- a/packages/dialog/package.json +++ b/packages/dialog/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 76b4918..152cf67 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/http/package.json b/packages/http/package.json index 5364a78..d9d66ca 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/loading/package.json b/packages/loading/package.json index 8948601..61d807a 100644 --- a/packages/loading/package.json +++ b/packages/loading/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/router/package.json b/packages/router/package.json index dde314f..654e50a 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/storage/package.json b/packages/storage/package.json index 9655be7..9c28c4b 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/theme/package.json b/packages/theme/package.json index a29ae49..6bb7791 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/toast/package.json b/packages/toast/package.json index b1c2540..e54d379 100644 --- a/packages/toast/package.json +++ b/packages/toast/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts", diff --git a/packages/translation/package.json b/packages/translation/package.json index c84758e..b9e45a4 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -13,6 +13,7 @@ "dist" ], "type": "module", + "sideEffects": false, "main": "./dist/index.cjs", "module": "./dist/index.mjs", "types": "./dist/index.d.mts",