Skip to content

Publishing Packages#

Jac projects publish to PyPI as standard Python wheels -- no pyproject.toml, no setuptools, no build backend. The jac build --as wheel command reads your jac.toml and produces a PEP 427-compliant .whl that pip install consumes directly. Anyone can then pip install your package, whether or not they use Jac.

This page covers the end-to-end flow: declaring metadata, building a wheel, testing it, and uploading it.

Overview#

The publishing pipeline has three steps:

  1. Declare package metadata in the [project] section of jac.toml.
  2. Build a wheel with jac build --as wheel -- it lands in dist/.
  3. Upload the wheel to PyPI with twine (upload is intentionally out of scope for jac).

Jac builds the wheel itself: it generates the METADATA, WHEEL, RECORD, top_level.txt, and entry_points.txt files and packs them into a reproducible ZIP archive. The result is indistinguishable from a wheel produced by hatch or setuptools.

1. Declare package metadata#

All publishing metadata lives in the [project] section of jac.toml. Only name and version are strictly required; everything else improves the PyPI listing.

[project]
name = "mylib"
version = "1.0.0"
description = "A handy Jac library"
license = "MIT"
readme = "README.md"
requires-python = ">=3.12"
keywords = ["jac", "jaseci", "ai"]
classifiers = [
  "Programming Language :: Python :: 3",
  "License :: OSI Approved :: MIT License",
  "Framework :: Jac",
]
authors = [{ name = "Your Name", email = "you@example.com" }]
maintainers = [{ name = "Your Name", email = "you@example.com" }]

[project.urls]
homepage = "https://example.com"
repository = "https://github.com/you/mylib"
issues = "https://github.com/you/mylib/issues"

[dependencies]
jaclang = ">=0.15.1"
requests = ">=2.28.0"

Classifiers appear as Classifier: headers in the wheel's METADATA and control how your package is displayed and filtered on PyPI (license badge, Python version tags, topic categories). Browse the full list at pypi.org/classifiers.

classifiers must be a TOML array

Writing classifiers as a plain string instead of an array is a TOML type error and will produce malformed wheel metadata. Always use [...] syntax as shown above.

Runtime dependencies declared under [dependencies] are written into the wheel's METADATA as Requires-Dist entries, so pip install mylib pulls them in automatically. [dev-dependencies] are not installed by default -- they ship as a dev extra (pip install mylib[dev]). [optional-dependencies.<group>] become wheel extras (pip install mylib[<group>]).

See the Configuration Reference for the full field list.

Migrated from [package]?

Releases before jaclang 0.15 used a separate [package] section for publishing metadata. It has been merged into [project]. If you have an old jac.toml, rename [package][project] and [package.include][project.include]; plain [package] tables are no longer read.

Controlling what ships#

By default jac build --as wheel collects a single directory named after the project (mylib/, with hyphens converted to underscores). Override this with [project.include]:

[project.include]
packages = ["mylib", "mylib_extras"]

[project.include.data]
# Extra non-source files to bundle, per package
mylib = ["templates/**/*", "data/*.json"]

.jac, .py, .pyi, .lark, py.typed, and .jir files are included by default. Build artifacts (.jac/, __pycache__/, dist/, virtualenvs, .git/, *.egg-info/) are always excluded. See [project.include] for the full pattern reference.

Single-file modules

[project.include] packages matches directories. A package that is a single top-level .py/.jac file is not currently collected -- put your code in a directory (a __init__.jac is enough) before bundling.

Console scripts#

Declare CLI commands with [entrypoints]:

[entrypoints.scripts]
# `pip install mylib` adds a `mylib` command on PATH
mylib = "mylib.cli:main"

[entrypoints.scripts] is written as [console_scripts] in the wheel. Other [entrypoints.<group>] tables are written through to the wheel metadata verbatim for consumers that use importlib.metadata.entry_points(); Jac itself no longer loads any entry-point group at startup.

