Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/quiet-bikes-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Fix webviewer scaffold installs by validating Vite native deps and repairing missing Rolldown bindings.
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ jobs:

- name: Build
run: pnpm ci:build
env:
PROOFKIT_MANIFEST_REVALIDATE_SECRET: ci-build-placeholder

cli-smoke:
name: CLI External Integration Smoke Tests
Expand Down Expand Up @@ -182,6 +184,8 @@ jobs:

- name: Build
run: pnpm ci:build
env:
PROOFKIT_MANIFEST_REVALIDATE_SECRET: ci-build-placeholder

- name: Publish preview packages
run: |
Expand Down
126 changes: 125 additions & 1 deletion packages/cli/src/core/executeInitPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
PromptService,
SettingsService,
} from "~/core/context.js";
import { DirectoryConflictError, FileSystemError, isCliError, UserCancelledError } from "~/core/errors.js";
import {
DirectoryConflictError,
ExternalCommandError,
FileSystemError,
isCliError,
UserCancelledError,
} from "~/core/errors.js";
import { applyPackageJsonMutations } from "~/core/planInit.js";
import type { InitPlan } from "~/core/types.js";
import { getIntentInstallCommand } from "~/helpers/intent.js";
Expand All @@ -33,6 +39,9 @@ import { sortPackageJson } from "~/utils/sortPackageJson.js";
const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]);
const IMPORT_ALIAS_WILDCARD_REGEX = /\*/g;
const IMPORT_ALIAS_TRAILING_SLASH_REGEX = /\/?$/;
const ROLLDOWN_NATIVE_BINDING_REGEX =
/Cannot find native binding|@rolldown[/+]binding-[\w-]+|rolldown[\s\S]*native binding/i;
const PNPM_ROLLDOWN_REPAIR_INSTRUCTION = "Delete node_modules and pnpm-lock.yaml, then run: pnpm install --force";
const chalk = new Chalk({ level: 1 });

const formatCommand = (command: string) => chalk.cyan(command);
Expand Down Expand Up @@ -94,6 +103,36 @@ function getPackageScriptCommand(plan: InitPlan, scriptName: string) {
return { command, args };
}

function getErrorDetails(error: unknown): string {
const parts: string[] = [];
const add = (value: unknown) => {
if (typeof value === "string" && value.trim()) {
parts.push(value.trim());
}
};
if (error instanceof Error) {
add(error.message);
} else if (typeof error === "string") {
add(error);
} else if (typeof error === "object" && error !== null) {
add("message" in error ? error.message : undefined);
}
const cause = typeof error === "object" && error !== null && "cause" in error ? error.cause : undefined;
if (typeof cause === "object" && cause !== null) {
add("message" in cause ? cause.message : undefined);
add("stdout" in cause ? cause.stdout : undefined);
add("stderr" in cause ? cause.stderr : undefined);
add("shortMessage" in cause ? cause.shortMessage : undefined);
} else {
add(cause);
}
return Array.from(new Set(parts)).join("\n");
}

function isRolldownNativeBindingError(error: unknown): boolean {
return ROLLDOWN_NATIVE_BINDING_REGEX.test(getErrorDetails(error));
}

