Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e755c8d
feat(templates): add the base Vite + React template
fernandomg Oct 5, 2026
9481e35
feat(templates): add the localnet fragment
fernandomg Oct 5, 2026
9704d60
feat: add the create-canton-dappbooster CLI
fernandomg Oct 5, 2026
a5aaecd
chore: register the scaffolder and templates with the root tooling
fernandomg Oct 5, 2026
321aa9e
chore: bump the scaffolder and base template to 0.5.5
gabitoesmiapodo Oct 8, 2026
595c3aa
refactor: move the templates into create-canton-dappbooster
gabitoesmiapodo Oct 8, 2026
33762b2
refactor(kit): list the manifests the version scripts read
gabitoesmiapodo Oct 8, 2026
5b2fb44
fix(templates): allow the localnet build scripts under pnpm
gabitoesmiapodo Oct 9, 2026
c04d7de
feat: redraw create-canton-dappbooster with Ink
gabitoesmiapodo Oct 9, 2026
b5c73e0
refactor: rename the create-canton-dappbooster folder to canton-create
gabitoesmiapodo Oct 9, 2026
558457d
refactor(canton-create): split the scaffold into starter, daml-toolin…
gabitoesmiapodo Oct 9, 2026
444b714
refactor: move the vesting dApp to examples/amulet-vesting
gabitoesmiapodo Oct 9, 2026
f5598a4
refactor: rename the examples folder to example-dapps
gabitoesmiapodo Oct 9, 2026
9c65102
feat(canton-create): ask for the local network and the dApp, with a f…
gabitoesmiapodo Oct 9, 2026
0f32581
feat(canton-create): pack the example dApps in try
gabitoesmiapodo Oct 9, 2026
e6b8248
feat(canton-create): record a demo of the installer
gabitoesmiapodo Oct 10, 2026
d502470
docs(canton-create): write the installer docs
gabitoesmiapodo Oct 10, 2026
29a1b34
docs: write the scaffold and example READMEs for a new project
gabitoesmiapodo Oct 10, 2026
f45b145
docs: describe canton-create in the root docs
gabitoesmiapodo Oct 10, 2026
3ae4cf1
docs: clean up the root CLAUDE.md and architecture.md wording
gabitoesmiapodo Oct 10, 2026
1b65625
fix(canton-create): clean up after a failed demo recording and fix tr…
gabitoesmiapodo Oct 10, 2026
41c6913
docs: correct facts in the installer and root docs
gabitoesmiapodo Oct 10, 2026
d923037
refactor(canton-create): trim the demo recorder and try
gabitoesmiapodo Oct 10, 2026
ec144d8
docs: stop repeating what other sections already say
gabitoesmiapodo Oct 10, 2026
2fb7362
refactor: run the monorepo loop on the scaffold's own scripts
gabitoesmiapodo Oct 10, 2026
e8ecd45
chore: stop ignoring PNG files at the repo root
gabitoesmiapodo Oct 10, 2026
cced5d3
build: publish every public package in the release
gabitoesmiapodo Oct 10, 2026
d11cac3
Merge branch 'main' into feat/create-canton-dappbooster
gabitoesmiapodo Oct 10, 2026
767b1fe
refactor: move kit/ into scripts/
gabitoesmiapodo Oct 13, 2026
22411b1
refactor: point everything at scripts/
gabitoesmiapodo Oct 13, 2026
5741732
chore: match the Biome schema to the installed CLI
gabitoesmiapodo Oct 14, 2026
a7d871d
fix(canton-create): remove what was written when Ctrl-C cancels a run
gabitoesmiapodo Oct 14, 2026
379c8c0
fix(canton-create): keep a versioned example name in the example scope
gabitoesmiapodo Oct 14, 2026
59fa001
docs(canton-create): reword the example name rule
gabitoesmiapodo Oct 14, 2026
cba4b8a
fix(canton-create): only use an installed example on the installer's …
gabitoesmiapodo Oct 14, 2026
9d342f8
fix(canton-create): accept an example tarball with any top folder
gabitoesmiapodo Oct 14, 2026
01e1e3a
test(canton-create): check every frame in the two App tests that race…
gabitoesmiapodo Oct 14, 2026
65a466c
fix(canton-create): remove the parent folders a failed run created
gabitoesmiapodo Oct 14, 2026
0f0b1b4
fix: read the workspace packages from pnpm in the version scripts
gabitoesmiapodo Oct 14, 2026
8510545
build: declare canton-barebones only in the localnet layer
gabitoesmiapodo Oct 14, 2026
17ee635
fix(canton-create): refuse a layer manifest field the merge would drop
gabitoesmiapodo Oct 14, 2026
4999091
fix(canton-create): refuse an .env.example block that redefines a key
gabitoesmiapodo Oct 14, 2026
f81a64f
docs: drop the stale versions and package count
gabitoesmiapodo Oct 15, 2026
a6e38c8
fix(canton-create): stop pointing projects without a LocalNet at mint…
gabitoesmiapodo Oct 15, 2026
f8996d6
fix: drop the -- that pnpm forwards to the installer and dev-stack.sh
gabitoesmiapodo Oct 15, 2026
190b0d4
chore: run the installer and script tests in the pre-commit hook
gabitoesmiapodo Oct 15, 2026
dd7afeb
docs: require pre-commit entries for every new workspace package
gabitoesmiapodo Oct 15, 2026
f90109d
fix(canton-create): run the scaffold scripts from a symlinked or spac…
gabitoesmiapodo Oct 15, 2026
08e8c17
fix(canton-create): say why dpm build failed in build-dar.mjs
gabitoesmiapodo Oct 15, 2026
9bec3af
docs: point the root README at create-canton-dappbooster
gabitoesmiapodo Oct 15, 2026
d6c4618
docs(canton-create): show the preselect commands for npm and pnpm
gabitoesmiapodo Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm -r --filter '!./dapp/daml' run --if-present build
- run: pnpm -r run --if-present build
- run: pnpm knip
- run: pnpm docs:check
- run: pnpm run check:anatomy
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Verify the tag matches the manifests
env:
TAG: ${{ github.event.release.tag_name }}
run: node kit/check-versions.mjs "${TAG#v}"
run: node scripts/check-versions.mjs "${TAG#v}"
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
Expand Down
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ typedoc/
# canton-barebones: its scaffolded config, the Splice checkout and the runtime env
.canton-localnet/