Consumers who install your package into a Jac project (jac install mylib) can run its console-script with jac x mylib under the jac runtime, without it being on their shell PATH.

2. Build the wheel#

jac build --as wheel

This writes dist/<name>-<version>-py3-none-any.whl. Build to a different directory with -o:

jac build --as wheel -o /tmp/wheels

jac build --as wheel ships .jir bytecode files only if they already exist in your source tree -- it does not regenerate them. Shipped bytecode is keyed by Python version and validated against a source hash; on a consumer running a different Python version (or if the bytecode is missing or stale), the runtime transparently recompiles the bundled .jac source on first import -- a mismatch never breaks the package.

Wheels are reproducible: every ZIP entry uses a fixed timestamp, so the same source produces a byte-identical wheel.

3. Test before uploading#

Always install the wheel into a clean environment before publishing:

python -m venv test_env
source test_env/bin/activate
pip install dist/mylib-1.0.0-py3-none-any.whl
python -c "import mylib"   # or exercise your console script
deactivate

4. Upload to PyPI#

jac does not upload -- use twine:

pip install twine

# Upload to TestPyPI first to verify the listing renders correctly
twine upload --repository testpypi dist/*

# Then publish to the real index
twine upload dist/*

In CI, authenticate with an API token: twine upload dist/* -u __token__ -p "$PYPI_TOKEN".

Publishing to npm (npmjs.org)#

Client-side Jac libraries can also be published to npm so JavaScript and TypeScript projects can npm install them -- whether or not they use Jac. jac build --as npm reads the same jac.toml and produces an npm-compatible .tgz: it compiles your client modules to JavaScript (ES modules), generates package.json, and emits .d.ts TypeScript declarations.

jac build --as npm             # -> dist/<name>-<version>.tgz
jac build --as wheel           # build the wheel; run both commands for wheel + npm tarball

What goes in the package:

  • Compiled JavaScript. Every .cl.jac (and plain .jac) client module under [project.include] compiles to a sibling .js, preserving the import structure. def:pub / glob:pub symbols become ESM exports.
  • package.json. Built from [project] (name, version, description, license, keywords, repository) plus [dependencies.npm]. Override npm-specific fields under [npm]:

    [npm]
    name = "@yourscope/mylib"   # scoped npm name (defaults to the normalized project name)
    entry = "mylib/index.cl.jac" # entry module (defaults to an index.* module)
    
  • TypeScript declarations. A .d.ts is generated for each module and package.json types/exports point at the entry's declarations, so TypeScript consumers get full type-checking. Function signatures, obj/node classes, and globals are all typed; JSDoc is also embedded in the .js.

Pure client code only

npm packages must be standalone client code. A module that crosses a server boundary (a sv import/call) can't run as a plain npm install, so jac build --as npm rejects it with a clear error. Keep server-coupled code in your app, not in the published library.

The runtime dependency#

Libraries that use JSX or the reactive API (createSignal, createEffect, …) reference the Jac client runtime. The build wires an import { … } from "@jaseci/runtime" into those modules and adds @jaseci/runtime to dependencies automatically. The runtime is a normal, React-independent npm package; modules that explicitly import from react instead get react/react-dom added to peerDependencies.

The @jaseci/runtime package itself is produced by an internal maintenance build (run when cutting a jaclang release), not by a user-facing jac build projection -- it emits @jaseci/runtime at the current jaclang version.

Upload to npm#

jac does not upload -- use the npm CLI:

npm pack dist/<name>-<version>.tgz   # optional: inspect the contents
npm publish dist/<name>-<version>.tgz --access public

In CI, authenticate with an automation token via NODE_AUTH_TOKEN.

Editable installs#

While developing a library locally, install it in editable mode so changes are picked up without rebuilding:

jac install -e .

This installs the project's runtime dependencies and writes a complete .dist-info/ directory into site-packages, so pip show mylib and pip list report it correctly -- all without a pyproject.toml. You can also editable-install a cloned dependency from anywhere:

jac install -e /path/to/cloned/lib

See Also#