From 148279c4dce979a4048eac80cfce08698f1c1d24 Mon Sep 17 00:00:00 2001 From: Atila Fassina Date: Tue, 6 Oct 2026 18:12:39 +0200 Subject: [PATCH] feat: let apps init render the package manager in scripts The build, prebuild, and predev scripts hardcoded `pnpm run`, so the Databricks CLI had to parse and rewrite shell commands in package.json when a user picked npm. Use the same `or .packageManager "pnpm"` expression as app.yaml.tmpl and README.md.tmpl so `databricks apps init` renders the selected manager directly. CLIs that don't pass packageManager still get pnpm. package.json is parsed as JSON before rendering (by the CLI and by our tooling), so the expression uses a backtick string literal instead of double quotes. The smoke fixture now renders the placeholder instead of rewriting `pnpm run` to `npm run`, and a new test fails if a template script hardcodes a package manager. Co-authored-by: Isaac Signed-off-by: Atila Fassina --- template/package.json | 6 +++--- tools/template-artifacts.test.ts | 17 ++++++++++++++++- tools/template-smoke-runner.ts | 24 ++++++++++++------------ 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/template/package.json b/template/package.json index e54659479..e9181b64c 100644 --- a/template/package.json +++ b/template/package.json @@ -9,7 +9,7 @@ "dev": "NODE_ENV=development tsx watch --tsconfig ./tsconfig.server.json --env-file-if-exists=./.env ./server/server.ts", "build:client": "tsc -b tsconfig.client.json && vite build --config client/vite.config.ts", "build:server": "tsc -b tsconfig.server.json && tsdown -c tsdown.server.config.ts", - "build": "pnpm run build:server && pnpm run build:client", + "build": "{{or .packageManager `pnpm`}} run build:server && {{or .packageManager `pnpm`}} run build:client", "typecheck": "tsc -b tsconfig.server.json && tsc -b tsconfig.client.json", "lint": "eslint .", "lint:fix": "eslint . --fix", @@ -18,8 +18,8 @@ "format:fix": "prettier --write .", "test": "vitest run", "clean": "rm -rf client/dist dist build node_modules", - "prebuild": "pnpm run sync && appkit generate-types --wait", - "predev": "pnpm run sync && pnpm run typegen", + "prebuild": "{{or .packageManager `pnpm`}} run sync && appkit generate-types --wait", + "predev": "{{or .packageManager `pnpm`}} run sync && {{or .packageManager `pnpm`}} run typegen", "sync": "appkit plugin sync --write --silent", "typegen": "appkit generate-types", "setup": "appkit setup --write" diff --git a/tools/template-artifacts.test.ts b/tools/template-artifacts.test.ts index 8732bb0e9..9f6075468 100644 --- a/tools/template-artifacts.test.ts +++ b/tools/template-artifacts.test.ts @@ -16,7 +16,10 @@ import { parse } from "yaml"; import { checkTemplateArtifact } from "./check-template-artifact"; import { preservePackageManagerArtifacts } from "./template-artifacts"; -import { selectSmokePackageManager } from "./template-smoke-runner"; +import { + PACKAGE_MANAGER_PLACEHOLDER, + selectSmokePackageManager, +} from "./template-smoke-runner"; const root = resolve(import.meta.dirname, ".."); const temporaryDirectories: string[] = []; @@ -67,6 +70,18 @@ test("static variants retain both package managers for the final init", () => { } }); +test("template scripts invoke the package manager selected by apps init", () => { + const { scripts } = JSON.parse( + readFileSync(join(root, "template/package.json"), "utf-8"), + ); + for (const script of Object.values(scripts)) { + // A hardcoded manager would make npm apps shell out to pnpm. + expect(script.replaceAll(PACKAGE_MANAGER_PLACEHOLDER, "")).not.toMatch( + /(^|[;&|(]\s*)(npm|pnpm|yarn|bun)\s/, + ); + } +}); + test.each(["npm", "pnpm"] as const)( "%s prebuild runs sync and forwards --wait to AppKit", (manager) => { diff --git a/tools/template-smoke-runner.ts b/tools/template-smoke-runner.ts index 2ea8bb031..92766f03f 100644 --- a/tools/template-smoke-runner.ts +++ b/tools/template-smoke-runner.ts @@ -17,26 +17,26 @@ import { parse } from "yaml"; export type PackageManager = "npm" | "pnpm"; +/** Go template expression `databricks apps init` renders to the selected package manager. */ +export const PACKAGE_MANAGER_PLACEHOLDER = "{{or .packageManager `pnpm`}}"; + /** Test fixture shaping; end-user package-manager selection belongs to the CLI. */ export function selectSmokePackageManager( appDir: string, pm: PackageManager, ): void { - if (pm === "pnpm") { - rmSync(join(appDir, "package-lock.json"), { force: true }); - return; - } - for (const file of ["pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"]) { - rmSync(join(appDir, file), { force: true }); - } const packagePath = join(appDir, "package.json"); const pkg = JSON.parse(readFileSync(packagePath, "utf-8")); - pkg.packageManager = `npm@${execFileSync("npm", ["--version"], { encoding: "utf-8" }).trim()}`; for (const [name, script] of Object.entries(pkg.scripts)) { - pkg.scripts[name] = script - .split(" && ") - .map((command) => command.replace(/^pnpm run /, "npm run ")) - .join(" && "); + pkg.scripts[name] = script.replaceAll(PACKAGE_MANAGER_PLACEHOLDER, pm); + } + if (pm === "pnpm") { + rmSync(join(appDir, "package-lock.json"), { force: true }); + } else { + for (const file of ["pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc"]) { + rmSync(join(appDir, file), { force: true }); + } + pkg.packageManager = `npm@${execFileSync("npm", ["--version"], { encoding: "utf-8" }).trim()}`; } writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); }