# dev-stack.sh: logs of the background processes it starts
.dev-stack/

# wallet-gateway: its sqlite wallet and signing stores
.wallet-gateway/

Expand Down Expand Up @@ -47,11 +50,15 @@ docs/
**/.playwright-mcp/
test-results/
playwright-report/
# stray captures land at the repo root; no tracked image belongs there
/*.png

# brainstorming visual companion
.superpowers/

# vitest coverage output
coverage/

# canton-create: the sandbox try.sh installs the packed CLI into
canton-create/.try/
# the DAML starter's build output and fetched DARs
canton-create/scaffold/starter/daml/.daml/
canton-create/scaffold/starter/daml/deps/
2 changes: 1 addition & 1 deletion .lintstagedrc.format.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// The writing half of the pre-commit, kept apart so the read-only gates in .lintstagedrc.mjs can
// run concurrently behind it. See .husky/pre-commit.
export default {
'{canton-connect,canton-dappbooster,canton-theme,dapp/frontend,kit,scripts}/**/*.{ts,tsx,js,jsx,json,jsonc,mjs,cjs,css}':
'{canton-connect,canton-dappbooster,canton-theme,example-dapps,scripts,canton-create}/**/*.{ts,tsx,js,jsx,json,jsonc,mjs,cjs,css}':
'biome check --write --no-errors-on-unmatched',
}
14 changes: 7 additions & 7 deletions .lintstagedrc.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Read-only gates only. The formatter runs first, from .lintstagedrc.format.mjs, so these are safe
// to run concurrently with each other.
export default {
'canton-connect/src/**/*.{ts,tsx,js,jsx}': () => 'pnpm -C canton-connect test',
Comment thread
fernandomg marked this conversation as resolved.
'canton-dappbooster/src/**/*.{ts,tsx,js,jsx}': () => 'pnpm -C canton-dappbooster test',
'dapp/frontend/src/**/*.{ts,tsx,js,jsx}': () => 'pnpm -C dapp/frontend test',
// One task for the whole doc gate, under the name CI uses: typedoc resolves both packages in a
// single run anyway, so splitting it per package bought a second and two ways to drift.
// Snippets live in the READMEs as well as in the doc blocks, so a doc edit re-runs the compile.
'example-dapps/*/src/**/*.{ts,tsx,js,jsx}': (files) =>
[...new Set(files.map((file) => file.match(/example-dapps\/[^/]+/)?.[0]))].map(
(example) => `pnpm -C ${example} test`,
),
'canton-create/**/*.{ts,tsx,mjs,sh,json}': () => 'pnpm -C canton-create test',
'{canton-create/scaffold/*/scripts,scripts}/*.{mjs,sh}': () =>
'node --test "canton-create/scaffold/*/scripts/*.test.mjs" "scripts/*.test.mjs"',
'{canton-connect,canton-dappbooster}/{src/**/*.{ts,tsx},doc-fixtures.d.ts,README.md,architecture.md,coming-from-wagmi.md,typedoc.json}':
() => 'pnpm docs:check',
// Either side of the L2/L3 contract moving on its own is what this catches.
'{canton-dappbooster/src/{components,providers}/**/anatomy.ts,canton-theme/src/**/*.css}': () =>
'pnpm run check:anatomy',
}
718 changes: 356 additions & 362 deletions CLAUDE.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ Canton dAppBooster is an open-source local development stack and boilerplate by

