perf(devframe): micro-dependency sweep - #233
Merged
Merged
Conversation
Storage's only JSON parsing need is reading back its own previously written state (node/storage.ts), where destr's lenient (non-strict) handling of bare keywords/quoted strings never applies - the file is always a JSON object literal. Inline a ~15-line JSON.parse + reviver that mirrors destr's core __proto__/constructor.prototype prototype-pollution guard, and drop the ~36 KB destr runtime dependency. @devframes/hub also depends on destr (client/remote.ts) - left alone in this PR per the plan's decided scope.
All three call sites (adapters/dev.ts, recipes/common-rpc-functions.ts, plugins/assets's reveal-in-folder) pass a bare string target and never use open()'s options - the npm open package is inlined via tsdown onlyBundle with its full is-wsl/wsl-utils/run-applescript/ powershell-utils/default-browser* dependency tree, producing a ~19 KB dist chunk for functionality a ~30-line spawn helper covers: darwin 'open', win32 'cmd /c start', linux 'xdg-open', with WSL detection (/proc/version) preferring wslview and falling back to invoking cmd.exe directly. Drops the unused wait option along with the open dependency (and its now-unreferenced transitive shims from tsdown's onlyBundle list) - deliberate, narrow surface break; the utils/open subpath export is unchanged. Verified the new implementation fails gracefully (ENOENT, caught by the existing call-site try/catches) on a plain Linux container with none of open, xdg-open, or wslview installed.
The only client-side use of ua-parser-modern was formatting navigator.userAgent into a short device label before sending it in the anonymous:devframe:auth(:exchange) handshake - the parsed shape never crossed the wire, only the resulting string did. Send the raw navigator.userAgent instead and parse+format it at the server ingress (node/auth/state.ts, where it's stored), keeping the persisted label format and the ua: string wire shape identical while moving ua-parser-modern out of the ~90 KB client bundle every embedded page loads.
node/storage.ts and node/rpc-shared-state.ts reach devframe/utils/shared-state through the same tsconfig path alias the browser build's own utils/shared-state entry uses - two independent rolldown graphs, so each was inlining its own copy of immer (dist/storage-*.mjs and dist/shared-state-*.mjs, ~38 KB and ~33 KB). Add devframe/utils/shared-state to the node build's deps.neverBundle so it's externalized instead of resolved through the tsconfig path alias; the emitted import resolves at runtime to the already-built dist/utils/shared-state.mjs chunk via Node's package self-reference (the client build runs first in the same tsdown invocation, so the chunk exists on disk by the time the node build needs it). Verified end-to-end: built dist/node/index.mjs's createStorage() round-trips through the self-referencing import correctly, and immer's bytes now appear in exactly one dist chunk.
✅ Deploy Preview for devfra ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Executes plan-039 (devframe core micro-dependency sweep). Four of the five planned items landed; item 5 (dead devDeps) is skipped with the reasoning below.
1.
destr→ local safe-parsepackages/devframe/src/node/storage.tswas the only runtime call site (destr(text, { strict: true })). Storage only ever reads back JSON it wrote itself viaJSON.stringify, so destr's lenient non-strict parsing of bare keywords/quoted strings never applied. Replaced with a ~15-lineJSON.parse+ reviver that mirrors destr's__proto__/constructor.prototypeprototype-pollution guard, and dropped the ~36 KBdestrdependency.@devframes/hubalso depends ondestr(client/remote.ts) — left alone per the plan's decided scope.2.
open→ native spawn helperAll three call sites (
adapters/dev.ts,recipes/common-rpc-functions.ts,plugins/assets'sreveal-in-folder) pass a bare string and never useopen()'s options. Rewroteutils/open.tsas a ~50-line spawn helper:darwin→open,win32→cmd /c start,linux→xdg-open, with WSL detection (/proc/versioncontainsmicrosoft) preferringwslview, falling back tocmd.exedirectly. Dropped the unusedwaitoption (deliberate, narrow API break — thedevframe/utils/opensubpath itself is unchanged) and removedopenplus its now-unreferenced transitive shims (bundle-name,default-browser,default-browser-id,define-lazy-prop,is-docker,is-in-ssh,is-inside-container,is-wsl,powershell-utils,run-applescript,wsl-utils) fromtsdown.config.ts'sonlyBundlelist.Verified the replacement fails gracefully (
ENOENT, caught by the existing call-sitetry/catch) on this Linux container, which has none ofopen,xdg-open, orwslviewinstalled.Measured:
dist/client/index.mjsunaffected (open lived only in the node build); the dedicatedopen-*.mjschunk (~19 KB) is gone entirely.3.
ua-parser-modern→ server-sideThe client's only use was formatting
navigator.userAgentinto a short device label before sending it in theanonymous:devframe:auth(:exchange)handshake — the parsed shape never crossed the wire, only the resulting string did. The client now sends the rawnavigator.userAgent; parsing + formatting moved to the server ingress (node/auth/state.ts, where the label is stored), keeping the persisted label format and theua: stringwire shape identical.Measured:
dist/client/index.mjsdropped from 90,433 → 62,461 bytes (−27.3 KB, matching the plan's −25–30 KB estimate).4. immer deduped between node and browser builds
node/storage.tsandnode/rpc-shared-state.tsreacheddevframe/utils/shared-statethrough the same tsconfig path alias the browser build's ownutils/shared-stateentry uses — two independent rolldown graphs, each inlining its own copy ofimmer(dist/storage-*.mjs~38 KB,dist/shared-state-*.mjs~33 KB).Added
devframe/utils/shared-stateto the node build'sdeps.neverBundleonly (a newnodeDeps, not touching the shareddepsthe browser/dts configs still use) so it's externalized instead of resolved through the path alias. The emittedimport 'devframe/utils/shared-state'resolves at runtime to the already-builtdist/utils/shared-state.mjschunk via Node's package self-reference (the client build runs first in the sametsdowninvocation, so the chunk is on disk by the time the node build needs it).Verified end-to-end: built
dist/node/index.mjs'screateStorage()round-trips through the self-referencing import correctly (mutate → debounced write → file persisted), andgrep-ing the built chunks shows immer'screateDraft/produceWithPatchesin exactly one file post-change (previously two).dist/storage-*.mjsdropped from ~38 KB → ~4 KB.The
rpc-augmentssingle-declaration constraint (config 3, the combined dts build) is untouched — this change only affects config 2's runtime JS output.5. Dead devDeps — SKIPPED, plan evidence was wrong
The plan claimed
valibotandtinyglobbyhave "zero imports inpackages/devframe/src/". That's true only ifpackages/devframe/test/(the package's own top-level test directory, separate fromsrc/**/__tests__/) is excluded from the check — and it shouldn't be:tinyglobbyis imported bypackages/devframe/test/dts-dedupe.test.ts, which is the regression test guarding the exact "rpc-augments single declaration" invarianttsdown.config.ts's config-3 comment (and this plan's own STOP conditions) call out as load-bearing. Removing it breaks that test.valibotis imported by sevensrc/**/__tests__/*.test.tsfiles (flags.test.ts,mcp-server.test.ts,to-json-schema.test.ts,host-agent.test.ts,dump.test.ts,rpc/types.test.ts,rpc/validate-io.test.ts) exercising devframe's Standard Schema interop against a real third-party validator — the doc-comment mentions the plan's evidence noticed are real, but they're not the only usages.knip(run clean, zero findings) already correctly treats both as used via its default test-file entry detection, confirming noknip.jsoncoverride is masking anything. Skipped this item rather than force a removal that breaks tests, per the plan's own "material mismatch → skip and note" guidance.Verification
pnpm lint && pnpm knip && pnpm build && pnpm typecheck— all clean (0 errors; 3 pre-existing unrelated jsdoc warnings inwire-codec.ts).vitest run(root, all projects) — 1132/1132 passing, including thetsnapiAPI snapshot suite (onlydevframe/utils/open's deliberate signature narrowing required--allow-breakingto accept).open()andcreateStorage()against the builtdist/output (see items 2 and 4 above).packages/devframe/distsize: 746,375 → 696,630 bytes (−48.6 KB) even before counting the removednode_modulesinstall weight (destr~36 KB + theopentree's ~300+ KB across its transitive packages).Created with the help of an AI agent (OpenCode).