GitShow/colinhacks/zshy
colinhacks

zshy

๐Ÿ’ Bundler-free build tool for TypeScript libraries. Powered by tsc.

by colinhacks
Star on GitHubForknpm

TypeScript

1.1k stars21 forks11 contributorsActive ยท 7h agoSince 2025v0.8.0MIT

Meet the team

See all 11 on GitHub โ†’
colinhacks
colinhacks242 contributions
DallasHoff
DallasHoff8 contributions
marcalexiei
marcalexiei4 contributions
jandolezal71
jandolezal714 contributions
noritaka1166
noritaka11663 contributions
djhi
djhi3 contributions
Ehesp
Ehesp2 contributions
43081j
43081j2 contributions

Languages

View on GitHub โ†’
TypeScript99.4%
CSS0.5%
JavaScript0.1%

Commit activity

Last 12 weeks ยท 7 commits

Full graph โ†’

Community health

2 of 6 standards met

Community profile โ†’
42
โœ“READMEโœ“Licenseโ—‹Contributingโ—‹Code of Conductโ—‹Issue Templateโ—‹PR Template

Recent PRs & issues

Active ยท 2 in progress ยท Last activity 7h ago
See all on GitHub โ†’
colinhacks
feat: prototype a TypeScript 7.1 compile engine, gated on the committed fixture outputOpenPR

