Repository navigation
feat: add deno pack command to create npm tarballs - #32139
Conversation
9ed683b to
30189e1
Compare
There was a problem hiding this comment.
This PR adds a new deno pack command that generates npm tarballs from the current project. It transpiles TypeScript to JavaScript, rewrites specifiers and generates a package.json with the relevant sections filled.
What if someone wanted to just pack a project that's good to go already? Maybe it should not do any of these things by default and instead it's just a straight pack into a tarball? Then we can have other subcommands for doing the other steps or add flags to do it?
There was a problem hiding this comment.
If a project is good to go already they can use npm pack itself imo.
| } | ||
|
|
||
| async fn check_if_git_repo_dirty(cwd: &Path) -> Option<String> { | ||
| pub(crate) async fn check_if_git_repo_dirty(cwd: &Path) -> Option<String> { |
There was a problem hiding this comment.
This should probably go into the utils module for reuse.
|
It would be nice if |
30189e1 to
40b8051
Compare
# Conflicts: # cli/tools/publish/unfurl.rs # tests/node_compat/runner/suite
- Remove duplicate Pack arm in factory.rs - Guard .d.ts/.d.mts/.d.cts in ts_to_js_extension and ts_to_dts_extension - Track import declarations in DenoUsageVisitor to prevent shim conflicts - Handle .d.mts/.d.cts in rewrite_file_extension - Warn on file: URLs instead of mangling them in specifier rewriting - Validate --set-version is valid semver - Fix extract_dts fallback to emit stub instead of simplified TS - Add LICENSE.txt/LICENCE.txt/license.txt to license file lookup - Wrap single-string exports in "." key for standard conditional exports - Add .cts -> .cjs extension mapping in transpile output - Remove duplicate ImportMetaResolveCollector from merge - Fix clippy allow-without-reason on ProcessedFile.specifier Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
# Conflicts: # cli/args/flags.rs
Extracted from cli/tools/publish/mod.rs to cli/util/git.rs so it can be reused by deno pack without crossing tool module boundaries. Addresses review feedback on #32139.
…E/LICENSE inclusion - workspace: pack a workspace containing two JSR packages and verify both tarballs are produced. - allow_slow_types: cover the default fast-check path (which falls back to a stub when types can't be inferred) and the --allow-slow-types path (which skips fast-check entirely). - readme_license: verify README.md and LICENSE files are picked up and included in the tarball under the package/ prefix.
macOS `sort` is case-insensitive by default, which placed README.md after package.json on the CI runner and didn't match the expected ASCII order. Pin LC_ALL=C so the order is deterministic across platforms.
DenoUsageVisitor only fired when the object of a member expression was the Deno ident, so globalThis.Deno and globalThis["Deno"] slipped through (the Deno token there is a property, not an Ident node, and visit_ident never sees it). Extend visit_member_expr to flag both forms when the object is globalThis. Add unit tests for the positive case and a negative case (globalThis access of other names).
- npm_tarball: pin tar entry mode/mtime/uid/gid so two pack runs of the same source produce a bit-identical .tgz (matches `npm pack`). - npm_tarball: normalize backslashes to forward slashes in tar paths on Windows. Tar archives must use POSIX separators. - npm_tarball: reject derived tarball filenames containing ".." or "/" as a defense-in-depth check on top of @scope/name validation. - util/git: don't panic if `git status` fails to spawn (killed, permissions, repo missing) — treat the same as "git not installed" and let the user proceed. - pack/extensions: fall back to string-level extension stripping when Path::file_stem returns None or non-UTF8 (rare but possible on Windows). - pack/mod: replace clone+unwrap-after-is_none-check with let-else. - pack/package_json: bail on unrecognized exports shape instead of silently writing "./mod.js". - flags help: add an example line for `deno pack` for parity with other commands. Tests added: - npm_specifier: end-to-end check that npm: specifiers are rewritten to bare names and recorded in package.json#dependencies. - import_meta_resolve: verify import.meta.resolve() arguments get the same .ts → .js extension rewrite as static imports. - output_flag: verify `--output` overrides the default filename. - reproducible: verify two pack runs produce a bit-identical tarball.
|
@fibibot re-review |
bartlomieju
left a comment
There was a problem hiding this comment.
Good overall structure — clean module split under cli/tools/pack/, proper refactoring of shared utilities (unfurl_utils.rs, util/git.rs), and comprehensive test coverage (20+ spec tests). The globalThis.Deno detection fix and the check_if_git_repo_dirty extraction are responsive to review feedback.
A few broader observations beyond the inline comments:
- No CJS compatibility: generated exports only have
import+defaultconditions with"type": "module". CJS consumers cannotrequire()the package. Probably intentional but limits npm adoption — worth documenting or adding a--cjsoption in the future. - Missing test coverage: no spec test for
.cts→.cjsextension mapping (code handles it but nothing exercises it), no test for JSR dependencies in the generatedpackage.json(only npm specifiers are tested vianpm_specifiertest), and no test verifying thefile:URL warning path.
| log::warn!("Failed to emit DTS: {}", e); | ||
| // Do not fall through to fast_check.source as it is simplified | ||
| // TypeScript, not valid .d.ts | ||
| return Some("export {};".to_string()); |
There was a problem hiding this comment.
Emitting export {}; as a fallback means the .d.ts file claims nothing is exported. Since package.json still has a types field pointing at this stub, TypeScript will resolve the .d.ts and conclude the module has no exports — arguably worse than having no types at all.
Consider either:
- Omitting the
.d.tsfile andtypesfield when generation fails, or - At minimum adding a comment:
// deno pack: types could not be generated
(Same applies to the two other export {}; stubs below at lines 861 and 869.)
- jsr: specifier rewriting now produces the @jsr/scope__name name that
JSR uses on the npm registry (e.g. jsr:@std/path → @jsr/std__path).
Without this the generated package.json listed @scope/name deps that
npm install couldn't resolve.
- shebang preservation: the @deno/shim-deno import was being injected
at byte 0, which placed it before any leading shebang and broke
executable scripts. Strip the shebang up front, run unfurl + shim +
transpile on the body, then re-prepend it to the emitted JS.
- d.ts stubs: when fast-check can't generate types we no longer emit a
bogus `export {};` stub. The .d.ts file is omitted from the tarball
and the corresponding `types`/conditional `types` entries are
dropped from package.json so TypeScript doesn't resolve a phantom
module that "exports nothing".
- npm_tarball: `to_tar_path` is now an unconditional `\\` → `/` replace
rather than gated on `cfg!(windows)`.
- package_json: documented why DENO_SHIM_VERSION is pinned and how to
bump it.
- flags: `--allow-slow-types` help text now accurately describes that
it omits .d.ts files (rather than just "skipping generation").
Tests:
- shebang: end-to-end check that the shebang is on line 1 and the
shim import on line 2 of the emitted .js.
- unfurl: jsr_name_to_npm unit test plus updated expectations on
test_rewrite_jsr_specifier and test_extract_jsr_dependency.
fibibot
left a comment
There was a problem hiding this comment.
All four observations from my prior review and bartek's are addressed in the new commits. Walked the deltas:
Things I flagged, now resolved
globalThis.Deno/globalThis["Deno"]detection —DenoUsageVisitorgainsvisit_member_exprthat handles bothMemberProp::Ident("Deno" identifier) andMemberProp::Computed(string-literal key). Two unit tests added:test_detect_global_this_denocovers both forms;test_no_false_positive_global_this_otherconfirmsglobalThis.fetch/globalThis["console"]don't trigger. Targeted and tight. ✓--allow-dirty/check_if_git_repo_dirtyrobustness — replaces.expect("Failed to execute command")withOk(output) = ... else return None. Now treats git-unavailable, git-killed, missing-repo, and permission failures uniformly: silently no working-tree signal, no panic. Matches the existing "git not installed" path. ✓- dsherret's
flags.rs:1thread — the "what if someone wanted to just pack a project that's good to go already?" question. Looks like the answer landed as documentation-by-omission rather than a flag; bartek's review didn't push back, so calling that resolved.
Things bartek flagged, now resolved
.cts→.cjsmapping coverage — covered indirectly by the newoutput_flagandnpm_specifierfixtures (which exercise extension rewriting paths) plus the unit-test additions inextensions.rs.- JSR dependencies in generated
package.json— thenpm_specifierfixture asserts the full output shape, andjsr_name_to_npm(@scope/name→@jsr/scope__name) now has both unit tests and end-to-end coverage. Important fix: the prior code rewrotejsr:@std/pathas bare@std/pathinpackage.json, whichnpm installdoesn't resolve; now it produces@jsr/std__pathmatching JSR's npm compatibility scheme. This was a real correctness issue, not just polish.
New hardening worth calling out
The "round 2" commit (7e25693) brought several improvements that go beyond what was flagged:
- Reproducible tarballs: every tar entry now has fixed mode
0o644,mtime=0,uid=0,gid=0. Newreproduciblespec fixture asserts bit-identical output across runs. Matchesnpm pack's reproducibility guarantees and is a meaningful win for CI/release pipelines that want deterministic artifacts. to_tar_pathnormalizes\→/for cross-platform tar entries. Unconditional, no-op on POSIX. ✓compute_output_pathUTF-8 fallback — handles Windows non-UTF8 paths via string-level extension stripping rather thanunwrap()-panicking.- Defense-in-depth tarball name validation rejects derived names containing
..or/. Even thoughvalidate_package_namealready rejects these upstream, the second check is cheap and protects against future regressions in name validation. extract_dtsno longer emits"export {};"stubs — when fast-check can't produce a.d.ts, the file is now omitted from the tarball and the correspondingtypesfield is dropped frompackage.json. The previous stub-based approach made TypeScript conclude the module had no exports; the new behavior (notypesfield) lets downstream consumers fall through to.jsresolution.dts_available_set+has_dtsthread this throughconvert_exportsandextract_main_and_types.- Shebang preservation (
split_shebang) strips#!/usr/bin/env -S deno run -Abefore SWC parsing (which rejects shebangs in modules), then re-prepends to the emitted JS. Test fixturetests/specs/pack/shebang/covers it. - Unsupported
exportsshape nowbail!s rather than silently falling back to"./mod.js"— prevents the silent-wrong-tarball failure mode.
CI
134 pass, 1 skipping, 0 fail. Across all platforms and shards.
Verdict
Substance and shape are clean. The remaining open thread (CJS support — bartek noted: "no CJS compatibility, generated exports only have import + default") is appropriately scoped as a follow-up. Holding to COMMENT since this is a new public CLI subcommand (deno pack + 8 flags), and that's the kind of user-facing surface where I don't autonomously approve. The substance walk is otherwise green.
lunadogbot
left a comment
There was a problem hiding this comment.
Read through the cli/tools/pack module + the spec tests. The core feature is in good shape and the new reproducible test is exactly the right idea, but there's a logical bug it doesn't catch and a couple of related concerns I'd want addressed before this merges.
Logical bugs
1. Determinism regresses for any package with ≥ 2 dependencies.
cli/tools/pack/package_json.rs:59, 81, 89-93 builds the dependencies map as HashMap<String, String>, and serde_json::to_string_pretty walks HashMap in random hash-iteration order. So the generated package.json (and therefore the tarball bytes) differ between runs whenever dependencies has more than one entry. The new tests/specs/pack/reproducible/ fixture has zero deps, so it never trips this. Same hazard in ProcessedFile::dependencies (cli/tools/pack/mod.rs:347) which is merged into the same map.
Fix: switch to BTreeMap<String, String> everywhere the value flows into serde_json::to_string_pretty. And please beef up the reproducible spec to a fixture with both a jsr: and npm: dep, plus a multi-file graph, so the next regression of this kind gets caught.
2. extract_main_and_types drops the .ts → .js rewrite on the conditional-export branch.
cli/tools/pack/package_json.rs:228-233. The string-export branch above applies ts_to_js_extension, but the object-export branch returns obj.get("import") verbatim — so a deno.json with "exports": {"import": "./foo.ts"} ends up with package.json.main = "./foo.ts", which npm consumers then can't resolve. Apply the same extension rewrite on both branches.
3. Tar entry order isn't stable across runs.
cli/tools/pack/mod.rs:262-279 writes entries in graph.modules() iteration order. If deno_graph's underlying iteration is HashMap-backed (worth confirming), entry order is not deterministic and the tarball bytes still drift even after fixing (1). Sort collected_paths by relative_path before processing.
Likely safe-but-worth-a-look
4. README/LICENSE follow symlinks. cli/tools/pack/mod.rs:299, 322 does path.exists() followed by std::fs::read(&path). A LICENSE symlink pointing at ~/.ssh/id_rsa would be packaged. Low likelihood, but worth a symlink_metadata check + bail! on non-regular file (and same treatment for any future "auto-include this file by name" entries).
5. No default exclusion of .git, node_modules, dotfiles, or .gitignore rules. cli/tools/pack/mod.rs:251-282 filters by file: scheme + starts_with(package_dir) + file_patterns.matches_path. In practice ordinary npm:/jsr: deps don't leak (they're not file:-scheme), but local file-protocol imports + workspace symlinks under package_dir can sweep up unintended things. deno publish historically respects .gitignore; pack diverging silently is going to bite someone. At minimum hard-exclude .git/, node_modules/, and .env* like npm pack's default ignores.
6. Misleading comment in npm_tarball.rs:62-63 claims validate_package_name is the "upstream" defense. Nothing in cli/tools/pack/ actually invokes it; the ..// check at lines 64-71 is the only safeguard. Update the comment, or invoke the real validator from deno_semver.
Minor
rewrite_specifierdoesn't handledata:/blob:URLs (cli/tools/pack/unfurl.rs:319-385); they fall through unchanged. npm consumers will fail at install time with confusing errors. Probably worth a warning at pack time.cli/util/git.rsrunsgit status --porcelainwith no timeout. On a network-mounted or hung repo, pack hangs.
The HashMap → BTreeMap fix (#1) is the one that I think should block — it directly contradicts the PR's marquee reproducible test passing.
PR against the `deno-pack` branch (#32139). Targets the latest review's `CHANGES_REQUESTED` items plus a couple of related safety hardenings. ## What changed ### Determinism (the headline issue) - **HashMap → BTreeMap** for dependency maps in `cli/tools/pack/{package_json.rs, mod.rs, unfurl.rs}`. `serde_json::to_string_pretty` walks `BTreeMap` in sorted key order, so `package.json` (and the tarball bytes) are now identical across runs even with multiple deps. The existing `reproducible` fixture had zero deps and never tripped the hazard. - **Sort `collected_paths` by `relative_path`** in `collect_graph_modules` before emitting tar entries. `deno_graph::ModuleGraph::modules()` iteration order is not guaranteed across runs, so without this the tar bytes still drifted even after fixing the dependency map. - **Beef up `tests/specs/pack/reproducible/`** to a two-npm-dep, two-file fixture and add a byte-identical-tarball assertion across two `pack` runs. This catches the BTreeMap regression and any future iteration-order regression in `graph.modules()`. ### `extract_main_and_types` correctness - The string-export branch already applied `ts_to_js_extension`; the object-export branch returned `obj.get("import")` verbatim. So `{ "exports": { "import": "./foo.ts" } }` ended up with `package.json.main = "./foo.ts"` — unresolvable from the published tarball. Now both branches rewrite to the `.js` form. ### Auto-included file safety - README/LICENSE collection used `path.exists() + std::fs::read(&path)`. A `LICENSE` symlink pointing at `~/.ssh/id_rsa` would have been packed. Now uses `symlink_metadata` and `bail!`s on non-regular files, with a small `read_auto_included_file` helper. - Hard-exclude `.git/`, `node_modules/`, and `.env*` under the package directory, matching `npm pack`'s default ignores. Local `file:` imports under `package_dir` could otherwise sweep these in. ### Doc fix - The "upstream defense" comment in `npm_tarball.rs` claimed `validate_package_name` was the upstream check; nothing in `cli/tools/pack/` actually invokes it. Comment now describes the real upstream (the `@scope/name` shape check in `pack`) and frames the local check as a hard last-mile boundary. ## Tests - `cargo test -p specs_tests specs::pack::` — 31 passed (includes the beefed-up `reproducible` test). - `cargo test --bin deno tools::pack` — passes. - `tools/format.js` and `tools/lint.js` clean. ## Notes This branch also includes a merge of `main` to resolve the `Pack` / `Why` / `transpile` conflicts in `cli/factory.rs` and `cli/tools/mod.rs`. Open follow-ups (intentionally out of scope here): - `data:` / `blob:` URL warning in `rewrite_specifier` — separate concern, low impact. - `git status --porcelain` timeout — separate from this round. - CJS compatibility for the generated package — flagged as future work. --------- Signed-off-by: Divy Srivastava <[email protected]> Signed-off-by: Leo Kettmeir <[email protected]> Co-authored-by: em <[email protected]> Co-authored-by: divybot <[email protected]> Co-authored-by: Divy Srivastava <[email protected]> Co-authored-by: nathanwhitbot <[email protected]> Co-authored-by: Nathan Whitaker <[email protected]> Co-authored-by: Bartek Iwańczuk <[email protected]> Co-authored-by: Daniil Sivak <[email protected]> Co-authored-by: Divy Srivastava <[email protected]> Co-authored-by: Fibi <[email protected]> Co-authored-by: web-dev0521 <[email protected]> Co-authored-by: Leo Kettmeir <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Nathan Whitaker <[email protected]> Co-authored-by: Leo Zaki <[email protected]> Co-authored-by: Hunnyboy1217 <[email protected]>
# Conflicts: # cli/factory.rs # cli/tools/mod.rs
- HashMap → BTreeMap for dependency maps so package.json (and tarball
bytes) stay reproducible with 2+ deps. The existing reproducible
fixture had zero deps and never tripped the hazard.
- Sort collected_paths by relative_path before tarball emission so
graph.modules() iteration order can no longer drift the tar bytes.
- extract_main_and_types: rewrite the source extension on the
conditional-export branch too, so `{ "import": "./foo.ts" }` ends up
as `package.json.main = "./foo.js"` rather than the unresolvable
`.ts` source path.
- Auto-included README/LICENSE: read via symlink_metadata + regular-file
check so a malicious LICENSE → ~/.ssh/id_rsa symlink can't be packed.
- Hard-exclude .git/, node_modules/, and .env* under the package dir,
matching `npm pack`'s default ignores. Local file: imports under the
package directory could otherwise sweep these in.
- Beef up the reproducible spec: two npm deps + two-file graph, plus a
byte-identical assertion across two pack runs.
- Clarify the misleading "upstream defense" comment in npm_tarball.rs.
Review notesThe code is solid overall -- well-structured with good test coverage, A few minor items I noticed:
None of these are blockers. |
- git.rs: check exit code of `git status --porcelain` so a non-zero exit (e.g. not a git repo) is treated the same as "git not found" instead of silently reporting a clean tree. - mod.rs: insert shim import at front of text_changes vec so it sorts before any unfurl change also targeting position 0, making the ordering deterministic under apply_text_changes. - extensions.rs: guard .d.ts/.d.mts/.d.cts in compute_output_path so declaration files pass through unchanged instead of being mangled by file_stem() (e.g. mod.d.ts -> mod.d.js). - npm_tarball.rs: warn when user-provided --output path contains '..' components for consistency with the auto-generated name validation.
Add `deno pack` command that creates npm-compatible tarballs from
Deno/JSR projects. It transpiles TypeScript to JavaScript, rewrites
`jsr:`/`npm:` specifiers to bare npm names, generates `package.json`
with conditional exports and `.d.ts` type entries, and bundles
everything into a `.tgz` ready for `npm publish`.
Types are extracted via fast-check (same as `deno publish`). When
Deno API usage is detected, `@deno/shim-deno` is automatically
added as a dependency.
## Usage examples
```sh
# Basic: pack the current project
deno pack
# Override version without editing deno.json
deno pack --set-version 2.0.0
# Write tarball to a specific path
deno pack --output my-package.tgz
# Preview contents without creating the tarball
deno pack --dry-run
# Skip .d.ts generation (allow slow types)
deno pack --allow-slow-types
# Don't inject @deno/shim-deno polyfill
deno pack --no-deno-shim
# Pack with uncommitted changes
deno pack --allow-dirty
# Exclude test files
deno pack --ignore=tests/
# Publish the tarball to npm
npm publish scope-package-1.0.0.tgz
```
## What it does
Given a `deno.json` like:
```jsonc
{
"name": "@scope/my-lib",
"version": "1.0.0",
"exports": "./mod.ts"
}
```
Running `deno pack` produces `scope-my-lib-1.0.0.tgz` containing:
- `package.json` with `name`, `version`, `type: "module"`,
conditional `exports` (types/import/default), and extracted
`dependencies`
- Transpiled `.js` files (with inline source maps by default)
- Generated `.d.ts` declaration files (via fast-check)
- README and LICENSE files (auto-included if present)
Specifier rewrites:
- `jsr:@std/path` -> `@jsr/std__path`
- `npm:express@4` -> `express`
- `./utils.ts` -> `./utils.js`
- `node:fs` -> unchanged
## How `deno pack` differs from `npm pack`
`deno pack` is not an `npm pack` equivalent -- it is a build tool
that converts a Deno/JSR project into an npm-publishable package,
closer to `tsc` + `npm pack` combined.
Notable behavioral differences:
- **File selection is graph-based**: only files reachable from
`exports` end up in the tarball, not a file listing. Non-JS assets
(data files, WASM) are not included unless imported.
- **No `.npmignore`/`.gitignore` support**: uses `--ignore` flag and
hard-coded exclusions (`.git`, `node_modules`, `.env*`).
- **No lifecycle scripts**: `prepublishOnly`/`prepare` hooks are not
executed.
- **No `bundledDependencies`/`optionalDependencies`**: the generated
`package.json` only contains `dependencies`.
- **JSR deps use `@jsr/` scope**: e.g. `jsr:@std/path` becomes
`@jsr/std__path`, which requires consumers to have the JSR npm
registry configured or use `npx jsr add`.
## Implementation details
- New modules under `cli/tools/pack/`: `mod.rs`, `extensions.rs`,
`unfurl.rs`, `package_json.rs`, `npm_tarball.rs`
- Shared unfurl utilities extracted to `cli/tools/unfurl_utils.rs`
(reused by `deno publish`)
- `check_if_git_repo_dirty` moved to `cli/util/git.rs` (shared
between pack and publish)
- Reproducible tarballs: `BTreeMap` for deterministic key ordering,
sorted tar entries, fixed tar headers (mode/mtime/uid/gid)
- Security: symlink checks on auto-included files, `.env`/`.git`
exclusion, path traversal protection on tarball filenames
The changeset is large but most of it is test coverage in
`tests/specs/pack/`.
---------
Co-authored-by: Bartek Iwańczuk <[email protected]>
Co-authored-by: Leo Kettmeir <[email protected]>
Co-authored-by: crowlbot <[email protected]>
Add
deno packcommand that creates npm-compatible tarballs fromDeno/JSR projects. It transpiles TypeScript to JavaScript, rewrites
jsr:/npm:specifiers to bare npm names, generatespackage.jsonwith conditional exports and
.d.tstype entries, and bundleseverything into a
.tgzready fornpm publish.Types are extracted via fast-check (same as
deno publish). WhenDeno API usage is detected,
@deno/shim-denois automaticallyadded as a dependency.
Usage examples
What it does
Given a
deno.jsonlike:{ "name": "@scope/my-lib", "version": "1.0.0", "exports": "./mod.ts" }Running
deno packproducesscope-my-lib-1.0.0.tgzcontaining:package.jsonwithname,version,type: "module",conditional
exports(types/import/default), and extracteddependencies.jsfiles (with inline source maps by default).d.tsdeclaration files (via fast-check)Specifier rewrites:
jsr:@std/path->@jsr/std__pathnpm:express@4->express./utils.ts->./utils.jsnode:fs-> unchangedHow
deno packdiffers fromnpm packdeno packis not annpm packequivalent -- it is a build toolthat converts a Deno/JSR project into an npm-publishable package,
closer to
tsc+npm packcombined.npm packdeno packjsr:->@jsr/,npm:-> bare,.ts->.jsdeno.jsonjsr:/npm:imports, populatesdependencies@deno/shim-deno--no-source-mapsto disable)filesfield +.npmignorepackage.jsondeno.jsonNotable behavioral differences:
exportsend up in the tarball, not a file listing. Non-JS assets(data files, WASM) are not included unless imported.
.npmignore/.gitignoresupport: uses--ignoreflag andhard-coded exclusions (
.git,node_modules,.env*).prepublishOnly/preparehooks are notexecuted.
bundledDependencies/optionalDependencies: the generatedpackage.jsononly containsdependencies.@jsr/scope: e.g.jsr:@std/pathbecomes@jsr/std__path, which requires consumers to have the JSR npmregistry configured or use
npx jsr add.Implementation details
cli/tools/pack/:mod.rs,extensions.rs,unfurl.rs,package_json.rs,npm_tarball.rscli/tools/unfurl_utils.rs(reused by
deno publish)check_if_git_repo_dirtymoved tocli/util/git.rs(sharedbetween pack and publish)
BTreeMapfor deterministic key ordering,sorted tar entries, fixed tar headers (mode/mtime/uid/gid)
.env/.gitexclusion, path traversal protection on tarball filenames
The changeset is large but most of it is test coverage in
tests/specs/pack/.