Files
Oleg Isonen a8c1726418 fix: stabilize CLI bundle contract after publish (#6293)
## Summary

- bundle Zod into the published CLI because `bundleVersion` derives from
Zod schema internals
- bundle `@emotion/hash`, which produces the contract identifier
- prevent compatible dependency updates at install time from changing an
already-published CLI's contract
- add regression coverage for the CLI bundling boundary

## Root cause

`webstudio@0.294.0` externalized `zod` and declared `^4.4.3`. The
Builder was built with the lockfile version, while a fresh `npx
webstudio@latest` installation resolved Zod 4.5.4. Because the project
bundle contract is calculated at runtime from Zod definitions, the
Builder expected `bundle-1mym82p` while the freshly installed CLI sent
`bundle-8e39e9` despite both being the same Webstudio release.

## Packaged CLI reproduction

Tested the CLI built from the parent commit and this branch in the same
isolated `webstudio@0.294.0` installation with Zod 4.5.4. Both builds
executed the real local `webstudio sync` command against the same
capture server.

- Before:
`{"label":"before","zod":"4.5.4","bundleVersion":"bundle-8e39e9"}`
- After:
`{"label":"after","zod":"4.5.4","bundleVersion":"bundle-1mym82p"}`
- Live Builder: `{"bundleVersion":"bundle-1mym82p"}`

The fixed local CLI now sends the exact contract required by the live
Builder even though the install environment still contains Zod 4.5.4.

## Verification

- isolated before/after packaged CLI test through the real `sync`
request path
- `pnpm --filter webstudio exec vitest run
src/cli-bundle-externals.test.ts`
- `pnpm --filter webstudio typecheck`
- `pnpm --filter webstudio build`
- verified emitted CLI files contain no external imports from `zod` or
`@emotion/hash`
- `pnpm exec oxfmt --check packages/cli/vite.config.ts
packages/cli/src/cli-bundle-externals.test.ts`
- `git diff --check`
2026-08-31 23:45:45 +02:00

70 lines
1.8 KiB
TypeScript

import {
defaultClientConditions,
defaultServerConditions,
defineConfig,
} from "vite";
import { createRequire } from "node:module";
import pkg from "./package.json";
const require = createRequire(import.meta.url);
const nodeDecodeNamedCharacterReference =
require.resolve("decode-named-character-reference");
const externalDependencies = new Set(
Object.keys(pkg.dependencies).filter((name) => {
return name.startsWith("@webstudio-is/") === false;
})
);
// The project bundle contract is derived from Zod's schema definitions and
// hashed at runtime. Keep both implementations in the published artifact so a
// transitive dependency update cannot change bundleVersion after publication.
const bundledDependencies = new Set(["@emotion/hash", "acorn", "zod"]);
const getPackageName = (id: string) => {
if (id.startsWith("@")) {
return id.split("/").slice(0, 2).join("/");
}
return id.split("/")[0];
};
export const isExternal = (id: string) => {
const packageName = getPackageName(id);
return (
id.startsWith("node:") ||
(externalDependencies.has(packageName) &&
bundledDependencies.has(packageName) === false)
);
};
export default defineConfig({
resolve: {
alias: {
"decode-named-character-reference": nodeDecodeNamedCharacterReference,
},
conditions: ["webstudio", ...defaultClientConditions],
},
ssr: {
resolve: {
conditions: ["webstudio", ...defaultServerConditions],
},
},
build: {
target: "node22",
minify: false,
commonjsOptions: {
esmExternals: true,
},
lib: {
entry: ["src/cli.ts", "src/preview-server/preview-process-supervisor.ts"],
formats: ["es"],
},
rollupOptions: {
external: isExternal,
output: {
dir: "lib",
},
},
},
});