Skip to content

feat: flat output layout (clean break, no src/ wrapper)#10

Open
brainkim wants to merge 3 commits into
mainfrom
flat-output
Open

feat: flat output layout (clean break, no src/ wrapper)#10
brainkim wants to merge 3 commits into
mainfrom
flat-output

Conversation

@brainkim

@brainkim brainkim commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes #9.

What this does

Modules publish at the tarball root (<pkg>/index.js) instead of nested under src/. jsDelivr/unpkg serve literal paths and never consult the exports map, so the old layout 404'd every copy-pasted CDN file URL. This is the default behavior — no config flag — and a clean break: no compatibility layer ships.

consumer before after
cdn.jsdelivr.net/npm/<pkg>/index.js 404 200
cdn.jsdelivr.net/npm/<pkg>/src/index.js 200 404 after republish (accepted break)
bare specifier / exports map works works (keys never contained src/)
npm link root/dist symmetry works works (dist/ prefix strip unchanged)

The accepted break is small by construction: the src/ layout only ever existed for packages published with libuild 0.1.x (~9 months). The crank-ecosystem URLs that motivated #9 are already flat — this restores them.

Layout

dist/
  index.js  index.cjs  index.d.ts     # modules at the root
  bin/cli.js                          # executables stay in bin/
  _chunks/                            # code-splitting chunks
  impl/…                              # internal modules relocate with the tree
  package.json  README.md  …

Design notes from the issue discussion:

  • Proposal A (flat build via explicit {in, out} entry points), not B — keeping bin/ as a subdir at build time is what per-entry output mapping gives natively.
  • No collision guard needed: with bin/ a subdir and outputs carrying .js/.cjs/.d.ts extensions, no reachable collision with copied metadata files exists. (The src/bin/ entry flavor in the old code is dead — nothing produces source: 'src'.)
  • An earlier revision of this branch shipped re-export stubs under dist/src/ for old URLs; dropped in the second commit per discussion — band-aid off.

Bugs found & fixed along the way

  1. UMD plugin corrupted sibling entries (pre-existing on main): it wrapped every .js in its outdir, so any package with src/umd.ts plus other entries shipped UMD-mangled ESM files. Now wraps only its own output; regression test included.
  2. --save bin kept stale nested paths (caught by test-update agent): bin: "src/cli.js" saved as ./dist/src/cli.js; now flattened to the real executable.
  3. --save idempotency (caught by agents): the ambient .d.ts export whitelist and the multi-entry discovery heuristic both assumed /src/-shaped saved values; both now accept flat forms.

Verification

  • 137 tests, 133 pass — the 4 failures are the pre-existing npm-flag-filtering failures, identical on main.
  • New test/flat-output.test.ts: flat layout + no src/ in output, root-module runtime resolution (CDN-literal simulation), P1: Module augmentation and .d.ts paths broken in published package #1-style guard (internal-module .d.ts + declare module relocate together, relative imports intact), UMD non-corruption, npm pack tarball shape (asserts nothing ships under src/), --save bin regression.
  • E2E outside the suite: packed a fixture tarball, consumed via tsc (nodenext) through the exports map — typechecks; runtime imports work.
  • Dogfooded: libuild builds itself flat (dist/src gone), CLI executes, --save round-trips.

Also in this release (maintenance batch)

Per "never let a breaking change go to waste", commit 26b6723 folds in:

  • esbuild ^0.19^0.28.1 (nine esbuild-breaking minors; zero API fallout), commander 15, patch bumps
  • .tsx entry points (Support .tsx entry points #6): discovered and built; tsconfig jsx settings honored for declarations
  • build --save missing 'import' field in '.' export entry #7 fixed: --save no longer drops import/require conditions from a types-only . export
  • publish flag whitelist restored: the security validation from 47a93bb was silently dropped in the commander rewrite — the 4 chronically failing security tests were right all along. Suite is now fully green: 139 pass, 0 fail.
  • dead-code removal (unreachable src/bin flavor, unused params, deprecated substr)

Breaking-change notes (0.2.0)

  • Literal <pkg>/src/* URLs into previously published packages 404 once those packages republish. Node/bundler consumers unaffected.
  • Root --save paths change shape (./dist/index.js); old saved paths migrate automatically on the next --save.
  • The dist files field is dropped when it only contained layout dirs (npm packs the whole dist dir); author extras keep the field with module patterns appended.

🤖 Generated with Claude Code

brainkim and others added 2 commits July 17, 2026 01:48
Modules now publish at the package root (dist/index.js) instead of
nested under src/ (dist/src/index.js), so direct CDN file URLs like
cdn.jsdelivr.net/npm/<pkg>/index.js resolve. CDNs serve literal paths
and never consult the exports map, so the src/ wrapper 404'd every
copy-pasted file URL.

Layout:
- src/<entry>      -> dist/<entry>.{js,cjs,d.ts}
- bin/<entry>      -> dist/bin/<entry>.js   (unchanged)
- chunks           -> dist/_chunks/         (was dist/src/_chunks/)
- internal modules -> relocated with the tree (relative imports intact)

dist/src/ now holds compatibility stubs - tiny re-export shims (plus a
full UMD copy) - so <pkg>/src/<entry>.js paths from packages published
with older libuild versions keep resolving. Stubs re-export `default`
only when the entry has one, detected via the esbuild metafile. Node
consumers are unaffected either way: exports-map keys never contained
src/.

Also fixes discovered along the way:
- UMD plugin wrapped (and corrupted) every sibling .js in its outdir;
  it now wraps only the UMD output file
- --save root bin could point at a path that exists but is not the
  built executable; paths are normalized to the real artifact
- --save is idempotent again for ambient .d.ts exports and for
  multi-entry discovery with flat saved export values

Fixes #9

Co-Authored-By: Claude Opus 4.8 <[email protected]>
The src/ layout only existed for packages published with libuild since
0.1.x, a small set. The crank-ecosystem CDN URLs that motivated #9 are
already flat - flat output restores them. Carrying a stub layer in
every future tarball to protect a handful of /src/ deep links wasn't
worth it: literal <pkg>/src/* URLs 404 once a package republishes.

Node consumers are unaffected (exports keys never contained src/).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@brainkim brainkim changed the title feat: flat output layout with src/ compatibility stubs feat: flat output layout (clean break, no src/ wrapper) Jul 17, 2026
Never let a breaking change go to waste:

- deps: esbuild ^0.19 -> ^0.28.1 (nine esbuild-breaking minors),
  commander 15, glob/expect patch bumps. Zero API fallout.
- .tsx entry points discovered and built; tsconfig jsx settings
  honored for declarations (fallback: preserve). Fixes #6
- --save no longer drops the import/require conditions from a
  types-only "." export (broke bundler resolution of symlinked
  packages). Fixes #7
- restore npm flag whitelist on `libuild publish`: the security
  validation from 47a93bb was silently dropped in the commander
  rewrite (60d24e7); unknown/unsafe flags are warned and stripped
  again. The 4 "failing" security tests were correct all along -
  the suite is now fully green (139 pass, 0 fail)
- dead code: unreachable src/bin entry flavor, unused params,
  deprecated substr, untyped catches

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: opt-in flat output (drop the src/ wrapper) so direct CDN file URLs stay clean

1 participant