package.vlt
Every package has a package.vlt at its root: the manifest, written in Velt. velt new and
velt init write one.
import type { Package } from "velt/package";
export const pkg: Package = {
name: "hello",
version: "0.1.0",
velt: "0.1", // the velt versions that build it: the newest 0.1.x
dependencies: {
json: "1.2", // registry package, semver requirement
http: { version: "0.3" }, // object form
util: { path: "../util" }, // local package (version optional)
"http-server": { // from a git repository
git: "https://github.com/velt-lang/velt",
tag: "v0.5.0",
path: "packages/http-server",
},
},
paths: { "@app/*": "src/*" }, // import { x } from "@app/util" → src/util.vlt (or .ts, .tsx)
};The file is data only. velt reads it the way it would read JSON, without compiling or
running anything, so installing a package never runs its code. Besides the type import, it holds
one export const pkg: Package = { … } made of strings, numbers, true/false, arrays and
objects. Comments and trailing commas are fine; anything else (null, template literals,
variables, calls, spreads) is an error that points at it:
package.vlt:5:13: error: `null` is not allowed; leave the key outIn an editor, the language server (velt lsp) completes the fields valid where
you type, explains each on hover, and reports exactly the errors velt would, as you type. It
also asks the package's registry: it completes package names and versions in dependencies,
shows a dependency's newest and locked versions on hover, and flags requirements that no
published version matches or that leave out a newer one (with a fix). The
Package type documents the same fields. Leave out the fields you don't
need. A key the manifest doesn't know is an error, with a
suggestion when it is close to one (dependecies → dependencies). velt manifest checks the
file and reports these errors without building anything.
name, version, description, keywords, entry
-
name: lowercase letters and digits, starting with a letter, with single-or_between words (text-kit,text_kit; nottext--kit,text_-kitortextkit-). Names that differ only in-versus_count as the same in a registry.veltandstdare reserved for the standard library (its modules arevelt/…), as a package name and as a dependency's name (the package name `velt` is reserved for the standard library), and so are names whose first word isrt,sigornative(rt,rt-str) because a package's native functions are namedvelt_<name>__…and those prefixes are Velt's own (velt_rt_…is the runtime). -
A name can also be scoped, as on npm:
@scope/name, each part lowercase words as above (@sigx/server-renderer). Registries hold unscoped packages only, so a package with a scoped name is a local (path) dependency of others, and can't havenativecode yet. -
version: a semantic version. -
description: one line about the package, shown byvelt searchand registry listings: at most 300 characters, no line breaks, no surrounding whitespace. -
keywords: search words, such as["json", "parser"]: at most 10, each lowercase letters, digits and-(at most 32 characters, starting with a letter or digit).velt searchmatches every word of its text against names, keywords and descriptions, names first.The registry keeps both per published version, so changing them means publishing a new version.
-
entry: the program's root file, a path inside the package (default"src/main.vlt"). A package withsrc/main.vltis runnable; a package withsrc/lib.vltis a library that other packages import. A package can have both. Without anentry,src/main.tsorsrc/main.tsx(src/lib.ts,src/lib.tsx) work likesrc/main.vlt(src/lib.vlt); two of them at once are an error.
velt
The velt versions that build the package. velt new and velt init write the major.minor of
the velt that created the package (a pre-release writes its whole version, such as
"0.2.0-rc.1"), so installing a newer velt doesn't change how it builds: pre-1.0 minors may
change the language, and a toolchain's standard library and runtime belong to it.
The velt on your PATH, the launcher, runs the version the package's
velt.lock.json records when the field accepts it, so every machine builds a
commit with the same velt; otherwise (no lock yet, or velt update) the newest installed version
the field accepts, installing one first when none is
(velt toolchain). A toolchain's
own velt started directly warns when its version is not one the field accepts:
warning: package.vlt asks for velt "0.2", but this is velt 0.1.0; it may not build the packagevelt | builds with |
|---|---|
"0.1" | the newest installed 0.1.x |
"0.1.3" | 0.1.3 or a newer 0.1.x |
"=0.1.3" | exactly 0.1.3 |
">=0.1, <0.3" | a requirement with an operator means what it means in dependencies |
"Builds with" is the choice when the package has no lock yet and on velt update; after that,
the version the lock records, while the field accepts it.
A version without an operator stays within its minor version, also after 1.0: "1.2" accepts
1.2.5 but not 1.3.0, unlike the same requirement in dependencies. A major version alone
("1") accepts any 1.x. Pre-releases match only
a requirement that names one ("0.2.0-rc.1"). Leave the field out to build with any velt.
velt manifest and every other command check that the value is a requirement.
dependencies
import … from "json" and "json/sub" resolve through dependencies: the dependency's
src/lib.vlt, or src/sub.vlt / src/sub/index.vlt (or .ts, .tsx, tried like a relative
import's files). Standard library imports ("velt/x")
and relative imports ("./x") never do. Names that aren't identifiers are quoted
("my-lib": "1.0", "@sigx/reactivity": { path: "../reactivity" }; a scoped dependency needs
a path or git). { git: "https://…", branch: "main", path: "packages/x" } takes the package
from a git repository (git dependencies): a Velt package, or an
npm package (or a whole workspace of them) compiled from its TypeScript sources. The official packages (sqlite, postgres, redis, http-server) come this way or by
path (official packages). velt add edits this object and keeps your comments. In a member of a
workspace, name: "workspace" depends on another member, or on the
requirement the workspace writes for name. See
Packages. A bare import that no dependency (and no paths alias) claims looks
for an npm package.
paths
Import aliases, like TypeScript's compilerOptions.paths, replace long ../../ chains:
- A bare specifier that matches a pattern resolves to the target, relative to the package root,
before
dependenciesare consulted."velt/x"and"./x"are never aliased. - A pattern has at most one
*, at its end, and so does its target (both or neither). The longest matching pattern wins. Each pattern has one target. - Targets must stay inside the package (no
.., not absolute). - Each package's aliases apply to its own modules only.
jsx
jsx: { importSource: "sigx" },The module whose jsx-runtime compiles the package's JSX: a dependency, "velt/jsx" (the
default), a paths alias, or ./dir relative to the package root. A
/** @jsxImportSource x */ comment at the top of a file wins (JSX); Velt also
reads it from a // comment, tsc only from a block comment.
define
define: { __DEV__: "false", __SIGX_VERSION__: "\"1.0.1\"" },Build-time constants, as Vite's define. Every reference to the name in the build's modules
(the package's, and those of the npm packages it compiles) that nothing in scope declares is
the literal, so if (__DEV__) { … } becomes if (false) { … } and the branch folds away, and
{ __DEV__ } is { __DEV__: false }. A name the code declares keeps its own value, as with
esbuild's define: a parameter, a local, an import, a loop variable, a catch parameter or a
function or class of that name, in its scope. A property (o.__DEV__) and a type are left
alone too. Each value is the
source of a literal, as in Vite: "true", "false", "null", a number ("3"), or a string
in quotes ("\"1.0.1\"" or "'1.0.1'"); other expressions are an error. Only the root
package's define applies.
TypeScript code declares such constants in a declaration file, as sigx's src/env.d.ts does:
declare const __DEV__: boolean;Velt reads these ambient declarations (declare const, let and var, also inside
declare global { … }) from modules and from the .d.ts files of each package it compiles (in
the package's directory and below its src/). As in TypeScript, a declaration is global only
in declare global or in a file without imports and exports; in a module (such as an
index.d.ts with export declare const version: string;) it is the module's own. Velt has no
global variables, so a declared constant that a module refers to (without declaring the name
itself) needs a define; without one, the declaration is an error that says so. A define whose literal is not of the declared type (boolean, string, number)
is an error too.
native
native: { targets: ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin"] },The package includes a Rust crate (a cdylib + staticlib built on the velt_native crate)
whose functions its Velt code declares (Packages with native code).
path: the crate's directory, a directory name in the package root (default"native").targets: the targetsvelt publishpublishes a prebuilt library for (it fails if one is missing):x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,x86_64-apple-darwin,aarch64-apple-darwin,x86_64-pc-windows-msvc,aarch64-pc-windows-msvc.wasm: must befalse(the default); WebAssembly libraries are not supported yet.
build
build: { cpu: "x86-64-v3" },How the package's program is built. Only the root package's applies.
cpu: the CPU the program is built for, asvelt build --cpu:"baseline","x86-64-v2"(the default on x86_64),"x86-64-v3","x86-64-v4","native"or a CPU name such as"znver4"(CPU).--cpuandVELT_CPUtake precedence, and a CPU of another architecture does not apply to a build for this one ("x86-64-v3"leaves arm64 builds at their default,"neoverse-n1"x86_64 builds at x86-64-v2).
tsCompat
tsCompat: ["src/components", "src/models"],Folders whose modules a TypeScript client shares, so they must stay in the
common subset of TypeScript and Velt (like the
client's tsconfig.json include). velt check --ts-compat without paths lints every .vlt,
.ts and .tsx file in them (velt check),
and the editor shows the findings, with their fixes, as you type
(editors). Nothing else changes: velt build and a
plain velt check never lint.
- Each entry is a
/-separated path relative to the package root (src/models; not./src/models,../sharedor/abs, no trailing/). - Each folder once: a folder listed twice, or inside another listed one, is an error. Case
doesn't matter here (
src/modelsandsrc/Models/suboverlap): on macOS and Windows they are the same folder, so such a list would mean something else on Linux. - The files in a folder are found as
velt checkfinds a package's:node_modules/,target/, hidden and symlinked directories are skipped, and so is a package nested in the folder (a directory with its ownpackage.vlt), which lints its owntsCompat. - Not empty: leave the field out instead of writing
[]. - A folder that doesn't exist is a warning in the editor and an error for
velt check --ts-compat.
registry
registry: "https://registry.example.com",A root package's registry URL, or VELT_REGISTRY set to an http(s):// URL (which takes
precedence), replaces the local registry directory for velt add, velt install and
velt publish (Registries).
Other tools
velt manifest --json prints the validated manifest as JSON, with the defaults filled in, for
tools that can't read Velt:
$ velt manifest --json
{
"dependencies": {
"json": "1.2"
},
"entry": "src/main.vlt",
"name": "hello",
"paths": {},
"velt": "0.1",
"version": "0.1.0"
}velt.lock.json
velt install writes velt.lock.json, which pins the exact version and content hash of every
dependency. It is generated JSON, pretty-printed with a stable key order so diffs stay readable:
"version": 1 and a "packages" array with one entry per package (name, version, source:
"registry", "path+<relative path>", "git+<url>?branch=main&path=<dir>#<commit>" (git dependencies) or,
in a workspace, "workspace+<member dir>", checksum, dependencies, and for a package with
native code a "native" object with the checksum of its prebuilt library for every published
target). In a package with a velt field, it also records the velt version that builds the
package, "velt": "0.2.1": the one that created the lock, kept until velt update (or until
the field doesn't accept it). The velt on your PATH runs that version in the package, installing it
if needed, so a laptop and CI build a commit with the same compiler even when newer patch
versions are installed. Commit it for applications. --locked on build, run, test and
install fails instead of changing it, and fails when a different velt runs than the lock
records.