Lakutata Workspaces
A project made of several packages (an application, its web interface, shared libraries, native addons) is a workspace: one repository whose root package.json lists the directories of its packages. The package manager (npm, pnpm or yarn) links the packages to each other; the lakutata command runs their scripts in the order of their dependencies, gives them one version, and adds new packages. It covers the part of lerna that these projects use. Every template but default makes a workspace (ProjectStructure, section 2).
1. A workspace
The root package.json of a project made by lakutata create --name shop --template web-react (its migration and Docker scripts left out):
{
"name": "shop-workspace",
"version": "0.0.1",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "lakutata run rebuild",
"test": "npm run build && lakutata run test",
"dev": "lakutata run dev --parallel",
"start": "npm start -w app",
"version:patch": "lakutata version patch --yes"
},
"devDependencies": {
"@lakutata/cli": "^3.0.0",
"@types/node": "^22.0.0",
"typescript": "^6.0.3"
}
}- The packages are the directories that the patterns of
workspacesmatch and that hold apackage.jsonwith aname:*matches within one directory name,**any number of directories, and!packages/oldleaves a directory out.node_modulesand the directories starting with a dot are never searched. A pnpm workspace lists its patterns inpnpm-workspace.yamlinstead, as a list underpackages:(- 'packages/*'lines, or['packages/*']). - The commands work from any directory of the workspace: they use the nearest parent directory that declares a workspace.
- The dependencies between packages are the ones in
dependencies,devDependencies,peerDependenciesoroptionalDependencies, whatever their range (^0.0.1,*,workspace:*,file:). - The build tools (
@lakutata/cli,typescript,@types/node) are development dependencies of the root, so every package can use them in its scripts.
lakutata list shows the packages, their versions, their directories and the packages each one depends on. Here, app (the server) depends on ui (the web interface), whose built pages it serves:
2 packages, version 0.0.1
app 0.0.1 (private) packages/app → ui
ui 0.0.1 (private) packages/ui2. Running scripts
lakutata run <script> runs an npm script in every package that has it. A package starts once the packages it depends on (directly or not) have finished, and packages that do not depend on each other run at the same time. Each line of output starts with the name of its package:
lib │ start lib
tools │ start tools
tools │ end tools
lib │ end lib
app │ start app
app │ end app
✔ build ran in tools, lib, app| Option | Meaning |
|---|---|
--parallel | Start all the packages at once, without waiting for their dependencies: for scripts that keep running, such as dev servers and watchers (lakutata run dev --parallel) |
--scope <names...> | Only these packages; * matches any characters (--scope 'app' '@shop/*'). The packages they depend on are neither run nor waited for, and a scope matching no package is an error |
--concurrency <n> | How many packages run at a time (the number of processors by default) |
--no-bail | Go on after a failure: the packages that depend on the failed one are skipped, the others still run |
- Failures: by default, no package starts after a failure, and those already running finish. The command ends with
error: build failed in lib, not run in appand the exit code 1. - No package has the script: the command prints
No package has the script lintand succeeds. - The package manager: the scripts run with
npm run,pnpm runoryarn run, chosen from thepackageManagerfield of the rootpackage.json("pnpm@9.15.0"), else from the files at the root (pnpm-lock.yamlorpnpm-workspace.yaml,yarn.lock,package-lock.json); npm by default. - Arguments: the scripts take no extra arguments (
lakutata run build -- --watchis refused): add a script instead.
3. Giving the packages a version
All the packages of a workspace share one version. lakutata version <bump> gives it to all of them, and updates the dependencies between them. From version 1.2.3:
lakutata version patch # 1.2.4, after a confirmation in a terminal
lakutata version minor --git --yes # 1.3.0, committed and tagged v1.3.0
lakutata version preminor --preid beta # 1.3.0-beta.0, then prerelease gives 1.3.0-beta.1
lakutata version 2.0.0The output, in a workspace at 0.0.16:
Version 0.0.16 → 0.0.17
app 0.0.16 → 0.0.17
lib 0.0.16 → 0.0.17
tools 0.0.16 → 0.0.17
✔ Version 0.0.17 given (packages/app/package.json, packages/lib/package.json, packages/tools/package.json, package.json)- The current version is the
versionoflerna.jsonwhen that file gives one, else the one of the rootpackage.json, else the highest version among the packages, a prerelease included (0.0.0when there is none). The new version is written back there, and into every package, the private ones included. - The bumps are
major,minor,patch,premajor,preminor,prepatch,prerelease, or an explicit version.--preid <id>names the prerelease of thepre…bumps (betagives1.3.0-beta.0; without it,1.3.0-0). - The dependencies between the packages follow:
^0.0.16becomes^0.0.17, and so do~,>=,=andworkspace:^ranges.*,workspace:*andfile:dependencies are left as they are. With0.xversions this matters, because^0.0.16does not accept0.0.17. - The lock file follows too:
package-lock.jsonorpnpm-lock.yamlis updated, so thatnpm ci(orpnpm install --frozen-lockfile) still accepts it. Ayarn.lockis not: the command reminds you to runyarn install. --gitcommits the changed files, and only them, with the messagev1.3.0and tags the commitv1.3.0. Nothing is pushed. The repository and the tag are checked before a file is changed.- The confirmation is asked only in a terminal: in CI, or when the output is piped, the version is given at once.
--yesskips it in a terminal too.
4. Adding a package
lakutata add <name> creates a TypeScript package in the workspace; --to <package> adds it to the dependencies of another package. In the web-react project of section 1, a package of code shared by the server and the web interface:
lakutata add @shop/shared --to app✔ Package @shop/shared 0.0.1 added: packages/shared (package.json, tsconfig.json, src/index.ts, src/tests/index.spec.ts, README.md), a dependency of app
Next: npm install (links the package), then npm run rebuild -w @shop/shared--to names one package: add the dependency to ui by hand, in packages/ui/package.json:
{
"dependencies": {
"@shop/shared": "^0.0.1"
}
}Then npm install links the new package, and lakutata list shows it:
3 packages, version 0.0.1
app 0.0.1 (private) packages/app → ui, @shop/shared
@shop/shared 0.0.1 (private) packages/shared
ui 0.0.1 (private) packages/ui → @shop/sharednpm run build (lakutata run rebuild) now builds @shop/shared first, then ui, then app, and both import it as any package: import {greet} from '@shop/shared'.
- Where: in the directory of the packages of the workspace, without the scope of the name: the directory of the first pattern such as
apps/*(packages/*when there is one). A workspace whose patterns have no such directory (only packages named one by one, orpackages/**) is refused. - Its version is the version of the workspace (
0.0.1without one, or with the independent versions of lerna), and--toadds the dependency"@shop/shared": "^0.0.1". - Its files:
src/index.ts(an example function to replace), a test run bynode --test, and apackage.jsonwhose scripts areclean,build(tsc),rebuildandtest. It compilessrcintobuild, with its type declarations, which itsexportsgive to the other packages. Code that the web interface also uses must not use the modules of Node.js. - Private: the package is
"private": true. Remove that line to publish it (npm publish -w @shop/shared). - Nothing is installed: run the install of the package manager of the workspace afterwards, as the command says (
pnpm install,yarn install), so that the other packages see the new one.
lakutata add <name> --native gyp|cmake|rust creates a native addon instead: see NativeAddons.
5. Coming from lerna
| lerna | lakutata |
|---|---|
lerna run rebuild | lakutata run rebuild |
lerna run dev --parallel | lakutata run dev --parallel |
lerna version patch --no-git-tag-version --no-push -y | lakutata version patch --yes |
lerna version patch | lakutata version patch --git (the tag is not pushed) |
- A workspace can keep its
lerna.json: lakutata reads and writes itsversiononly. The packages come from theworkspacesof the rootpackage.json(orpnpm-workspace.yaml), not from thepackagesoflerna.json. - lakutata gives all the packages one version: a
lerna.jsonwith"version": "independent"cannot be bumped. - It does not write changelogs, cache builds, select the packages changed since a release, or publish: publish with
npm publish -w <package>. Workspaces that need these keep lerna (or nx, turborepo) alongside it.