diff --git a/.changeset/vland-init-git-hooks.md b/.changeset/vland-init-git-hooks.md new file mode 100644 index 00000000..6a9056ba --- /dev/null +++ b/.changeset/vland-init-git-hooks.md @@ -0,0 +1,5 @@ +--- +"@variablelab/vland": patch +--- + +`vland init` runs `git init` before installing dependencies, so the git hooks a dependency sets up on install (the templates' lefthook ones) land in the new repo. Before, the install ran first, found no repo and skipped them, so a generated project's `lefthook.yml` never ran until a manual `lefthook install`. The first commit skips those hooks, because it's the template as generated. diff --git a/.changeset/vland-init-git-identity.md b/.changeset/vland-init-git-identity.md new file mode 100644 index 00000000..58bbfff7 --- /dev/null +++ b/.changeset/vland-init-git-identity.md @@ -0,0 +1,5 @@ +--- +"@variablelab/vland": patch +--- + +`vland init` makes the first commit as your own git identity, the `user.name` and `user.email` git resolves for the new repo. Before, it always committed as `vland `, so the commit wasn't yours and a signed one showed as "Unverified" on GitHub. It still falls back to `vland` and `noreply@variable.land` for whichever of the two git has no value for. diff --git a/.changeset/vland-templates-org-standard.md b/.changeset/vland-templates-org-standard.md new file mode 100644 index 00000000..d9481169 --- /dev/null +++ b/.changeset/vland-templates-org-standard.md @@ -0,0 +1,16 @@ +--- +"@variablelab/vland": minor +--- + +Projects from `vland init` follow Variable Lab's current standard: + +- mise pins Node.js 24 and pnpm 10.30.3, with a `mise.lock` that needs mise 2026.9.16 or later. `.node-version` is gone. +- `engines.node` is `>=22.18.0`, the first Node.js that strips types without a flag, which `rr` needs to load `run-run.config.mts`. +- CI sets up with `jdx/mise-action` and runs `rr check`. Every action is pinned by SHA, checkout keeps no credentials, and a new `workflow-audit.yml` runs zizmor. The monorepo gets a CI workflow, which tests and builds only what a PR affects. +- Changesets 3, with `commit: true` and `format: false`. The library's release workflow runs `changesets/action` v2 as vland-bot, so its repo needs the `VLAND_BOT_CLIENT_ID` variable and the `VLAND_BOT_PRIVATE_KEY` secret. +- The changelog links to `withvariablelab/` for a public library and `variableland/` otherwise, not `/`. +- pnpm waits a day before installing a new release, and runs lefthook's postinstall on every install, so the git hooks get installed. +- `@rrlab/cli` is `^1.3.4`. The `^0.1.0` the templates asked for doesn't exist, so `pnpm install` failed. +- The backend's `pnpm dev` runs `node --watch` and loads `.env`, without `tsx`. +- The library's published `exports` point at what `rr pack` builds (`index.mjs` and `index.d.mts`), and its README at the scoped package name. +- Every generated file passes both oxfmt and Biome (with `@rrlab/biome-config`), including `package.json` key order with `private` after `version`, so `rr check` starts green with either. The READMEs explain how to pair the oxc plugin with `ts`. diff --git a/.github/workflows/workflow-audit.yml b/.github/workflows/workflow-audit.yml index d7cae638..63ffe4df 100644 --- a/.github/workflows/workflow-audit.yml +++ b/.github/workflows/workflow-audit.yml @@ -28,8 +28,9 @@ jobs: persist-credentials: false # pipx ships with the runner image: no third-party setup action. + # Repo root, not .github/: it also scans the workflows vland/templates/* generate. - name: ๐Ÿ”’ Scan workflows - run: pipx run --spec zizmor==1.30.1 zizmor --format=github .github/ + run: pipx run --spec zizmor==1.30.1 zizmor --format=github . env: # Enables zizmor's online audits (impostor commits, known-vulnerable actions). GH_TOKEN: ${{ github.token }} diff --git a/vland/cli/README.md b/vland/cli/README.md index 424fa4b4..9a155cc9 100644 --- a/vland/cli/README.md +++ b/vland/cli/README.md @@ -43,7 +43,7 @@ See [`CLI.md`](./CLI.md) for the full reference (auto-generated per release). | `backend` | An Elysia (`@elysiajs/node`) backend with evlog, Vitest, Dockerfile, CI shape. | | `monorepo` | pnpm + Turbo workspace with an Elysia API, a Vite-React SPA, and a few internal packages. | -All templates target Node.js, use pnpm, and ship [`@rrlab/cli`](https://github.com/withvariablelab/dx/tree/main/run-run) (`rr`) as the single entry point for lint, format, type-check, and build. The per-tool config files (`biome.json`, `tsconfig.json`, `tsdown.config.ts`) are not bundled โ€” run `rr plugins add biome ts tsdown` (one at a time) in your new project to opt in. See the scaffolded README for the exact post-install setup. +All templates target Node.js, pin Node.js and pnpm with [mise](https://mise.jdx.dev) (`mise.toml` and `mise.lock`), run their GitHub Actions on SHA-pinned actions checked by [zizmor](https://docs.zizmor.sh), and ship [`@rrlab/cli`](https://github.com/withvariablelab/dx/tree/main/run-run) (`rr`) as the single entry point for lint, format, type-check, and build. The per-tool config files (`biome.json`, `tsconfig.json`, `tsdown.config.ts`) are not bundled โ€” run `rr plugins add biome ts tsdown` (one at a time) in your new project to opt in. See the scaffolded README for the exact post-install setup. ## Shell completion diff --git a/vland/cli/src/actions/init.ts b/vland/cli/src/actions/init.ts index 01a3fe59..d1bd16f8 100644 --- a/vland/cli/src/actions/init.ts +++ b/vland/cli/src/actions/init.ts @@ -5,8 +5,8 @@ import { hasTTY, palette } from "@variablelab/clibuddy"; import { installDependencies } from "nypm"; import type { Context } from "#src/services/ctx.ts"; import { logger } from "#src/services/logger.ts"; -import { applyRootPackage, replacePlaceholders } from "./placeholders.ts"; -import { fetchTemplate, TEMPLATE_META, TEMPLATES, type TemplateName, type Visibility } from "./template.ts"; +import { applyRootPackage, replacePlaceholders, rootPackageName } from "./placeholders.ts"; +import { fetchTemplate, GITHUB_OWNERS, TEMPLATE_META, TEMPLATES, type TemplateName, type Visibility } from "./template.ts"; export type InitOptions = { name?: string; @@ -45,21 +45,42 @@ async function isDirEmpty(dir: string): Promise { } } -async function readGitAuthor(shell: Context["shell"]): Promise { +type GitIdentity = { name: string; email: string }; + +const FALLBACK_GIT_IDENTITY: GitIdentity = { name: "vland", email: "noreply@variable.land" }; + +/** The `user.name` and `user.email` git resolves from the shell's cwd. Each is empty when unset. */ +async function readGitIdentity(shell: Context["shell"]): Promise { try { const [name, email] = await Promise.all([ shell.runCaptured("git", ["config", "--get", "user.name"], { throwOnError: false }), shell.runCaptured("git", ["config", "--get", "user.email"], { throwOnError: false }), ]); - const trimmedName = name.stdout.trim(); - const trimmedEmail = email.stdout.trim(); - if (!trimmedName) return undefined; - return trimmedEmail ? `${trimmedName} <${trimmedEmail}>` : trimmedName; + return { name: name.stdout.trim(), email: email.stdout.trim() }; } catch { - return undefined; + return { name: "", email: "" }; } } +async function readGitAuthor(shell: Context["shell"]): Promise { + const { name, email } = await readGitIdentity(shell); + if (!name) return undefined; + return email ? `${name} <${email}>` : name; +} + +/** + * `-c` flags that fill in only the half of the identity git can't resolve. A + * configured identity is left alone, so `commit.gpgsign` signs with the user's + * own email. + */ +async function gitIdentityFallbackArgs(shell: Context["shell"]): Promise { + const { name, email } = await readGitIdentity(shell); + return [ + ...(name ? [] : ["-c", `user.name=${FALLBACK_GIT_IDENTITY.name}`]), + ...(email ? [] : ["-c", `user.email=${FALLBACK_GIT_IDENTITY.email}`]), + ]; +} + function abort(message: string): never { cancel(message); process.exit(1); @@ -167,6 +188,9 @@ export async function runInit(ctx: Context, options: InitOptions) { projectName: name, author, year: new Date().getFullYear().toString(), + packageName: rootPackageName({ template, projectName: name, visibility }), + // Only a library picks its visibility: backend and monorepo roots are always private. + githubOwner: GITHUB_OWNERS[template === "library" ? (visibility ?? "public") : "private"], }); await applyRootPackage(dir, { template, projectName: name, visibility }); placeholderSpin.stop("Placeholders applied"); @@ -174,6 +198,17 @@ export async function runInit(ctx: Context, options: InitOptions) { const shouldInstall = await resolveYesNo(options.install, "Install dependencies?"); const shouldGit = await resolveYesNo(options.git, "Initialise a git repository?"); + const gitShell = shell.at(dir); + let gitInitError: unknown; + if (shouldGit) { + // Before the install, so a postinstall that sets up git hooks (lefthook's) finds the repo. + try { + await gitShell.runCaptured("git", ["init"]); + } catch (error) { + gitInitError = error; + } + } + if (shouldInstall) { const installSpin = spinner(); installSpin.start(`Installing dependencies with ${palette.highlight(PACKAGE_MANAGER)}`); @@ -197,17 +232,12 @@ export async function runInit(ctx: Context, options: InitOptions) { const gitSpin = spinner(); gitSpin.start("Initialising git repository"); try { - const gitShell = shell.at(dir).child({ - env: { - GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "vland", - GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "noreply@variable.land", - GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "vland", - GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "noreply@variable.land", - }, - }); - await gitShell.runCaptured("git", ["init"]); + if (gitInitError) throw gitInitError; await gitShell.runCaptured("git", ["add", "-A"]); - await gitShell.runCaptured("git", ["commit", "-m", "chore: initial commit from vland"]); + // Read after `git init`, so an `includeIf "gitdir:โ€ฆ"` identity for the new repo applies. + const identityArgs = await gitIdentityFallbackArgs(gitShell); + // --no-verify: the hooks the install just set up are for the user's commits, not the template as generated. + await gitShell.runCaptured("git", [...identityArgs, "commit", "--no-verify", "-m", "chore: initial commit from vland"]); gitSpin.stop("Initialised git repository"); } catch (error) { gitSpin.stop("Failed to initialise git", 1); diff --git a/vland/cli/src/actions/placeholders.ts b/vland/cli/src/actions/placeholders.ts index bb1a2c72..ffa5ffd9 100644 --- a/vland/cli/src/actions/placeholders.ts +++ b/vland/cli/src/actions/placeholders.ts @@ -1,14 +1,18 @@ import fs from "node:fs"; import { readdir, readFile, stat, writeFile } from "node:fs/promises"; import { extname, join } from "node:path"; -import { createPkg } from "@variablelab/clibuddy"; +import { createPkg, type Pkg } from "@variablelab/clibuddy"; import { logger } from "#src/services/logger.ts"; import { LIBRARY_SCOPES, type TemplateName, type Visibility } from "./template.ts"; +type PackageJson = Pkg["packageJson"]; + export type Placeholders = { projectName: string; author: string; year: string; + packageName: string; + githubOwner: string; }; const TEXT_EXTENSIONS = new Set([ @@ -86,7 +90,9 @@ function applyPlaceholders(content: string, values: Placeholders) { return content .replaceAll("{{projectName}}", values.projectName) .replaceAll("{{author}}", values.author) - .replaceAll("{{year}}", values.year); + .replaceAll("{{year}}", values.year) + .replaceAll("{{packageName}}", values.packageName) + .replaceAll("{{githubOwner}}", values.githubOwner); } export async function replacePlaceholders(rootDir: string, values: Placeholders) { @@ -119,15 +125,39 @@ export type ApplyRootPackageOptions = { }; /** - * Writes the root `package.json`'s `name` and `private` fields based on the - * chosen template and visibility. Uses `pkg-types` so field ordering and - * JSON formatting survive. - * - * Naming rules: - * - `library` private โ†’ `@variableland/`, `private: true` + * The root package's name: + * - `library` private โ†’ `@variableland/` * - `library` public โ†’ `@variablelab/` - * - `backend` โ†’ ``, `private: true` - * - `monorepo` โ†’ ``, `private: true` (root never publishes) + * - `backend`, `monorepo` โ†’ `` + */ +export function rootPackageName({ template, projectName, visibility }: ApplyRootPackageOptions): string { + return template === "library" ? `${LIBRARY_SCOPES[visibility ?? "public"]}/${projectName}` : projectName; +} + +/** + * Sets `private: true` right after `version`, where package.json sorters + * (oxfmt's among them) put it, so the file passes their format check as + * generated. Mutates in place: a copy would drop the formatting pkg-types + * remembers on the object. + */ +function setPrivate(packageJson: PackageJson): void { + const keys = Object.keys(packageJson); + const moved = keys + .slice(keys.indexOf("version") + 1) + .filter((key) => key !== "private") + .map((key) => [key, packageJson[key]] as const); + for (const [key] of moved) delete packageJson[key]; + delete packageJson.private; + packageJson.private = true; + for (const [key, value] of moved) packageJson[key] = value; +} + +/** + * Writes the root `package.json`'s `name` ({@link rootPackageName}) and + * `private` fields based on the chosen template and visibility. Uses + * `pkg-types` so field ordering and JSON formatting survive. `private: true` + * goes on a private library and on every backend and monorepo root, which + * never publishes. */ export async function applyRootPackage(rootDir: string, options: ApplyRootPackageOptions) { const debug = logger.subdebug("apply-root-package"); @@ -136,20 +166,11 @@ export async function applyRootPackage(rootDir: string, options: ApplyRootPackag const pkg = await createPkg(rootPath); if (!pkg) throw new Error("Could not find package.json"); - const { template, projectName, visibility } = options; + const { template, visibility } = options; try { - if (template === "library") { - const scope = LIBRARY_SCOPES[visibility ?? "public"]; - pkg.packageJson.name = `${scope}/${projectName}`; - if (visibility === "private") { - pkg.packageJson.private = true; - } - } else { - // backend + monorepo: bare project name, private root. - pkg.packageJson.name = projectName; - pkg.packageJson.private = true; - } + pkg.packageJson.name = rootPackageName(options); + if (template !== "library" || visibility === "private") setPrivate(pkg.packageJson); await pkg.write(pkg.packageJson); } catch (error) { debug("skipped %s", error); diff --git a/vland/cli/src/actions/template.ts b/vland/cli/src/actions/template.ts index 8757076c..d62ac248 100644 --- a/vland/cli/src/actions/template.ts +++ b/vland/cli/src/actions/template.ts @@ -25,6 +25,16 @@ export const LIBRARY_SCOPES: Record = { public: "@variablelab", }; +/** + * The GitHub owner of a project's repository, indexed by visibility: public + * projects live in the open-source `withvariablelab` org, private ones in + * `variableland`. + */ +export const GITHUB_OWNERS: Record = { + private: "variableland", + public: "withvariablelab", +}; + const GITHUB_SOURCE = "github:withvariablelab/dx"; const GITHUB_REF = "main"; diff --git a/vland/cli/test/helpers.ts b/vland/cli/test/helpers.ts index a19132a3..2cd51b8d 100644 --- a/vland/cli/test/helpers.ts +++ b/vland/cli/test/helpers.ts @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -13,6 +13,7 @@ type CliMode = "dev" | "prod"; type CliOptions = { cwd?: string; + env?: NodeJS.ProcessEnv; }; export function createTestCli(mode: CliMode = "prod") { @@ -22,13 +23,14 @@ export function createTestCli(mode: CliMode = "prod") { cwd: opts.cwd, env: mode === "dev" - ? { ...process.env, VLAND_TEMPLATES_DIR: TEMPLATES_DIR } + ? { ...process.env, VLAND_TEMPLATES_DIR: TEMPLATES_DIR, ...opts.env } : { ...process.env, NODE_ENV: "production", TEST: undefined, NO_COLOR: "1", VLAND_TEMPLATES_DIR: TEMPLATES_DIR, + ...opts.env, }, }); }; @@ -47,3 +49,29 @@ export function makeTmpDir(name: string): { dir: string; cleanup: () => void } { export function gitOutput(cwd: string, args: string[]): string { return spawnSync("git", args, { cwd, encoding: "utf8" }).stdout.trim(); } + +const WORKSPACE_BIN = path.join(REPO_ROOT, "node_modules", ".bin"); + +/** Runs the workspace's `oxfmt --check` over the project in `cwd`. */ +export function oxfmtCheck(cwd: string) { + return spawnSync(path.join(WORKSPACE_BIN, "oxfmt"), ["--check"], { cwd, encoding: "utf8" }); +} + +/** + * Runs the workspace's `biome check` over the project in `cwd`, set up the way + * `rr plugins add biome` sets it up: a biome.json that extends + * `@rrlab/biome-config`, resolved through a symlink to the workspace's + * node_modules. + */ +export function biomeCheck(cwd: string) { + writeFileSync( + path.join(cwd, "biome.json"), + `{\n "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",\n "extends": ["@rrlab/biome-config"]\n}\n`, + ); + symlinkSync(path.join(REPO_ROOT, "node_modules"), path.join(cwd, "node_modules"), "dir"); + return spawnSync(path.join(WORKSPACE_BIN, "biome"), ["check"], { + cwd, + encoding: "utf8", + env: { ...process.env, NO_COLOR: "1" }, + }); +} diff --git a/vland/cli/test/integration/init-git.test.ts b/vland/cli/test/integration/init-git.test.ts index 8a6a02f9..4e49090f 100644 --- a/vland/cli/test/integration/init-git.test.ts +++ b/vland/cli/test/integration/init-git.test.ts @@ -1,9 +1,27 @@ +import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { createTestCli, gitOutput, makeTmpDir, pathExists } from "#test/helpers.ts"; const cli = createTestCli(); +/** + * Points git at `gitconfig` alone (no system or real global config) and drops + * the identity env vars, which outrank any config. + */ +function isolatedGitEnv(gitconfig: string): NodeJS.ProcessEnv { + return { + GIT_CONFIG_GLOBAL: gitconfig, + GIT_CONFIG_NOSYSTEM: "1", + GIT_AUTHOR_NAME: undefined, + GIT_AUTHOR_EMAIL: undefined, + GIT_COMMITTER_NAME: undefined, + GIT_COMMITTER_EMAIL: undefined, + }; +} + +const LAST_COMMIT_IDENTITY = ["log", "-1", "--format=%an <%ae> | %cn <%ce>"]; + describe("vland init (git initialisation)", () => { let fixture: { dir: string; cleanup: () => void }; @@ -38,6 +56,56 @@ describe("vland init (git initialisation)", () => { expect(gitOutput(projectDir, ["log", "-1", "--pretty=%s"])).toBe("chore: initial commit from vland"); }); + test("commits as the user's git identity when one is configured", () => { + const gitconfig = path.join(fixture.dir, "gitconfig"); + writeFileSync(gitconfig, "[user]\n\tname = Jane Doe\n\temail = jane@example.com\n"); + + const name = "git-identity"; + const r = cli(`init ${name} -t library --no-install --git`, { cwd: fixture.dir, env: isolatedGitEnv(gitconfig) }); + expect(r.status).toBe(0); + + const projectDir = path.join(fixture.dir, name); + expect(gitOutput(projectDir, LAST_COMMIT_IDENTITY)).toBe("Jane Doe | Jane Doe "); + }); + + test("falls back to the vland identity when git has none configured", () => { + const gitconfig = path.join(fixture.dir, "gitconfig"); + writeFileSync(gitconfig, ""); + + const name = "git-no-identity"; + const r = cli(`init ${name} -t library --no-install --git`, { cwd: fixture.dir, env: isolatedGitEnv(gitconfig) }); + expect(r.status).toBe(0); + expect(r.stdout + r.stderr).not.toMatch(/Failed to initialise git/); + + const projectDir = path.join(fixture.dir, name); + expect(gitOutput(projectDir, LAST_COMMIT_IDENTITY)).toBe("vland | vland "); + }); + + test("runs git init before the install, and the first commit skips the hooks the install set up", () => { + // Stands in for pnpm and, like lefthook's postinstall, installs a git hook: one that rejects every commit. + // The corepack stub fails `corepack --version`, so nypm calls pnpm directly. + const bin = path.join(fixture.dir, "bin"); + mkdirSync(bin); + writeFileSync(path.join(bin, "corepack"), "#!/bin/sh\nexit 1\n", { mode: 0o755 }); + writeFileSync( + path.join(bin, "pnpm"), + "#!/bin/sh\nprintf '#!/bin/sh\\nexit 1\\n' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit\n", + { mode: 0o755 }, + ); + + const name = "git-hooks"; + const r = cli(`init ${name} -t library --install --git`, { + cwd: fixture.dir, + env: { PATH: `${bin}${path.delimiter}${process.env.PATH}` }, + }); + expect(r.status).toBe(0); + expect(r.stdout + r.stderr).toMatch(/Installed with/); + + const projectDir = path.join(fixture.dir, name); + expect(pathExists(path.join(projectDir, ".git", "hooks", "pre-commit"))).toBe(true); + expect(gitOutput(projectDir, ["log", "-1", "--pretty=%s"])).toBe("chore: initial commit from vland"); + }); + test("--no-git: skips and prints a Skipping note", async () => { const name = "git-off"; const r = cli(`init ${name} -t library --no-install --no-git`, { cwd: fixture.dir }); diff --git a/vland/cli/test/integration/init-templates.test.ts b/vland/cli/test/integration/init-templates.test.ts index 5c32d75e..b655b7c8 100644 --- a/vland/cli/test/integration/init-templates.test.ts +++ b/vland/cli/test/integration/init-templates.test.ts @@ -1,10 +1,16 @@ -import { readFile } from "node:fs/promises"; +import { readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { createTestCli, makeTmpDir, pathExists } from "#test/helpers.ts"; +import { TEMPLATES } from "#src/actions/template.ts"; +import { biomeCheck, createTestCli, makeTmpDir, oxfmtCheck, pathExists } from "#test/helpers.ts"; const cli = createTestCli(); +async function readChangelogRepo(projectDir: string): Promise { + const config = JSON.parse(await readFile(path.join(projectDir, ".changeset", "config.json"), "utf8")); + return config.changelog[1].repo; +} + describe("vland init (template scaffolding)", () => { let fixture: { dir: string; cleanup: () => void }; @@ -29,8 +35,10 @@ describe("vland init (template scaffolding)", () => { expect(license).not.toContain("{{"); const readme = await readFile(path.join(projectDir, "README.md"), "utf8"); - expect(readme).toContain(name); + expect(readme).toContain(`pnpm add @variablelab/${name}`); expect(readme).not.toContain("{{"); + + expect(await readChangelogRepo(projectDir)).toBe(`withvariablelab/${name}`); }); test("library: --visibility=private uses @variableland scope and sets private:true", async () => { @@ -41,6 +49,7 @@ describe("vland init (template scaffolding)", () => { const pkg = JSON.parse(await readFile(path.join(fixture.dir, name, "package.json"), "utf8")); expect(pkg.name).toBe(`@variableland/${name}`); expect(pkg.private).toBe(true); + expect(await readChangelogRepo(path.join(fixture.dir, name))).toBe(`variableland/${name}`); }); test("library: --visibility=public uses @variablelab scope and no private flag", async () => { @@ -117,6 +126,71 @@ describe("vland init (template scaffolding)", () => { const typesPkg = JSON.parse(await readFile(path.join(projectDir, "packages", "types", "package.json"), "utf8")); expect(typesPkg.name).toBe(`@${name}/types`); + + expect(await readChangelogRepo(projectDir)).toBe(`variableland/${name}`); + }); + + test.each(TEMPLATES)("%s: pins node and pnpm with mise and a lockfile", async (template) => { + const name = `std-${template}`; + const r = cli(`init ${name} -t ${template} --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + + const projectDir = path.join(fixture.dir, name); + expect(pathExists(path.join(projectDir, ".node-version"))).toBe(false); + + // rr loads run-run.config.mts with Node's type stripping, unflagged from 22.18.0. + const pkg = JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8")); + expect(pkg.engines.node).toBe(">=22.18.0"); + + const miseToml = await readFile(path.join(projectDir, "mise.toml"), "utf8"); + expect(miseToml).toMatch(/\[settings\]\nlockfile = true/); + + // CI's mise-action runs `mise install --locked`, which needs every tool locked. + const miseLock = await readFile(path.join(projectDir, "mise.lock"), "utf8"); + for (const tool of ["node", "pnpm"]) { + expect(miseLock).toContain(`[[tools.${tool}]]`); + expect(miseLock).toContain(`[tools.${tool}."platforms.linux-x64"]`); + } + }); + + test.each([ + ["library", ""], + ["library", "--visibility=private"], + ["backend", ""], + ["monorepo", ""], + ])("%s %s: every file passes oxfmt and Biome, so `rr check` starts green with either", (template, flags) => { + const name = `fmt-${template}`; + const r = cli(`init ${name} -t ${template} ${flags} --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + + // oxfmt (`rr plugins add oxc`) also formats Markdown and sorts package.json keys. + const projectDir = path.join(fixture.dir, name); + const fmt = oxfmtCheck(projectDir); + expect(fmt.status, fmt.stdout + fmt.stderr).toBe(0); + + const biome = biomeCheck(projectDir); + expect(biome.status, biome.stdout + biome.stderr).toBe(0); + }); + + test.each(TEMPLATES)("%s: pins every workflow action by SHA and checks out without credentials", async (template) => { + const name = `ci-${template}`; + const r = cli(`init ${name} -t ${template} --no-install --no-git`, { cwd: fixture.dir }); + expect(r.status).toBe(0); + + const workflowsDir = path.join(fixture.dir, name, ".github", "workflows"); + const workflows = await readdir(workflowsDir); + expect(workflows).toContain("ci.yml"); + expect(workflows).toContain("workflow-audit.yml"); + + for (const file of workflows) { + const body = await readFile(path.join(workflowsDir, file), "utf8"); + const actions = body.match(/(?<=uses: )\S+/g) ?? []; + for (const action of actions) { + expect(action, `${file}: ${action}`).toMatch(/@[0-9a-f]{40}$/); + } + const checkouts = actions.filter((action) => action.startsWith("actions/checkout@")).length; + expect(body.match(/persist-credentials: false/g)?.length ?? 0, file).toBe(checkouts); + } }); test("monorepo: ships an empty run-run.config.mts at root and in each workspace", async () => { diff --git a/vland/templates/backend/.github/workflows/ci.yml b/vland/templates/backend/.github/workflows/ci.yml index c4d61628..049783cd 100644 --- a/vland/templates/backend/.github/workflows/ci.yml +++ b/vland/templates/backend/.github/workflows/ci.yml @@ -1,22 +1,44 @@ name: CI on: - push: - branches: [main] pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: + name: Test runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: ๐Ÿ› ๏ธ Setup mise + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - node-version-file: .node-version - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm rr jsc - - run: pnpm rr tsc - - run: pnpm vitest run - - run: pnpm rr pack + install: true + cache: true + + - name: ๐Ÿ“ฆ Install dependencies + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ’… Lint and typecheck + run: pnpm rr check + + - name: ๐Ÿงช Test + run: pnpm test + + - name: ๐Ÿ”จ Build + run: pnpm rr pack diff --git a/vland/templates/backend/.github/workflows/workflow-audit.yml b/vland/templates/backend/.github/workflows/workflow-audit.yml new file mode 100644 index 00000000..49ccf0bc --- /dev/null +++ b/vland/templates/backend/.github/workflows/workflow-audit.yml @@ -0,0 +1,33 @@ +name: Workflow audit + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit-workflows: + name: Audit workflows + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Repo root, not .github/: keeps annotation paths repo-relative. + - name: ๐Ÿ”’ Scan workflows + env: + # Enables zizmor's online audits (impostor commits, known-vulnerable actions). + GH_TOKEN: ${{ github.token }} + run: pipx run --spec zizmor==1.30.1 zizmor --format=github . diff --git a/vland/templates/backend/.node-version b/vland/templates/backend/.node-version deleted file mode 100644 index a45fd52c..00000000 --- a/vland/templates/backend/.node-version +++ /dev/null @@ -1 +0,0 @@ -24 diff --git a/vland/templates/backend/Dockerfile b/vland/templates/backend/Dockerfile index a94044ab..c2451c8c 100644 --- a/vland/templates/backend/Dockerfile +++ b/vland/templates/backend/Dockerfile @@ -1,5 +1,5 @@ ARG NODE_VERSION=24 -ARG PNPM_VERSION=10.6.4 +ARG PNPM_VERSION=10.30.3 FROM node:${NODE_VERSION}-alpine AS base ENV PNPM_HOME=/pnpm @@ -8,7 +8,7 @@ RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate WORKDIR /app FROM base AS deps -COPY package.json pnpm-lock.yaml* ./ +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile FROM base AS build diff --git a/vland/templates/backend/README.md b/vland/templates/backend/README.md index 531fcdb8..0f14437a 100644 --- a/vland/templates/backend/README.md +++ b/vland/templates/backend/README.md @@ -4,12 +4,12 @@ A Node.js backend service built with [Elysia](https://elysiajs.com), [`@elysiajs ## Prerequisites -- Node.js >= 20 (24 recommended; see `.node-version`) -- pnpm 10.x +[mise](https://mise.jdx.dev) 2026.9.16 or later. It installs the Node.js and pnpm versions pinned in `mise.toml` and `mise.lock`, the same ones CI uses. Without mise, you need Node.js 22.18.0 or later, because `rr` loads `run-run.config.mts` with Node's built-in type stripping, and pnpm 10.30.3. ## Getting started ```sh +mise install cp .env.example .env pnpm install pnpm dev @@ -21,22 +21,31 @@ Open โ€” you should get `{ "ok": true }`. This project uses [`rr`](https://github.com/withvariablelab/dx/tree/main/run-run) (the `@rrlab/cli`) as the single entry point for lint, format, type-check, and build. The pre-configured `mise.toml` puts `./node_modules/.bin` on your `PATH`, so `rr` is invokable directly from the project root. +It starts with no plugins. Add the ones you want: + ```sh rr plugins add biome # lint + format (writes biome.json) rr plugins add ts # type-check (writes tsconfig.json) rr plugins add tsdown # bundle (writes tsdown.config.ts) +rr plugins add vitest # tests through `rr test` ``` +Each `rr plugins add` installs the plugin, records it in `run-run.config.mts` and scaffolds its config file, asking first. `--yes` takes the defaults without asking: use it from an agent or a script. `rr plugins remove ` undoes an add. + +For oxlint and oxfmt instead of Biome, run `rr plugins add oxc` in place of `biome`, then narrow it to `oxc({ only: ["lint", "format"] })` in `run-run.config.mts`, so that `ts` keeps the type-check. + +CI runs `rr check`, `pnpm test` and `rr pack`, so it needs `biome` (or `oxc`), `ts` and `tsdown`. + ## Develop -| Command | Purpose | -|---------|---------| -| `pnpm dev` | Run the server with watch mode (`tsx watch`). | -| `pnpm start` | Run the bundled server (`node dist/index.mjs`). | -| `pnpm test` | Run the Vitest suite. | -| `rr jsc` | Lint + format check (add `--fix` to auto-fix). | -| `rr tsc` | TypeScript type check. | -| `rr pack` | Bundle to `dist/index.mjs` via tsdown. | +| Command | Purpose | +| -------------- | ------------------------------------------------------------------ | +| `pnpm dev` | Run the server with `node --watch`, loading `.env` when it exists. | +| `pnpm start` | Run the bundled server (`node dist/index.mjs`). | +| `pnpm test` | Run the Vitest suite. | +| `rr check` | Lint + format check, then type-check. | +| `rr jsc --fix` | Auto-fix lint + format. | +| `rr pack` | Bundle to `dist/index.mjs` via tsdown. | ## Layout diff --git a/vland/templates/backend/mise.lock b/vland/templates/backend/mise.lock new file mode 100644 index 00000000..dd2ec248 --- /dev/null +++ b/vland/templates/backend/mise.lock @@ -0,0 +1,76 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 3 + +[[tools.node]] +version = "24.21.0" +backend = "core:node" +specifiers = ["24"] + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4" +url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a" +url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip" + +[[tools.pnpm]] +version = "10.30.3" +backend = "aqua:pnpm/pnpm" +specifiers = ["10.30.3"] + +[tools.pnpm."platforms.linux-arm64"] +checksum = "sha256:9488f48d6967bfa8f5ebc5c077aae297d2fe25287697b4c8edf991ff945d8af7" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742325" + +[tools.pnpm."platforms.linux-arm64-musl"] +checksum = "sha256:9488f48d6967bfa8f5ebc5c077aae297d2fe25287697b4c8edf991ff945d8af7" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742325" + +[tools.pnpm."platforms.linux-x64"] +checksum = "sha256:5dc971e801610471b5cd6729ba289531be6695bd50d5e833fe22b18896b13a06" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742326" + +[tools.pnpm."platforms.linux-x64-musl"] +checksum = "sha256:5dc971e801610471b5cd6729ba289531be6695bd50d5e833fe22b18896b13a06" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742326" + +[tools.pnpm."platforms.macos-arm64"] +checksum = "sha256:ccfb6e54fe29d13715e2fe418ef0374cde5a7f68b95f5ed5da5f564886af9d17" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-macos-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742324" + +[tools.pnpm."platforms.macos-x64"] +checksum = "sha256:d9be0fd3989c09f5abf7e52f10d8d0294f757af61bbbd59727d607c85205a788" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-macos-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742327" + +[tools.pnpm."platforms.windows-x64"] +checksum = "sha256:de8337a864056472542b951ab23ad54149cc63127bb2faf3175b7902b764f188" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-win-x64.exe" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742329" diff --git a/vland/templates/backend/mise.toml b/vland/templates/backend/mise.toml index 115c747b..edbcd7d1 100644 --- a/vland/templates/backend/mise.toml +++ b/vland/templates/backend/mise.toml @@ -1,6 +1,12 @@ +# mise.lock is lockfile revision 3, which mise reads from 2026.9.16. +min_version = "2026.9.16" + [env] _.path = ["{{config_root}}/node_modules/.bin"] [tools] node = "24" -pnpm = "10.6.4" +pnpm = "10.30.3" + +[settings] +lockfile = true diff --git a/vland/templates/backend/package.json b/vland/templates/backend/package.json index d0b3e736..17f32185 100644 --- a/vland/templates/backend/package.json +++ b/vland/templates/backend/package.json @@ -10,11 +10,8 @@ "#src/*": "./src/*", "#test/*": "./test/*" }, - "engines": { - "node": ">=20.0.0" - }, "scripts": { - "dev": "tsx watch src/index.ts", + "dev": "node --watch --env-file-if-exists=.env src/index.ts", "start": "node dist/index.mjs", "test": "vitest run" }, @@ -25,11 +22,13 @@ "zod": "^4.4.3" }, "devDependencies": { - "@rrlab/cli": "^0.1.0", - "@types/node": "latest", + "@rrlab/cli": "^1.3.4", + "@types/node": "^24.0.0", "lefthook": "2.1.1", - "tsx": "^4.21.0", "vitest": "^4.0.0" }, - "packageManager": "pnpm@10.6.4" + "engines": { + "node": ">=22.18.0" + }, + "packageManager": "pnpm@10.30.3" } diff --git a/vland/templates/backend/pnpm-workspace.yaml b/vland/templates/backend/pnpm-workspace.yaml new file mode 100644 index 00000000..7630bb68 --- /dev/null +++ b/vland/templates/backend/pnpm-workspace.yaml @@ -0,0 +1,12 @@ +onlyBuiltDependencies: + - lefthook + +# lefthook's postinstall installs the git hooks, so it has to run on every install: +# with the side-effects cache on, a lefthook already built for another project skips it. +sideEffectsCache: false + +# Supply-chain cooldown: malicious releases are usually pulled within 24h; pattern excludes need pnpm >= 10.17. +minimumReleaseAge: 1440 +minimumReleaseAgeExclude: + - "@variablelab/*" + - "@rrlab/*" diff --git a/vland/templates/library/.changeset/config.json b/vland/templates/library/.changeset/config.json index 60f9d233..db494b18 100644 --- a/vland/templates/library/.changeset/config.json +++ b/vland/templates/library/.changeset/config.json @@ -1,11 +1,12 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "{{projectName}}/{{projectName}}" }], - "commit": false, + "$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "{{githubOwner}}/{{projectName}}" }], + "commit": true, "fixed": [], "linked": [], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [], + "format": false } diff --git a/vland/templates/library/.github/workflows/ci.yml b/vland/templates/library/.github/workflows/ci.yml index c4d61628..049783cd 100644 --- a/vland/templates/library/.github/workflows/ci.yml +++ b/vland/templates/library/.github/workflows/ci.yml @@ -1,22 +1,44 @@ name: CI on: - push: - branches: [main] pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: + name: Test runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v4 + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: ๐Ÿ› ๏ธ Setup mise + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - node-version-file: .node-version - cache: pnpm - - run: pnpm install --frozen-lockfile - - run: pnpm rr jsc - - run: pnpm rr tsc - - run: pnpm vitest run - - run: pnpm rr pack + install: true + cache: true + + - name: ๐Ÿ“ฆ Install dependencies + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ’… Lint and typecheck + run: pnpm rr check + + - name: ๐Ÿงช Test + run: pnpm test + + - name: ๐Ÿ”จ Build + run: pnpm rr pack diff --git a/vland/templates/library/.github/workflows/release.yml b/vland/templates/library/.github/workflows/release.yml index 296cf8ba..c4e3f431 100644 --- a/vland/templates/library/.github/workflows/release.yml +++ b/vland/templates/library/.github/workflows/release.yml @@ -2,40 +2,64 @@ name: Release on: push: - branches: [main] - -concurrency: ${{ github.workflow }}-${{ github.ref }} + branches: + - main permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: release: + name: Release runs-on: ubuntu-latest + timeout-minutes: 15 permissions: - contents: write - pull-requests: write id-token: write # npm trusted publishing (OIDC) + contents: read steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + + - name: ๐Ÿ› ๏ธ Setup mise + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - node-version-file: .node-version - cache: pnpm - registry-url: https://registry.npmjs.org - - run: pnpm install --frozen-lockfile - - run: pnpm rr pack - - # No NPM_TOKEN: npm trusted publishing over OIDC, set up once per package (see README). - - name: Release or version PR - uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + install: true + cache: true + + - name: ๐Ÿ“ฆ Install dependencies + run: pnpm install --frozen-lockfile + + # vland-bot, not GITHUB_TOKEN, so CI runs on the version PR. + - name: ๐Ÿ”‘ Mint vland-bot token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.VLAND_BOT_CLIENT_ID }} + private-key: ${{ secrets.VLAND_BOT_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: ๐Ÿง‘โ€๐Ÿ’ป Configure git user + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" + git config user.name "${APP_SLUG}[bot]" + git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com" + + # No NPM_TOKEN: npm trusted publishing over OIDC (npm >= 11.5.1, bundled with mise's node 24). + - name: ๐Ÿš€ Release or version PR + uses: changesets/action@ae32849d5ba541f9ae29e40e22a623bc13562f51 # v2.1.2 with: - publish: pnpm changeset publish - commit: "chore: release" - title: "chore: release" - github-token: ${{ secrets.GITHUB_TOKEN }} - commitMode: github-api + commit-message: "chore: update versions" + pr-title: "chore: update versions" + publish-script: pnpm changeset publish + github-token: ${{ steps.app-token.outputs.token }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LEFTHOOK: 0 diff --git a/vland/templates/library/.github/workflows/workflow-audit.yml b/vland/templates/library/.github/workflows/workflow-audit.yml new file mode 100644 index 00000000..49ccf0bc --- /dev/null +++ b/vland/templates/library/.github/workflows/workflow-audit.yml @@ -0,0 +1,33 @@ +name: Workflow audit + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit-workflows: + name: Audit workflows + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Repo root, not .github/: keeps annotation paths repo-relative. + - name: ๐Ÿ”’ Scan workflows + env: + # Enables zizmor's online audits (impostor commits, known-vulnerable actions). + GH_TOKEN: ${{ github.token }} + run: pipx run --spec zizmor==1.30.1 zizmor --format=github . diff --git a/vland/templates/library/.node-version b/vland/templates/library/.node-version deleted file mode 100644 index a45fd52c..00000000 --- a/vland/templates/library/.node-version +++ /dev/null @@ -1 +0,0 @@ -24 diff --git a/vland/templates/library/README.md b/vland/templates/library/README.md index f0827c8b..a3113a47 100644 --- a/vland/templates/library/README.md +++ b/vland/templates/library/README.md @@ -5,38 +5,52 @@ A standalone TypeScript library scaffolded with [`@variablelab/vland`](https://g ## Install ```sh -pnpm add {{projectName}} +pnpm add {{packageName}} ``` ## Usage ```ts -import { greet } from "{{projectName}}"; +import { greet } from "{{packageName}}"; greet("vland"); ``` +## Prerequisites + +[mise](https://mise.jdx.dev) 2026.9.16 or later. It installs the Node.js and pnpm versions pinned in `mise.toml` and `mise.lock`, the same ones CI uses. Without mise, you need Node.js 22.18.0 or later, because `rr` loads `run-run.config.mts` with Node's built-in type stripping, and pnpm 10.30.3. + +```sh +mise install +pnpm install +``` + ## Set up tooling This project uses [`rr`](https://github.com/withvariablelab/dx/tree/main/run-run) (the `@rrlab/cli`) as the single entry point for lint, format, type-check, and build. The pre-configured `mise.toml` puts `./node_modules/.bin` on your `PATH`, so `rr` is invokable directly from the project root. +It starts with no plugins. Add the ones you want: + ```sh -pnpm install # also pulls in @rrlab/cli rr plugins add biome # lint + format (writes biome.json) rr plugins add ts # type-check (writes tsconfig.json) rr plugins add tsdown # bundle (writes tsdown.config.ts) +rr plugins add vitest # tests through `rr test` ``` -Each `rr plugins add` records the plugin in `run-run.config.mts` and scaffolds its config file. Re-run any time you want a different toolset. +Each `rr plugins add` installs the plugin, records it in `run-run.config.mts` and scaffolds its config file, asking first. `--yes` takes the defaults without asking: use it from an agent or a script. `rr plugins remove ` undoes an add. + +For oxlint and oxfmt instead of Biome, run `rr plugins add oxc` in place of `biome`, then narrow it to `oxc({ only: ["lint", "format"] })` in `run-run.config.mts`, so that `ts` keeps the type-check. + +CI runs `rr check`, `pnpm test` and `rr pack`, so it needs `biome` (or `oxc`), `ts` and `tsdown`. ## Develop ```sh -rr jsc # lint + format check -rr jsc --fix # auto-fix -rr tsc # type-check +rr check # lint + format check, then type-check +rr jsc --fix # auto-fix lint + format rr pack # build to dist/ -vitest run # tests +pnpm test # tests ``` ## Release @@ -44,11 +58,11 @@ vitest run # tests This package uses [Changesets](https://github.com/changesets/changesets). As a developer you only need to describe the change; the publish step is run by CI. ```sh -pnpm changeset # describe a change +pnpm changeset # describe a change, and commit it pnpm changeset version # bump versions + changelog (locally, when needed) ``` -CI invokes `pnpm changeset publish` on `main`. `prepublishOnly` runs `rr pack` automatically before publish. +On every push to `main`, the release workflow opens a "chore: update versions" PR with the pending changesets, or publishes with `pnpm changeset publish` once that PR is merged. `prepublishOnly` runs `rr pack` automatically before publish. The workflow acts as vland-bot, not `GITHUB_TOKEN`, so CI runs on that PR: the repository needs the `VLAND_BOT_CLIENT_ID` variable and the `VLAND_BOT_PRIVATE_KEY` secret. CI publishes with [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC), so there is no `NPM_TOKEN` secret. Packages marked `"private": true` are never published. One-time setup for a public package: diff --git a/vland/templates/library/mise.lock b/vland/templates/library/mise.lock new file mode 100644 index 00000000..dd2ec248 --- /dev/null +++ b/vland/templates/library/mise.lock @@ -0,0 +1,76 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 3 + +[[tools.node]] +version = "24.21.0" +backend = "core:node" +specifiers = ["24"] + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4" +url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a" +url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip" + +[[tools.pnpm]] +version = "10.30.3" +backend = "aqua:pnpm/pnpm" +specifiers = ["10.30.3"] + +[tools.pnpm."platforms.linux-arm64"] +checksum = "sha256:9488f48d6967bfa8f5ebc5c077aae297d2fe25287697b4c8edf991ff945d8af7" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742325" + +[tools.pnpm."platforms.linux-arm64-musl"] +checksum = "sha256:9488f48d6967bfa8f5ebc5c077aae297d2fe25287697b4c8edf991ff945d8af7" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742325" + +[tools.pnpm."platforms.linux-x64"] +checksum = "sha256:5dc971e801610471b5cd6729ba289531be6695bd50d5e833fe22b18896b13a06" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742326" + +[tools.pnpm."platforms.linux-x64-musl"] +checksum = "sha256:5dc971e801610471b5cd6729ba289531be6695bd50d5e833fe22b18896b13a06" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742326" + +[tools.pnpm."platforms.macos-arm64"] +checksum = "sha256:ccfb6e54fe29d13715e2fe418ef0374cde5a7f68b95f5ed5da5f564886af9d17" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-macos-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742324" + +[tools.pnpm."platforms.macos-x64"] +checksum = "sha256:d9be0fd3989c09f5abf7e52f10d8d0294f757af61bbbd59727d607c85205a788" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-macos-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742327" + +[tools.pnpm."platforms.windows-x64"] +checksum = "sha256:de8337a864056472542b951ab23ad54149cc63127bb2faf3175b7902b764f188" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-win-x64.exe" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742329" diff --git a/vland/templates/library/mise.toml b/vland/templates/library/mise.toml index 115c747b..edbcd7d1 100644 --- a/vland/templates/library/mise.toml +++ b/vland/templates/library/mise.toml @@ -1,6 +1,12 @@ +# mise.lock is lockfile revision 3, which mise reads from 2026.9.16. +min_version = "2026.9.16" + [env] _.path = ["{{config_root}}/node_modules/.bin"] [tools] node = "24" -pnpm = "10.6.4" +pnpm = "10.30.3" + +[settings] +lockfile = true diff --git a/vland/templates/library/package.json b/vland/templates/library/package.json index 790c7042..473ffeeb 100644 --- a/vland/templates/library/package.json +++ b/vland/templates/library/package.json @@ -4,34 +4,40 @@ "description": "", "license": "MIT", "author": "{{author}}", + "files": [ + "dist", + "src", + "!src/**/__tests__", + "!src/**/*.test.*", + "tsconfig.json" + ], "type": "module", "exports": { ".": "./src/index.ts" }, "publishConfig": { - "access": "public", "exports": { ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" } - } - }, - "files": ["dist", "src", "!src/**/__tests__", "!src/**/*.test.*", "tsconfig.json"], - "engines": { - "node": ">=20.0.0" + }, + "access": "public" }, "scripts": { "prepublishOnly": "rr pack", "test": "vitest run" }, "devDependencies": { - "@changesets/changelog-github": "0.5.2", - "@changesets/cli": "2.29.8", - "@rrlab/cli": "^0.1.0", - "@types/node": "latest", + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.3", + "@rrlab/cli": "^1.3.4", + "@types/node": "^24.0.0", "lefthook": "2.1.1", "vitest": "^4.0.0" }, - "packageManager": "pnpm@10.6.4" + "engines": { + "node": ">=22.18.0" + }, + "packageManager": "pnpm@10.30.3" } diff --git a/vland/templates/library/pnpm-workspace.yaml b/vland/templates/library/pnpm-workspace.yaml new file mode 100644 index 00000000..7630bb68 --- /dev/null +++ b/vland/templates/library/pnpm-workspace.yaml @@ -0,0 +1,12 @@ +onlyBuiltDependencies: + - lefthook + +# lefthook's postinstall installs the git hooks, so it has to run on every install: +# with the side-effects cache on, a lefthook already built for another project skips it. +sideEffectsCache: false + +# Supply-chain cooldown: malicious releases are usually pulled within 24h; pattern excludes need pnpm >= 10.17. +minimumReleaseAge: 1440 +minimumReleaseAgeExclude: + - "@variablelab/*" + - "@rrlab/*" diff --git a/vland/templates/monorepo/.changeset/config.json b/vland/templates/monorepo/.changeset/config.json index 54c13150..0d9fa09e 100644 --- a/vland/templates/monorepo/.changeset/config.json +++ b/vland/templates/monorepo/.changeset/config.json @@ -1,11 +1,12 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "{{projectName}}/{{projectName}}" }], - "commit": false, + "$schema": "https://unpkg.com/@changesets/config@4.0.1/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "{{githubOwner}}/{{projectName}}" }], + "commit": true, "fixed": [], "linked": [], "access": "restricted", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": [], + "format": false } diff --git a/vland/templates/monorepo/.github/workflows/ci.yml b/vland/templates/monorepo/.github/workflows/ci.yml new file mode 100644 index 00000000..b965084a --- /dev/null +++ b/vland/templates/monorepo/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # actions/checkout leaves no local `main`: --affected compares against the remote one. + TURBO_SCM_BASE: origin/main + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # --affected needs the history back to the merge base. + fetch-depth: 0 + persist-credentials: false + + - name: ๐Ÿ› ๏ธ Setup mise + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + install: true + cache: true + + - name: ๐Ÿ“ฆ Install dependencies + run: pnpm install --frozen-lockfile + + - name: ๐Ÿ’… Lint and typecheck + run: pnpm rr check + + - name: ๐Ÿงช Test + run: pnpm test --affected + + - name: ๐Ÿ”จ Build + run: pnpm build --affected diff --git a/vland/templates/monorepo/.github/workflows/workflow-audit.yml b/vland/templates/monorepo/.github/workflows/workflow-audit.yml new file mode 100644 index 00000000..49ccf0bc --- /dev/null +++ b/vland/templates/monorepo/.github/workflows/workflow-audit.yml @@ -0,0 +1,33 @@ +name: Workflow audit + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit-workflows: + name: Audit workflows + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Repo root, not .github/: keeps annotation paths repo-relative. + - name: ๐Ÿ”’ Scan workflows + env: + # Enables zizmor's online audits (impostor commits, known-vulnerable actions). + GH_TOKEN: ${{ github.token }} + run: pipx run --spec zizmor==1.30.1 zizmor --format=github . diff --git a/vland/templates/monorepo/.node-version b/vland/templates/monorepo/.node-version deleted file mode 100644 index a45fd52c..00000000 --- a/vland/templates/monorepo/.node-version +++ /dev/null @@ -1 +0,0 @@ -24 diff --git a/vland/templates/monorepo/README.md b/vland/templates/monorepo/README.md index 8887eb98..6aa27a9b 100644 --- a/vland/templates/monorepo/README.md +++ b/vland/templates/monorepo/README.md @@ -6,25 +6,25 @@ variableland-flavoured Node.js monorepo: pnpm workspaces + Turborepo + Vitest. ### Apps -| App | Description | -|-----|-------------| +| App | Description | +| ------------------------ | --------------------------------------------- | | [`apps/api`](./apps/api) | Elysia (`@elysiajs/node`) backend with evlog. | -| [`apps/web`](./apps/web) | Minimal Vite + React SPA. | +| [`apps/web`](./apps/web) | Minimal Vite + React SPA. | ### Packages -| Package | Description | -|---------|-------------| +| Package | Description | +| ------------------------------------ | ---------------------------------------------------------- | | [`packages/types`](./packages/types) | Shared zod schemas, consumed by `apps/api` and `apps/web`. | ## Prerequisites -- Node.js >= 20 (24 recommended; see `.node-version`) -- pnpm 10.x +[mise](https://mise.jdx.dev) 2026.9.16 or later. It installs the Node.js and pnpm versions pinned in `mise.toml` and `mise.lock`, the same ones CI uses. Without mise, you need Node.js 22.18.0 or later, because `rr` loads `run-run.config.mts` with Node's built-in type stripping, and pnpm 10.30.3. ## Getting started ```sh +mise install pnpm install pnpm dev ``` @@ -33,30 +33,37 @@ pnpm dev This project uses [`rr`](https://github.com/withvariablelab/dx/tree/main/run-run) (the `@rrlab/cli`) as the single entry point for lint, format, type-check, and build. The pre-configured `mise.toml` puts `./node_modules/.bin` on your `PATH`, so `rr` is invokable directly from anywhere inside the repo. -Add the plugins you want โ€” each one writes its own config file and registers itself in the workspace's `run-run.config.mts`: +It starts with no plugins. Add the ones you want: each one installs itself, writes its own config file and registers itself in the `run-run.config.mts` of the folder you run it in. ```sh # Workspace-wide formatting + linting (lives at root) rr plugins add biome # Type-checking iterates every workspace when invoked at root. -# Install ts-plugin at root (decline scaffolding root tsconfig.json), +# Install ts-plugin at root (decline scaffolding root tsconfig.json; with --yes +# it's created, and rr tsc still checks the workspaces, not the root), # then install it inside each workspace that needs its own tsconfig. rr plugins add ts # at root cd apps/api && pnpm rr plugins add ts && pnpm rr plugins add tsdown && cd ../.. -cd apps/web && pnpm rr plugins add ts && cd ../.. +cd apps/web && pnpm rr plugins add ts && cd ../.. # the "React app" preset cd packages/types && pnpm rr plugins add ts && pnpm rr plugins add tsdown && cd ../.. ``` Inside a workspace folder, prefix with `pnpm` (`pnpm rr `) โ€” pnpm resolves `rr` from the workspace root's `node_modules/.bin`. +Each `rr plugins add` asks before writing a config file. `--yes` takes the defaults without asking: use it from an agent or a script. `rr plugins add vitest` runs the tests through `rr test`, and `rr plugins remove ` undoes an add. + +For oxlint and oxfmt instead of Biome, run `rr plugins add oxc` in place of `biome`, then narrow it to `oxc({ only: ["lint", "format"] })` in `run-run.config.mts`, so that `ts` keeps the type-check. + +CI runs `rr check` at the root, then `pnpm test` and `pnpm build` for the workspaces a PR affects. + ## Develop -| Command | Purpose | -|---------|---------| -| `pnpm dev` | `turbo run dev` across every app. | -| `pnpm build` | `turbo run build`. | -| `pnpm test` | `turbo run test` across every workspace with tests. | -| `rr jsc` (root) | Lint + format check on the whole repo. | -| `rr tsc` (root) | Type-check every workspace that has a `tsconfig.json`. | -| `rr pack` (per workspace) | Bundle the workspace's `src/index.ts` via tsdown. | +| Command | Purpose | +| ------------------------- | -------------------------------------------------------------------------------------------------- | +| `pnpm dev` | `turbo run dev` across every app. | +| `pnpm build` | `turbo run build`. | +| `pnpm test` | `turbo run test` across every workspace with tests. | +| `rr check` (root) | Lint + format check on the whole repo, then type-check every workspace that has a `tsconfig.json`. | +| `rr jsc --fix` (root) | Auto-fix lint + format on the whole repo. | +| `rr pack` (per workspace) | Bundle the workspace's `src/index.ts` via tsdown. | diff --git a/vland/templates/monorepo/apps/api/README.md b/vland/templates/monorepo/apps/api/README.md index 9d7a3ab3..462643fa 100644 --- a/vland/templates/monorepo/apps/api/README.md +++ b/vland/templates/monorepo/apps/api/README.md @@ -1,8 +1,8 @@ -# @{{projectName}}/api +# {{projectName}}-api Elysia (`@elysiajs/node`) backend service for the {{projectName}} monorepo. ```sh cp .env.example .env -pnpm --filter @{{projectName}}/api dev +pnpm --filter {{projectName}}-api dev ``` diff --git a/vland/templates/monorepo/apps/api/package.json b/vland/templates/monorepo/apps/api/package.json index 60806295..52e4a558 100644 --- a/vland/templates/monorepo/apps/api/package.json +++ b/vland/templates/monorepo/apps/api/package.json @@ -9,20 +9,19 @@ "#test/*": "./test/*" }, "scripts": { - "dev": "tsx watch src/index.ts", + "dev": "node --watch --env-file-if-exists=.env src/index.ts", "start": "node dist/index.mjs", "test": "vitest run" }, "dependencies": { - "@{{projectName}}/types": "workspace:*", "@elysiajs/node": "^1.4.5", + "@{{projectName}}/types": "workspace:*", "elysia": "^1.4.28", "evlog": "^2.17.0", "zod": "^4.4.3" }, "devDependencies": { - "@rrlab/cli": "^0.1.0", - "tsx": "^4.21.0", + "@rrlab/cli": "^1.3.4", "vitest": "^4.0.0" } } diff --git a/vland/templates/monorepo/apps/api/src/routes/health.ts b/vland/templates/monorepo/apps/api/src/routes/health.ts index 08c6afaa..1cff8b3e 100644 --- a/vland/templates/monorepo/apps/api/src/routes/health.ts +++ b/vland/templates/monorepo/apps/api/src/routes/health.ts @@ -1,4 +1,6 @@ import { HealthSchema } from "@{{projectName}}/types"; import { Elysia } from "elysia"; -export const health = new Elysia().get("/health", () => HealthSchema.parse({ ok: true, uptime: process.uptime() })); +export const health = new Elysia().get("/health", () => { + return HealthSchema.parse({ ok: true, uptime: process.uptime() }); +}); diff --git a/vland/templates/monorepo/apps/web/README.md b/vland/templates/monorepo/apps/web/README.md index adab491e..a3a3e636 100644 --- a/vland/templates/monorepo/apps/web/README.md +++ b/vland/templates/monorepo/apps/web/README.md @@ -1,7 +1,7 @@ -# @{{projectName}}/web +# {{projectName}}-web Minimal Vite + React + TypeScript SPA. ```sh -pnpm --filter @{{projectName}}/web dev +pnpm --filter {{projectName}}-web dev ``` diff --git a/vland/templates/monorepo/apps/web/package.json b/vland/templates/monorepo/apps/web/package.json index 99f0ea47..06c407cf 100644 --- a/vland/templates/monorepo/apps/web/package.json +++ b/vland/templates/monorepo/apps/web/package.json @@ -15,7 +15,7 @@ "react-dom": "^19.2.6" }, "devDependencies": { - "@rrlab/cli": "^0.1.0", + "@rrlab/cli": "^1.3.4", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^6.0.1", diff --git a/vland/templates/monorepo/apps/web/src/App.tsx b/vland/templates/monorepo/apps/web/src/App.tsx index a052104c..8bb2f777 100644 --- a/vland/templates/monorepo/apps/web/src/App.tsx +++ b/vland/templates/monorepo/apps/web/src/App.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from "react"; import { type Health, HealthSchema } from "@{{projectName}}/types"; +import { useEffect, useState } from "react"; export function App() { const [health, setHealth] = useState(null); diff --git a/vland/templates/monorepo/mise.lock b/vland/templates/monorepo/mise.lock new file mode 100644 index 00000000..dd2ec248 --- /dev/null +++ b/vland/templates/monorepo/mise.lock @@ -0,0 +1,76 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 3 + +[[tools.node]] +version = "24.21.0" +backend = "core:node" +specifiers = ["24"] + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4" +url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a" +url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541" +url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip" + +[[tools.pnpm]] +version = "10.30.3" +backend = "aqua:pnpm/pnpm" +specifiers = ["10.30.3"] + +[tools.pnpm."platforms.linux-arm64"] +checksum = "sha256:9488f48d6967bfa8f5ebc5c077aae297d2fe25287697b4c8edf991ff945d8af7" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742325" + +[tools.pnpm."platforms.linux-arm64-musl"] +checksum = "sha256:9488f48d6967bfa8f5ebc5c077aae297d2fe25287697b4c8edf991ff945d8af7" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742325" + +[tools.pnpm."platforms.linux-x64"] +checksum = "sha256:5dc971e801610471b5cd6729ba289531be6695bd50d5e833fe22b18896b13a06" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742326" + +[tools.pnpm."platforms.linux-x64-musl"] +checksum = "sha256:5dc971e801610471b5cd6729ba289531be6695bd50d5e833fe22b18896b13a06" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-linux-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742326" + +[tools.pnpm."platforms.macos-arm64"] +checksum = "sha256:ccfb6e54fe29d13715e2fe418ef0374cde5a7f68b95f5ed5da5f564886af9d17" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-macos-arm64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742324" + +[tools.pnpm."platforms.macos-x64"] +checksum = "sha256:d9be0fd3989c09f5abf7e52f10d8d0294f757af61bbbd59727d607c85205a788" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-macos-x64" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742327" + +[tools.pnpm."platforms.windows-x64"] +checksum = "sha256:de8337a864056472542b951ab23ad54149cc63127bb2faf3175b7902b764f188" +url = "https://github.com/pnpm/pnpm/releases/download/v10.30.3/pnpm-win-x64.exe" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/362742329" diff --git a/vland/templates/monorepo/mise.toml b/vland/templates/monorepo/mise.toml index 115c747b..edbcd7d1 100644 --- a/vland/templates/monorepo/mise.toml +++ b/vland/templates/monorepo/mise.toml @@ -1,6 +1,12 @@ +# mise.lock is lockfile revision 3, which mise reads from 2026.9.16. +min_version = "2026.9.16" + [env] _.path = ["{{config_root}}/node_modules/.bin"] [tools] node = "24" -pnpm = "10.6.4" +pnpm = "10.30.3" + +[settings] +lockfile = true diff --git a/vland/templates/monorepo/package.json b/vland/templates/monorepo/package.json index b581d1d9..70a0c1a1 100644 --- a/vland/templates/monorepo/package.json +++ b/vland/templates/monorepo/package.json @@ -6,22 +6,22 @@ "license": "MIT", "author": "{{author}}", "type": "module", - "engines": { - "node": ">=20.0.0" - }, "scripts": { "dev": "turbo run dev", "build": "turbo run build", "test": "turbo run test" }, "devDependencies": { - "@changesets/changelog-github": "0.5.2", - "@changesets/cli": "2.29.8", - "@rrlab/cli": "^0.1.0", - "@types/node": "latest", + "@changesets/changelog-github": "1.0.1", + "@changesets/cli": "3.0.3", + "@rrlab/cli": "^1.3.4", + "@types/node": "^24.0.0", "lefthook": "2.1.1", - "turbo": "2.8.12", + "turbo": "2.11.9", "vitest": "^4.0.0" }, - "packageManager": "pnpm@10.6.4" + "engines": { + "node": ">=22.18.0" + }, + "packageManager": "pnpm@10.30.3" } diff --git a/vland/templates/monorepo/packages/types/package.json b/vland/templates/monorepo/packages/types/package.json index b94edd78..8ecf7e74 100644 --- a/vland/templates/monorepo/packages/types/package.json +++ b/vland/templates/monorepo/packages/types/package.json @@ -3,15 +3,19 @@ "version": "0.0.0", "private": true, "license": "MIT", + "files": [ + "src", + "!src/**/__tests__", + "!src/**/*.test.*" + ], "type": "module", "exports": { ".": "./src/index.ts" }, - "files": ["src", "!src/**/__tests__", "!src/**/*.test.*"], "dependencies": { "zod": "^4.4.3" }, "devDependencies": { - "@rrlab/cli": "^0.1.0" + "@rrlab/cli": "^1.3.4" } } diff --git a/vland/templates/monorepo/pnpm-workspace.yaml b/vland/templates/monorepo/pnpm-workspace.yaml index c7d76e81..80432015 100644 --- a/vland/templates/monorepo/pnpm-workspace.yaml +++ b/vland/templates/monorepo/pnpm-workspace.yaml @@ -4,3 +4,13 @@ packages: onlyBuiltDependencies: - "@biomejs/biome" - lefthook + +# lefthook's postinstall installs the git hooks, so it has to run on every install: +# with the side-effects cache on, a lefthook already built for another project skips it. +sideEffectsCache: false + +# Supply-chain cooldown: malicious releases are usually pulled within 24h; pattern excludes need pnpm >= 10.17. +minimumReleaseAge: 1440 +minimumReleaseAgeExclude: + - "@variablelab/*" + - "@rrlab/*"