Basilic
Deployment

Publishing Packages

How to publish monorepo packages to npm using the dual-mode export strategy.

Basilic distribution (create-basilic)

The public npm package is create-basilic, built from tools/create-basilic. Do not run scripts/prepare-publish.mjs for it (files: ["dist"] would drop the bundled template).

  1. Release Please opens a release PR from conventional commits (root version + tools/create-basilic/package.json).
  2. Maintainers merge that PR after scaffold acceptance. The tag is vX.Y.Z.
  3. publish-create-basilic.yml checks out that SHA, assembles and packs once, tests the tarball, publishes with npm trusted publishing (id-token only on that job), and attaches the same tarball + SHA-256 to the GitHub Release.
  4. Prerelease versions (0.1.0-next.1) use npm dist-tag next, not latest.

Maintainer setup (one-time)

  • Claim npm create-basilic (or an owned scope) and bind a trusted publisher to this repo, publish-create-basilic.yml, and the release environment.
  • Install a GitHub App for Release Please (RELEASE_PLEASE_APP_ID / RELEASE_PLEASE_APP_PRIVATE_KEY) so follow-on checks run. Default GITHUB_TOKEN does not chain.
  • Protect the release environment. Squash-merge with PR_TITLE + PR_BODY. Authorize who may merge release PRs.

Recovery

Defective release → patch + deprecate; never rewrite tags; never promise to un-generate adopter repos. If npm succeeded and the GitHub asset failed, re-attach the retained artifact. Never overwrite an npm version.

Dual-mode @repo/* packing

Packages use dual-mode exports. Pattern A (@repo/core, @repo/react, @repo/ui): workspace import is src/; prepack builds and rewrites to dist/ for npm. Pattern B (@repo/utils, @repo/error, @repo/email): workspace Node already uses dist/; prepack still rewrites remaining src/ paths (types/source) and packs files: ["dist"]. ESM details: ESM & TypeScript Strategy. This path is not how create-basilic is published. v1 does not publish every @repo/* package.

How It Works

The publishing process uses npm lifecycle hooks:

prepack Hook

Runs before pnpm pack or pnpm publish:

  1. Builds the package (pnpm build runs first via the prepack script)
  2. Stores original package.json exports/main/types in .package-originals.json
  3. Transforms all export paths from src/ to dist/ (subpath exports preserved)
  4. Sets files: ["dist"] to ensure only built files are included

postpack Hook

Runs after packing:

  1. Reads the stored originals from .package-originals.json
  2. Restores the original package.json configuration
  3. Removes the temporary .package-originals.json file

Package Configuration

Packages that use these scripts should have:

Pattern A — development exports (in package.json):

{
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  }
}

Publishing exports (automatically transformed by prepare-publish.mjs):

{
  "exports": {
    ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
    "./async": { "types": "./dist/async/index.d.ts", "import": "./dist/async/index.js" }
  },
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"]
}

Packages with subpath exports have all paths transformed; main and types are only set when a root (.) export exists.

Usage

These scripts are automatically invoked via npm/pnpm lifecycle hooks in package.json:

{
  "scripts": {
    "prepack": "pnpm build && node ../../scripts/prepare-publish.mjs",
    "postpack": "node ../../scripts/restore-publish.mjs"
  }
}

Publishing a Package

  1. Build and test your package locally
  2. Update version in package.json (or use pnpm version)
  3. Publish:
    cd packages/your-package
    pnpm publish

The prepack hook will automatically:

  • Build the package
  • Switch exports to dist/
  • Create the tarball

The postpack hook will automatically:

  • Restore the original package.json
  • Clean up temporary files

Scripts Reference

The publishing scripts (prepare-publish.mjs and restore-publish.mjs) are located in the /scripts directory.

Notes

  • The scripts use process.cwd() to find the package's package.json (they run from within each package directory)
  • If .package-originals.json doesn't exist, restore-publish.mjs exits gracefully (useful for first-time runs)
  • These scripts are only needed for packages that will be published to npm
  • Private workspace-only packages don't need these scripts

On this page