CtrlK
BlogDocsLog inGet started
Tessl Logo

pre-publish

Validate a package before publishing to npm. Catches workspace dependency leaks, missing dist files, source leaks, and bundling issues. TRIGGER when: publishing to npm, modifying package.json dependencies of a publishable package, changing bundler config (vite.config, tsup.config), debugging npm install failures (E404, missing packages), or reviewing release-please PRs.

77

Quality

96%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Pre-Publish Validation Skill

Mandatory checklist before publishing any package to npm. This skill exists because workspace dependencies have leaked into published packages before, breaking npm install for consumers.

Before changing dependencies in a publishable MoltNet package, also load ../rendered-pack-3252bc08/SKILL.md. It records the workspace protocol, private-package bundling, and lockfile rules that apply here.

When to trigger

  • Before running pnpm publish or npm publish on any package
  • Before merging a PR that modifies package.json of a publishable package
  • When adding or moving dependencies in a publishable package
  • When modifying a package's Vite/bundler config
  • Any time release-please creates a release PR

Discover the publish surface

A package is publishable if it has a files field in package.json and is not marked "private": true. Do not maintain a package list in this skill; it goes stale. Discover the current set from apps/*/package.json, libs/*/package.json, and packages/*/package.json, then confirm each project has a check:pack target with pnpm exec nx show project <name> --json.

The native CLI platform packages under packages/cli/npm/* are assembled and validated by the Go CLI release job. Do not mistake them for ordinary TypeScript package projects.

Checklist

1. Provenance repository metadata

Every publishable package must identify this GitHub repository and its exact directory in the monorepo:

"repository": {
  "type": "git",
  "url": "git+https://github.com/getlarge/themoltnet.git",
  "directory": "libs/<package-directory>"
}

Use the actual apps/, libs/, or packages/ path for directory. npm trusted publishing validates repository.url against the GitHub Actions OIDC provenance. Missing or mismatched metadata passes tarball creation but fails the registry PUT with E422 Error verifying sigstore provenance bundle.

check:pack validates the canonical repository URL and matching monorepo directory for every publishable package.

2. Workspace dependency placement

Private workspace packages (@moltnet/*) must NEVER appear in dependencies of a publishable package. They are not published to npm and will cause npm install to fail.

Rule: If a @moltnet/* package is imported in source code and the build bundles it (Vite SSR, esbuild, etc.), it belongs in devDependencies.

Check:

rg -n '@moltnet/' <package>/package.json

Expected: @moltnet/* entries appear only under devDependencies, never under dependencies.

Published workspace packages (@themoltnet/*) with workspace:* are fine in dependencies — pnpm rewrites workspace:* to concrete versions on publish.

3. Run Nx dependency checks as an early warning

Every ordinary publishable Node project with a build target must opt into createNxDependencyChecksConfig in its local eslint.config.mjs.

Keep includeTransitiveDependencies at its default (false) unless the entire transitive graph is intentionally shipped as manifest dependencies. A Vite SSR package that bundles private @moltnet/* libraries will otherwise receive false demands for their implementation dependencies.

Document every ignoredDependencies entry next to the exception. Valid exceptions include:

  • private @moltnet/* build inputs that are bundled into dist;
  • dependencies loaded by a plugin name, dynamic adapter, or other edge Nx cannot discover statically;
  • runtime dependencies promoted from a bundled private package closure.

Nx dependency checks compare the source/project graph with package.json. They do not inspect emitted JS, package.json#files, code-split chunks, or the packed manifest. A passing lint target is never publication proof.

Run through Nx:

pnpm exec nx run <project>:lint

4. Preserve the runtime dependency boundary

For publishable Node packages, use this default:

  • Bundle private @moltnet/* packages and keep them in devDependencies.
  • Externalize every installable runtime dependency. Public workspace @themoltnet/* packages stay in dependencies with workspace:*; third-party packages stay in dependencies or peerDependencies.
  • Permit a bundled public/third-party dependency only for a documented loader or artifact constraint, and protect the exception with a focused bundle test.

Vite SSR normally externalizes registry dependencies, but source-direct workspace exports can still be bundled. For Vite 8/Rolldown, put public workspace dependencies in the active build-level external option. In this repository, use externalizeInstallableDependencies from vite.shared.ts in build.rolldownOptions.external (or build.rollupOptions.external for configs driven by Rollup). It derives externals from the package's dependencies, optionalDependencies, and peerDependencies, excluding private @moltnet/*. Do not duplicate dependency-name arrays. The predicate fails the build when a bundled private package reaches an undeclared third-party import; promote that package to the public manifest. Do not rely on comments and ssr.external: inspect emitted JS.

5. Build produces a valid bundle

For bundled packages (Vite SSR), verify the bundle doesn't contain runtime imports to private workspace packages:

# Build the package and its workspace deps first
pnpm exec nx run <project>:build

# Check the bundle has no @moltnet/ imports
rg '@moltnet/' <package>/dist

Expected: zero matches. All @moltnet/* code should be inlined.

Also verify that runtime imports for public workspace dependencies remain in the emitted JS and that asset filenames such as .wasm, migrations, and native bindings were not detached from the package that owns them.

6. Run check:pack

pnpm exec nx run <project>:check:pack

This validates:

  • dist/index.js exists in the tarball
  • dist/index.d.ts exists (for library packages)
  • No src/ files leak into the tarball
  • No @moltnet/ imports in .d.ts files
  • No @moltnet/ packages in dependencies
  • Canonical npm provenance repository URL and monorepo directory
  • Every relative static or dynamic import in packed JavaScript resolves to a file included in the tarball

This relative-import check is what catches a generated dist/assets/*.js chunk that exists after build but is excluded by package.json#files.

7. Verify the tarball contents

pnpm pack --dry-run --json | jq '.files[].path'

Check that:

  • Only expected files are included (typically dist/ and package.json)
  • No source files, test files, or config files leaked

Use pnpm pack, not npm pack, when validating the publish transformation: pnpm resolves catalog: and workspace:* protocols for the packed manifest.

8. Test install and exercised runtime paths

pnpm pack
test_dir="$(mktemp -d)"
cd "$test_dir"
npm init -y
npm install <tarball-path>
node -e "import('<package-name>')"

Use a clean temporary consumer for packages with assets, native bindings, source exports, or chained public workspace dependencies. Pack and install the whole local dependency set so the smoke tests published dist exports rather than workspace source shortcuts.

Importing a package or running --help only checks eagerly loaded modules. For lazy adapters, optional native dependencies, dynamic imports, and command branches, add a focused smoke that exercises that path. Do not claim the smoke loads the full module graph unless it actually invokes those paths.

Common mistakes and how they happen

Missing repository metadata (the tasks-orchestrator incident)

What happened: @themoltnet/tasks-orchestrator@0.2.0 built, tested, and passed the old check:pack, but all three npm publish attempts failed with E422. The Sigstore provenance bundle identified https://github.com/getlarge/themoltnet, while the packed package.json had no repository.url.

Why it was not caught: The validator checked tarball contents and private workspace dependency leaks, but not metadata consumed by npm trusted publishing.

Prevention: Copy the canonical repository object when making any package publishable. Run check:pack before merge; it now checks both the repository URL and the package directory.

Workspace deps in dependencies (the legreffier incident)

What happened: @moltnet/api-client, @moltnet/crypto-service, and @themoltnet/design-system were listed in dependencies instead of devDependencies in @themoltnet/legreffier. Vite correctly bundled them into dist/index.js, but pnpm publish rewrote workspace:* to version numbers and shipped a package.json that referenced unpublished packages.

Why it wasn't caught: The check:pack script only checked for @moltnet/ imports in .d.ts files and src/ leaks in the tarball. It didn't check the dependencies field itself.

Prevention: The check:pack script now validates that no @moltnet/* packages appear in dependencies. Run it before every publish.

Private task/runtime deps in pi-extension (recurring release failure)

What happened: Release CI for @themoltnet/pi-extension@0.21.0 passed the Vite build, then failed check:pack because @moltnet/tasks appeared in dependencies.

Why this matters: @moltnet/tasks is private. Consumers cannot install it from npm. If pi-extension imports it and Vite bundles it via ssr.noExternal, it belongs in devDependencies, not dependencies.

Prevention:

  • Treat @moltnet/tasks like every other private @moltnet/* package.
  • Keep bundled private workspace deps in devDependencies.
  • Keep published @themoltnet/* packages in dependencies only when runtime consumers need to install them.
  • If the failure says private workspace packages in dependencies, fix package.json placement before changing Vite.

Generated SDK chunk omitted from agent-daemon

What happened: the daemon bundled the source-direct @themoltnet/sdk/node entry. SDK's lazy OS-keyring import became a relative dist/assets/*.js chunk, but agent-daemon's files allowlist shipped only dist/*.js. The local build worked while the npm tarball failed to resolve OAuth2 client secrets.

Why lint and the old smoke missed it: Nx dependency checks were not enabled for agent-daemon and cannot inspect tarball file selection. The bin smoke ran a help path that never invoked OAuth/keyring resolution.

Prevention: externalize installable public dependencies with externalizeInstallableDependencies, validate relative imports against actual tarball entries, and exercise lazy runtime paths when they are release-critical.

SDK pattern (correct)

@themoltnet/sdk does it right:

  • @moltnet/api-client and @moltnet/crypto-service in devDependencies
  • ssr.noExternal: [/@moltnet\//] in vite.config.ts to explicitly bundle
  • Only @noble/* (published) packages in dependencies

Integration with CI

The check:pack target runs in the affected pre-merge CI graph and again in the release workflow before pnpm publish. Pre-merge CI sets MOLTNET_SKIP_REGISTRY_SMOKE=1 because a coordinated release may reference a workspace version that is not on npm yet; it still runs the tarball, declaration and provenance checks. Release jobs run the registry install smokes after their dependency packages publish and remain the last line of defense.

# From .github/workflows/release.yml
- run: pnpm exec nx run <project>:check:pack
- run: pnpm --filter <package> publish --no-git-checks --access public --provenance

Reminders

  • Moving a dependency from dependencies to devDependencies requires updating the lockfile (pnpm install)
  • Treat source-direct public workspace packages as explicit build externals; Vite may otherwise inline them even though registry dependencies stay external
  • The files field in package.json controls what goes in the tarball — keep it minimal
  • Nx dependency checks are source-graph lint, not emitted-artifact validation
  • A clean consumer smoke must exercise lazy paths to validate them
Repository
getlarge/themoltnet
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.