## Installation

Run the install script and follow the on-screen instructions.
Run the installer and follow the on-screen instructions.

```bash
pnpm dlx dappbooster --canton
pnpm create canton-dappbooster
```

A more detailed [step-by-step installation guide](https://docs.dappbooster.cc/installation/step-by-step) is available in the [documentation](https://docs.dappbooster.cc/).

## Starting the dev stack
Comment thread
fernandomg marked this conversation as resolved.

Once Canton dAppBooster is installed, the easiest way to start the development stack is using the `dev-stack` script. It allows you to install, bring up and tear down everything you need in a simple way.
Once Canton dAppBooster is installed, the easiest way to start the development stack is using the `stack` script. It allows you to install, bring up and tear down everything you need in a simple way.

```bash
./scripts/dev-stack.sh
pnpm stack
```

---
Expand Down
135 changes: 71 additions & 64 deletions architecture.md

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.13/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"vcs": {
"clientKind": "git",
Expand All @@ -12,9 +12,9 @@
"canton-connect/**",
"canton-dappbooster/**",
"canton-theme/**",
"dapp/frontend/**",
"kit/**",
"example-dapps/**",
"scripts/**",
"canton-create/**",
"!**/api-specs",
"!**/*.svg"
]
Expand Down Expand Up @@ -47,7 +47,7 @@
},
"overrides": [
{
"includes": ["kit/**", "**/scripts/**", "**/*.d.ts"],
"includes": ["**/scripts/**", "**/*.d.ts"],
"linter": {
"rules": {
"style": {
Expand All @@ -63,7 +63,7 @@
}
},
{
"includes": ["dapp/frontend/**"],
"includes": ["example-dapps/**"],
"css": {
"parser": {
"cssModules": false,
Expand Down Expand Up @@ -110,7 +110,7 @@
}
},
{
"includes": ["dapp/frontend/**"],
"includes": ["example-dapps/**"],
"linter": {
"rules": {
"style": {
Expand Down Expand Up @@ -172,7 +172,7 @@
}
},
{
"includes": ["dapp/frontend/src/**", "!**/*.test.ts", "!**/*.test.tsx", "!**/testing/**"],
"includes": ["example-dapps/*/src/**", "!**/*.test.ts", "!**/*.test.tsx", "!**/testing/**"],
"linter": {
"rules": {
"style": {
Expand Down
6 changes: 3 additions & 3 deletions canton-connect/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ the session and the transports. Browser-only, and built to stay cheap to delete.
indistinguishable from a real wallet's.
- **The hook and config surface is documented in JSDoc, and nowhere else.** The published reference
is generated from it, and a table beside the code drifts from it unnoticed.
- **App-agnostic.** No imports from `dapp/`; name no wallet.
- **App-agnostic.** No imports from `example-dapps/`; name no wallet.

## Bumping `dapp-sdk`

Expand All @@ -53,8 +53,8 @@ the session and the transports. Browser-only, and built to stay cheap to delete.
- the shape of the `SPLICE_WALLET_PICKER_RESULT` message
- `connect()` calls `this.walletPicker` only when it reaches the picker, so a swapped one is honored

Why: [`architecture/popup-close-guard.md`](architecture/popup-close-guard.md). Serve `dapp/frontend`
and walk all five:
Why: [`architecture/popup-close-guard.md`](architecture/popup-close-guard.md). Serve
`example-dapps/amulet-vesting` and walk all five:

1. Close the picker without choosing, three times over: the button re-enables each time, and the
next real connect raises exactly one approval prompt.
Expand Down
2 changes: 1 addition & 1 deletion canton-connect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Most of that machinery exists to work around gaps in `@canton-network/dapp-sdk`,

## Status

Published to npm as `@bootnodedev/canton-connect`. `dapp/frontend` in this repo uses it too, and links the local folder as long as the version satisfies the declared range.
Published to npm as `@bootnodedev/canton-connect`. `example-dapps/amulet-vesting` in this repo uses it too, and links the local folder as long as the version satisfies the declared range.

## Install

Expand Down
14 changes: 7 additions & 7 deletions canton-connect/architecture/popup-close-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ origin and the message type, so the next successful connect woke every past one:

`settleAbandonedConnect` drains them at the close instead. It posts the SDK's own
`SPLICE_WALLET_PICKER_RESULT` to our window, the only thing that makes that listener unsubscribe.
The `providerId` matches no registered adapter, so it fails with `WalletNotFoundError`
before reaching a wallet, then rejects out of `waitForWalletPickerRetrySelection` because the popup
is closed. `walletType` stays `'browser'` to keep it out of the branch that registers a remote
adapter from the message, and no `name` is sent, so anything else on the page watching for a pick
can tell the two apart; `dapp/frontend` does exactly that to label its connect button. The drain is
skipped while a second guard is in flight, since the message would resolve that one's live waiter
too.
The `providerId` matches no registered adapter, so it fails with `WalletNotFoundError` before
reaching a wallet, then rejects out of `waitForWalletPickerRetrySelection` because the popup is
closed. `walletType` stays `'browser'` to keep it out of the branch that registers a remote adapter
from the message, and no `name` is sent, so anything else on the page watching for a pick can tell
the two apart; `example-dapps/amulet-vesting` does exactly that to label its connect button. The
drain is skipped while a second guard is in flight, since the message would resolve that one's live
waiter too.

A cancel can land before the popup exists: `connect()` waits ~300 ms on extension discovery before
it calls the picker. A `connect.cancel` inside that wait finds nothing to close and nothing to
Expand Down
2 changes: 1 addition & 1 deletion canton-connect/doc-fixtures.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// The placeholder vocabulary an @example may lean on. `kit/docs-check.mjs` compiles every
// The placeholder vocabulary an @example may lean on. `scripts/docs-check.mjs` compiles every
// snippet with this file as a root, so an example may use only what is declared here, and a
// placeholder's type is the real one — a loosened signature here would hide a broken example.
//
Expand Down
2 changes: 1 addition & 1 deletion canton-connect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ type Exact<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
type Assert<T extends true> = T

// Fails typecheck when an SDK bump changes the account entry. `Account` is defined through it so
// it cannot go unused, which `dapp/frontend`'s `noUnusedLocals` would reject.
// it cannot go unused, which `example-dapps/amulet-vesting`'s `noUnusedLocals` would reject.
type AccountPinned = Assert<Exact<Wallet, PinnedAccount>>

/**
Expand Down
2 changes: 1 addition & 1 deletion canton-connect/typedoc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://typedoc.org/schema.json",
"extends": ["../kit/typedoc.shared.json"],
"extends": ["../scripts/typedoc.shared.json"],
"entryPoints": ["src/index.ts", "src/testing/index.ts"],
"projectDocuments": ["coming-from-wagmi.md"],
"intentionallyNotExported": ["ConnectionActorRef", "AccountPinned"],
Expand Down
File renamed without changes.
84 changes: 84 additions & 0 deletions canton-create/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Agent configuration: `canton-create`

This file applies only to `canton-create/`, the `create-canton-dappbooster` package. For
monorepo-wide rules see [`../CLAUDE.md`](../CLAUDE.md). For how a run and the scaffold layers fit
together, see [`architecture.md`](architecture.md).

## Every option is a flag

- An agent runs the installer without a terminal, so it can only pass flags. Every question the
wizard asks gets a flag in the same change. The `USAGE` text in `src/index.tsx` and the flag table
in the README both list it.
- Without a terminal the installer asks nothing, so every question also needs a default.
- A flag that would only repeat a default does not exist, so there is no `--app` and no `--yes`.

## Scaffold rules

- Each app owns its contract. `scaffold/daml-tooling/` holds no Daml, only the scripts that build
and deploy whatever `daml/daml.yaml` the app carries.
- A layer may add files and merge `scripts`, `dependencies` and `devDependencies` into the app's
manifest. It never redefines a key an earlier layer set. `scaffold` appends to `.gitignore` and
`README.md`, and merges `.env.example` by key, leaving out a block whose keys the file already
sets. Any other overwrite is a design error in `scaffold/`, and `scaffold` throws
`ScaffoldConflictError`. So does any other field in a layer's `package.json`, apart from the
layer's own `name`, `private`, `version` and `type`, because the merge would drop it.
- A layer's `README.md` is a section that gets appended, so it starts with a `##` heading and never
with a title.
- The root range rule has no exception in `scaffold/`: the tarball ships the folders as they are, so
nothing rewrites a `workspace:` range on the way out.
- A file that has to land as `.gitignore` or `pnpm-workspace.yaml` lives here as `_gitignore` or
`_pnpm-workspace.yaml`, and `scaffold` renames it back. npm drops a `.gitignore` from every
tarball, and a `pnpm-workspace.yaml` inside the monorepo makes pnpm treat the folder as a
workspace root of its own.
- The monorepo runs the `daml-tooling/` and `localnet/` scripts on `example-dapps/amulet-vesting`
instead of keeping copies. Each script finds the app's `daml/` and `.env` through
`CANTON_APP_DIR`, which defaults to its own project. A new script reads the app from there, never
from a path to its own folder. Their tests sit beside them, and `files` keeps the tests out
of the tarball.
- `node dist/index.js` run from this folder is not a test of the installer. Here `scaffold/` holds
`node_modules` and build output that `files` leaves out of the tarball, and the copy fails on
them. Run the packed package instead: `pnpm try`, or `src/testing/packedScaffold.ts` in a test.

## Examples

An example is a complete app published on its own as `@bootnodedev/canton-example-<name>`, and the
installer never ships one inside its tarball. Adding one takes:

- the app in `example-dapps/<name>/`, with a `files` list naming what it publishes and
`publishConfig.access: "public"`
- its name, label and hint in `EXAMPLES` in `src/apps.ts`

`pnpm-workspace.yaml` already lists `example-dapps/*`. So the root `release` script publishes every
folder there, and the version bump and `check:versions` cover it too.

## Dependencies

`tsdown` bundles Ink, React and every other import into `dist/index.js`, so the published package
has no dependencies to install. A new import goes in `devDependencies` for the same reason.
`react-devtools-core` is the one import left out of the bundle, because Ink only loads it when
`DEV=true`.

## Demo recording

`demo.svg` in the README is an animated SVG of a real run. Record it again after any change to the
terminal UI:

```shell
canton-create/scripts/record-demo.ts
```

The script packs the installer, runs it in a temporary directory, and overwrites `demo.svg`. The
install it starts needs network. Its comments explain the trimming and the frame merging.

- It runs on Bun, because it needs a pseudo-terminal. Node has none without `node-pty`, a native
module, and `Bun.spawn` opens one through its `terminal` option.
- [svg-term-cli](https://github.com/marionebl/svg-term-cli) does the conversion, through `pnpm dlx`.
It is not a dependency. `COLS` and `ROWS` set the window, so a change to either means changing
the size of the `<img>` in the README too.

## Testing

`pnpm test` runs `vitest`, and the component tests render through `ink-testing-library`. It can drop
a key sent before the input handler mounts, so `src/components/App.test.tsx` sends each key through
`pressUntil`. The test then checks all the frames joined, because the last frame can be empty
once the app exits.
Loading
Loading