TypeScript 7 ships under the package name and removes the classic compiler API, so zshy's โ†’ โ†’ architecture has no direct successor. The 7.1 nightly puts back enough to rebuild it: , / , and a virtual filesystem with a callback. This PR adds , a drop-in for built on that API, and a fidelity test that rebuilds every fixture with committed output through it and compares byte-for-byte. Nothing on the default build path changes. still calls the classic engine, changes only by exporting so the two engines share one copy of the seal epilogue, and is not reachable from , so it does not ship in . The new engine is exercised only by , against pinned as a devDependency. Wiring it in behind a flag is a follow-up for when 7.1 is stable. Measured against the committed output All thirteen fixtures with committed build output, 335 files, both passes, compared whole โ€” line included: JavaScript is byte-identical in every fixture. , , โ€” including the epilogue, the CJS interop line, the shim, and every specifier rewrite. Declarations are byte-identical everywhere except four files in , which are two upstream tsgo emit-shape changes: now declares as rather than , and drops the type annotation in favour of an initialiser. Both pairs are type-equivalent, checked with an invariant-position under tsc 5.8 with a deliberately mismatched control that fails (TS2322), so the check is not vacuous. Sourcemap differ in 76 maps, pinned per fixture as the exact set (). They are mostly not zshy's doing: tsc 5.8.3 against tsgo on the same sources with no zshy transforms anywhere already differs in 36 of 42 maps in . Any route onto TypeScript 7 inherits that. The arrays resolve to the same files in every map. How the transforms run without transformers Custom transformers (microsoft/TypeScript#63875 item 3C) are not in the nightly, and measuring the proposal as written shows they would not fit zshy anyway. 3C hands back the post-lowering AST: under , , and counts are all zero by then, so the specifier rewriter and the CJS interop transformer match nothing. Its substrate also collapses statements, duplicates JSDoc and drops comments on whole-file output. So each transform runs over output instead, which is the same shape already has on . The specifier rewrite and the shim work by node position: the emitted file is parsed through a second, VFS-backed , and only a literal that is in a specifier position and that the classic transformer would have rewritten (read off the source AST) is replaced. A relative-looking string anywhere else โ€” a default parameter, an object key, a comment โ€” is left alone, exactly as the classic engine leaves it. Four TypeScript 7 breaking changes this hit These are worth having on the record, since anything that drives the new API will run into them. 1. does not auto-include . With unset, tsc and the classic walk ; the TS 7 API does not. The first run of produced 31 diagnostics against tsc's 3, all / . reproduces the walk and passes the result as when the user has not set it. 2. is removed (TS5108). zshy forces for the CJS pass today. The engine drops and lets the default apply; / are not substitutes because they change emit. 3. is removed, and non-relative targets are rejected ("Non-relative paths are not allowed"). The fixture could not produce a program at all. resolves each target against the old and re-expresses it relative to the tsconfig directory. 4. TS2882 fires on side-effect imports of assets (). Intentional upstream (microsoft/TypeScript#63181). The engine filters it when the specifier is one of zshy's asset extensions, which the classic path copies through anyway. Also: is gone (zshy forces , so no effect), and tsgo re-serialises JSON outputs, so those are copied from source bytes. Found on the way is dead code. never sets or on , so the gate in always takes the null branch. The committed confirms it: is emitted unrewritten, which means published output for users contains unresolvable specifiers today. Not touched here. is instantiated and discarded in . Why not Rolldown Evaluated first and rejected, with the receipts in the fixture sources: oxc's enum emit () breaks declaration merging against tsc's ; namespace lowering rebinds internals to locals, which is runtime-observable; fails on 17 of 116 Zod files ( alone has 160 errors); and Rolldown omits , inlines constants, and chokes on asset imports. None of that is fixable from zshy's side without changing what it emits. CI The gate also pins the failing half of the contract: (TS2322) and a new fixture (TS9007 under ) must report the classic engine's error count and write nothing. Declaration-emit diagnostics are part of whenever is on, so the engine collects and the emit's own diagnostics the same way. The main job runs the fidelity gate against the pinned nightly as part of . A new advisory job () re-runs it against whatever currently is, so upstream emit drift shows up on its own rather than at adoption time. Two things worth knowing about the checks on this PR: is red before this branch. is 7.0.2 now, which has no classic API, so (zshy building itself through the classic engine) fails with . The same job fails on at ccdf114 (run 34154449844). The matrix comment still says covers 6.x; deciding what that leg should test now is separate from this PR. now points at the nightly. The alias ships a bin too, and pnpm settles a bin conflict between two direct dependencies of the same package by version, so the higher one wins the link. Nothing that runs in CI or in the scripts goes through the bin โ€” , and all load programmatically via tsx โ€” but in this checkout is tsgo 7.1 until the alias goes away. The compat job's version print reads instead, so it names the package under test.

colinhacks ยท 1w ago
Novohudonossor
hasMutableExportedBinding skips destructured declarations, so a live exported binding still gets settledOpenIssue

Reading through the work from #73, I think misses destructured declarations, which lets exactly the case it guards against through. : The check sits before both branches, so any binding pattern is dropped early. That misses two shapes: Either way returns false, settling stays enabled, and the accessor gets snapshotted. That produces the divergence the comment right above the function describes: keeps reporting the load-time value while reports the current one. destructuring is correctly irrelevant here โ€” it's filtered by the check above. The gap is only for / binding patterns. Treating a non- declaration with a binding pattern as mutable would close the first shape; collecting the destructured element names into would close the second. I have not built a package to reproduce this end to end โ€” it's a read of the code as merged, and the same lines are on now (). If destructured exports are outside what zshy intends to support, this is not worth your time. To be upfront about where this came from: I found it with a code review tool I am building (ReviewGate) while running it over real repositories, then read the function and checked each shape against it by hand before writing this up. Close it if I have misread the intent.

Novohudonossor ยท 2w ago
yagop
CJS output's `sourceMappingURL` and map `file` field point at the ESM map name (#71)OpenPR

Closes #71 When emitting the CJS (and ) build, in renamed the output path (โ†’, โ†’, and the matching files) but wrote the contents unchanged, so: / kept / the companion / kept / Every CJS consumer then resolved to the ESM map (different ) and misreported positions. This rewrites those in-file references whenever the extension is renamed. [x] T1 Rewrite comment and map field in hook [x] T2 Add regression test: CJS/dts outputs reference their own / [x] T3 Regenerate committed fixtures with corrected map references [x] T4 Anchor rewrite to end-of-file so a sourceMappingURL-like string in user code is left alone (review)

yagop ยท 1mo ago

Recent fixes

View closed PRs โ†’
colinhacks
ci: stage npm publishes instead of publishing directlyMergedPR

CI can no longer publish zshy. The trusted publisher is stage-only, so the publish script runs and a maintainer approves each version with 2FA. The latest workflow is only now. A dispatch needs a GitHub session or token with write access, which a stolen push credential does not give, so bumping on main no longer publishes. The version still comes from that file; a first step fails the run on any ref but . Staging skips a version already live and tolerates one already staged. The restore to 0.0.0 is unchanged. The latest workflow installs npm >= 11.15, drops the deleted NPM_TOKEN, waits for approval before the release steps, and skips release creation when the tag already has one. The canary workflow no longer touches npm. Nobody is there to approve a canary on every push to main. Not exercised end to end โ€” npm is locked.

colinhacks ยท 7h ago
colinhacks
feat: add sealCjsExports, settling the CommonJS re-export accessors into data propertiesMergedPR

TypeScript emits every re-export as a accessor, so a consumer that calls an API off the namespace โ€” โ€” reads the function through a getter on every call. The engine never sees a constant callee, and the call is not inlined. Measured on zod, which zshy builds. 252 of the 255 exports on its are accessors: Writing once and calling that recovers all of it under , which is what points at the namespace read rather than at the function behind it. What the option does copies a source descriptor instead of wrapping it, whenever that descriptor is a non-writable, non-configurable data property: So a module that seals its own exports on the way out settles the re-exports of everything above it in the same build. handles the data properties in one step, and the loop beside it handles the configurable accessors that leaves behind. The reach stops at the build boundary. from a module zshy did not seal โ€” a dependency, a hand-written โ€” defines its accessors non-configurable, and nothing can redefine them afterwards. Those stay getters. On zod, where every module in the chain is built here, that takes every entrypoint from all-accessors to all-data-properties, and to 110M ops/s โ€” the ESM build's number. Why not reassign Assigning a flat object settles the same exports in one place and was the first attempt. It blinds , so Node can no longer offer named exports when an ESM caller resolves to the CommonJS build, and attw reports a missing . The fixture pins it: an ESM subprocess imports named bindings out of the built . Where sealing backs off The emitted CommonJS is parsed before the epilogue is spliced in, because sealing is not safe or useful everywhere. A module that assigns to from inside a function keeps its namespace unfrozen. TypeScript compiles plus to , and freezing would make that throw at the first mutation with nothing to catch it at build time. A module that rebinds gets no epilogue at all. That is what the cjs interop transform does for a lone default export, and it means callers never touch the object the epilogue would have sealed. A mutable exported binding anywhere in the package turns off accessor settling for the whole build. TypeScript compiles a named re-export to an unconditional getter onto the source binding, and nothing in the emitted output tells apart one forwarding to a live from one forwarding to a constant. Settling it pinned at while from the same package reported . Such a package now keeps its getters and takes only the freeze, so both module systems agree. Sealing keys off the output extension, not the build pass. A source is emitted as by both passes and the ESM pass writes last, so it would otherwise ship unsealed and silently unseal everything re-exported through it. output is real ESM and stays excluded. Off by default Sealing is observable. A sealed export becomes non-writable and non-configurable, so stubbing one at runtime stops working. Loading also costs a few milliseconds more โ€” about 3ms on zod โ€” and the CommonJS output grows by the epilogue, 627 bytes per module. The ESM build is untouched. Turning it on by default once it has some mileage seems reasonable, but that is a separate call.

colinhacks ยท 2w ago
Structured data for AI agents

Repository: colinhacks/zshy. Description: ๐Ÿ’ Bundler-free build tool for TypeScript libraries. Powered by tsc. Stars: 1119, Forks: 21. Primary language: TypeScript. Languages: TypeScript (99.4%), CSS (0.5%), JavaScript (0.1%). License: MIT. Latest release: v0.8.0 (2w ago). Open PRs: 2, open issues: 6. Last activity: 7h ago. Community health: 42%. Top contributors: colinhacks, DallasHoff, marcalexiei, jandolezal71, noritaka1166, djhi, Ehesp, 43081j, KirillTregubov, pullfrog[bot].

ยท@ofershap

Replace github.com with gitshow.dev