function getMeaningfulDirectoryEntries(entries: string[]) {
return entries.filter((entry) => {
if (AGENT_METADATA_DIRS.has(entry)) {
Expand Down Expand Up @@ -405,6 +444,91 @@ export const executeInitPlan = (plan: InitPlan) =>
stdout: "pipe",
stderr: "pipe",
});

if (plan.request.appType === "webviewer" && plan.request.packageManager === "pnpm") {
consoleService.info("Validating Vite native dependencies...");
const validateVite = processService.run("pnpm", ["exec", "vite", "--version"], {
cwd: plan.targetDir,
stdout: "pipe",
stderr: "pipe",
});
const validationResult = yield* Effect.either(validateVite);

if (validationResult._tag === "Left") {
if (!isRolldownNativeBindingError(validationResult.left)) {
return yield* Effect.fail(validationResult.left);
}

const validationDetails = getErrorDetails(validationResult.left);
consoleService.warn(
[
"Vite native dependency validation failed because Rolldown native bindings are missing.",
validationDetails ? `Validation output:\n${validationDetails}` : undefined,
`Repairing install: ${PNPM_ROLLDOWN_REPAIR_INSTRUCTION}`,
]
.filter(Boolean)
.join("\n"),
);

yield* fs.remove(path.join(plan.targetDir, "node_modules"));
yield* fs.remove(path.join(plan.targetDir, "pnpm-lock.yaml"));
const repairResult = yield* Effect.either(
processService.run("pnpm", ["install", "--force"], {
cwd: plan.targetDir,
stdout: "pipe",
stderr: "pipe",
}),
);

if (repairResult._tag === "Left") {
const repairDetails = getErrorDetails(repairResult.left);
return yield* Effect.fail(
new ExternalCommandError({
message: [
"Vite native dependency repair failed.",
"Repair command: pnpm install --force",
repairDetails ? `Repair output:\n${repairDetails}` : undefined,
`Manual recovery: ${PNPM_ROLLDOWN_REPAIR_INSTRUCTION}`,
]
.filter(Boolean)
.join("\n"),
command: "pnpm",
args: ["install", "--force"],
cwd: plan.targetDir,
cause: repairResult.left,
}),
);
}

const repairedValidation = yield* Effect.either(
processService.run("pnpm", ["exec", "vite", "--version"], {
cwd: plan.targetDir,
stdout: "pipe",
stderr: "pipe",
}),
);

if (repairedValidation._tag === "Left") {
const repairedValidationDetails = getErrorDetails(repairedValidation.left);
return yield* Effect.fail(
new ExternalCommandError({
message: [
"Vite native dependency validation still failed after repair.",
"Validation command: pnpm exec vite --version",
repairedValidationDetails ? `Validation output:\n${repairedValidationDetails}` : undefined,
`Manual recovery: ${PNPM_ROLLDOWN_REPAIR_INSTRUCTION}`,
]
.filter(Boolean)
.join("\n"),
command: "pnpm",
args: ["exec", "vite", "--version"],
cwd: plan.targetDir,
cause: repairedValidation.left,
}),
);
}
}
}
}

if (plan.tasks.runUltraciteInit) {
Expand Down
165 changes: 165 additions & 0 deletions packages/cli/tests/executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe("executeInitPlan command paths", () => {

expect(tracker.commands).toEqual([
"pnpm install",
"pnpm exec vite --version",
[
"pnpx ultracite init --quiet --linter oxlint --pm pnpm --frameworks react --editors universal cursor",
"--agents universal claude codex --hooks cursor windsurf codebuddy claude --integrations husky lint-staged",
Expand All @@ -87,6 +88,170 @@ describe("executeInitPlan command paths", () => {
expect(pnpmWorkspaceFile).toContain("blockExoticSubdeps: true");
});

it("validates Vite native dependencies after webviewer pnpm install", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-vite-validate-ok-"));
const tracker = {
commands: [] as string[],
gitInits: 0,
codegens: 0,
filemakerBootstraps: 0,
};

const plan = planInit(
makeInitRequest({
appType: "webviewer",
dataSource: "none",
packageManager: "pnpm",
noInstall: false,
noGit: true,
cwd,
}),
{
templateDir: getSharedTemplateDir("vite-wv"),
packageManagerVersion: "11.0.0",
},
);

await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", tracker })));

expect(tracker.commands).toContain("pnpm install");
expect(tracker.commands).toContain("pnpm exec vite --version");
expect(tracker.commands).not.toContain("pnpm install --force");
});

it("repairs pnpm webviewer install when Rolldown native binding is missing", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-vite-validate-repair-"));
const projectDir = path.join(cwd, "demo-app");
const console = {
error: [] as string[],
info: [] as string[],
note: [] as Array<{ message: string; title?: string }>,
success: [] as string[],
warn: [] as string[],
};
const tracker = {
commands: [] as string[],
gitInits: 0,
codegens: 0,
filemakerBootstraps: 0,
};

const plan = planInit(
makeInitRequest({
appType: "webviewer",
dataSource: "none",
packageManager: "pnpm",
noInstall: false,
noGit: true,
cwd,
}),
{
templateDir: getSharedTemplateDir("vite-wv"),
packageManagerVersion: "11.0.0",
},
);

await Effect.runPromise(
executeInitPlan(plan).pipe(
makeTestLayer({
console,
cwd,
packageManager: "pnpm",
processRuns: {
"pnpm exec vite --version": [
new ExternalCommandError({
message: "Cannot find native binding. Missing @rolldown/binding-darwin-arm64",
command: "pnpm",
args: ["exec", "vite", "--version"],
cwd: projectDir,
}),
{ stdout: "vite/8.0.13", stderr: "" },
],
},
tracker,
}),
),
);

expect(tracker.commands).toContain("pnpm install --force");
expect(tracker.commands.filter((command) => command === "pnpm exec vite --version")).toHaveLength(2);
expect(console.warn.join("\n")).toContain("Rolldown native bindings are missing");
expect(console.warn.join("\n")).toContain("Delete node_modules and pnpm-lock.yaml, then run: pnpm install --force");
});

it("returns actionable error when Vite validation still fails after repair", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-vite-validate-repair-fail-"));
const projectDir = path.join(cwd, "demo-app");
const console = {
error: [] as string[],
info: [] as string[],
note: [] as Array<{ message: string; title?: string }>,
success: [] as string[],
warn: [] as string[],
};
const tracker = {
commands: [] as string[],
gitInits: 0,
codegens: 0,
filemakerBootstraps: 0,
};

const plan = planInit(
makeInitRequest({
appType: "webviewer",
dataSource: "none",
packageManager: "pnpm",
noInstall: false,
noGit: true,
cwd,
}),
{
templateDir: getSharedTemplateDir("vite-wv"),
packageManagerVersion: "11.0.0",
},
);

const failure = await getFailure(
executeInitPlan(plan).pipe(
makeTestLayer({
console,
cwd,
packageManager: "pnpm",
processRuns: {
"pnpm exec vite --version": [
new ExternalCommandError({
message: "Cannot find native binding. Missing @rolldown/binding-darwin-arm64",
command: "pnpm",
args: ["exec", "vite", "--version"],
cwd: projectDir,
}),
new ExternalCommandError({
message: "Cannot find native binding. Missing @rolldown/binding-darwin-arm64",
command: "pnpm",
args: ["exec", "vite", "--version"],
cwd: projectDir,
}),
],
"pnpm install --force": [{ stdout: "reinstalled", stderr: "" }],
},
tracker,
}),
),
);

expect(failure).toMatchObject({
_tag: "ExternalCommandError",
message: expect.stringContaining("Vite native dependency validation still failed after repair"),
});
expect(failure).toMatchObject({
message: expect.stringContaining(
"Manual recovery: Delete node_modules and pnpm-lock.yaml, then run: pnpm install --force",
),
});
expect(tracker.commands).toContain("pnpm install --force");
expect(console.warn.join("\n")).toContain("Validation output");
});

it("runs Ultracite with browser framework presets", async () => {
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-ultracite-browser-"));
const tracker = {
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/tests/test-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function makeTestLayer(options: {
deployDemoFile?: unknown;
packageManagerGetVersion?: Partial<Record<PackageManager, unknown>>;
};
processRuns?: Record<string, Array<unknown | { stdout: string; stderr: string }>>;
}) {
const tracker = options.tracker;
const promptScript = {
Expand Down Expand Up @@ -348,6 +349,18 @@ export function makeTestLayer(options: {
run: (command: string, args: string[]) => {
const processCommand = [command, ...args].join(" ");
tracker?.commands.push(processCommand);
const scriptedResult = options.processRuns?.[processCommand]?.shift();
if (scriptedResult !== undefined) {
if (
typeof scriptedResult === "object" &&
scriptedResult !== null &&
"stdout" in scriptedResult &&
"stderr" in scriptedResult
) {
return Effect.succeed(scriptedResult as { stdout: string; stderr: string });
}
return Effect.fail(scriptedResult as ExternalCommandError);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const processRunFailure = options.failures?.processRun;
if (options.failProcessCommand === processCommand) {
if (!processRunFailure) {
Expand Down
Loading