Skip to content
Merged
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
122 changes: 122 additions & 0 deletions scripts/check-changed-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import { expect, it } from "vite-plus/test";

import { vitePlusCommand } from "./check-changed-command.ts";

function fixture() {
const root = NodeFS.realpathSync(
NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3 check & (vp) % ! ")),
);
const write = (file: string, content: string) => {
const path = NodePath.join(root, file);
NodeFS.mkdirSync(NodePath.dirname(path), { recursive: true });
NodeFS.writeFileSync(path, content);
};
NodeFS.copyFileSync(
NodePath.join(import.meta.dirname, "check-changed-command.ts"),
NodePath.join(root, "command.ts"),
);
write("package.json", JSON.stringify({ type: "module" }));
write(
"node_modules/vite-plus/package.json",
JSON.stringify({ type: "module", exports: { "./bin": "./cli.js" } }),
);
write(
"node_modules/vite-plus/cli.js",
`import fs from 'node:fs';
fs.writeFileSync(process.env.CALL_FILE, JSON.stringify({
args: process.argv.slice(2), cwd: process.cwd(), marker: process.env.ARG_MARKER
}));
process.stdout.write('vp stdout');
process.stderr.write('vp stderr');
process.exit(Number(process.env.EXIT_STATUS));`,
);
write(
"run.mjs",
`import { spawn, spawnSync } from 'node:child_process';
import { vitePlusCommand } from './command.ts';
const [command, args] = vitePlusCommand(process.argv.slice(3), 'win32');
const options = { cwd: process.env.CHILD_CWD, env: process.env };
if (process.argv[2] === 'sync') {
const result = spawnSync(command, args, { ...options, encoding: 'utf8' });
process.stdout.write(result.stdout ?? '');
process.stderr.write(result.stderr ?? '');
if (result.error) console.error(result.error.message);
process.exitCode = result.status ?? 1;
} else {
const child = spawn(command, args, options);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
child.on('error', error => console.error(error.message));
child.on('close', status => { process.exitCode = status ?? 1; });
}`,
);
const cwd = NodePath.join(root, "temporary install");
NodeFS.mkdirSync(cwd);
return {
cwd,
run(mode: string, args: Array<string>, status: number) {
return NodeChildProcess.spawnSync(
process.execPath,
[NodePath.join(root, "run.mjs"), mode, ...args],
{
cwd: NodeOS.tmpdir(),
encoding: "utf8",
env: {
...process.env,
// No vp, vp.cmd or node launcher can be found on PATH.
PATH: root,
Path: root,
CALL_FILE: NodePath.join(root, "call.json"),
CHILD_CWD: cwd,
ARG_MARKER: "inherited",
EXIT_STATUS: String(status),
},
},
);
},
call: () => JSON.parse(NodeFS.readFileSync(NodePath.join(root, "call.json"), "utf8")),
[Symbol.dispose]() {
NodeFS.rmSync(root, { recursive: true, force: true });
},
};
}

it.each(["linux", "darwin"] as const)("keeps the existing %s launcher", (platform) => {
const args = ["fmt", "--check", "a file.ts"];
expect(vitePlusCommand(args, platform)).toEqual(["vp", args]);
});

it.each(["sync", "async"])("launches the Windows CLI without a shim (%s)", (mode) => {
using project = fixture();
const args = [
"fmt",
"--check",
"space & (draft) %PATH% ! ^ 研究.ts",
'embedded"quote',
"trailing\\",
"",
"& echo injected>injected.txt",
];
const result = project.run(mode, args, 0);
expect(result.error).toBeUndefined();
expect(result.status).toBe(0);
expect(result.stdout).toBe("vp stdout");
expect(result.stderr).toBe("vp stderr");
expect(project.call()).toEqual({ args, cwd: project.cwd, marker: "inherited" });
expect(NodeFS.existsSync(NodePath.join(project.cwd, "injected.txt"))).toBe(false);
});

it.each(["sync", "async"])("preserves a failing CLI's output and status (%s)", (mode) => {
using project = fixture();
const args = ["install", "--lockfile-only", "--ignore-scripts"];
const result = project.run(mode, args, 23);
expect(result.status).toBe(23);
expect(result.stdout).toBe("vp stdout");
expect(result.stderr).toBe("vp stderr");
expect(project.call()).toEqual({ args, cwd: project.cwd, marker: "inherited" });
});
19 changes: 19 additions & 0 deletions scripts/check-changed-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeURL from "node:url";

export function vitePlusCommand(
args: ReadonlyArray<string>,
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone Node tooling has no Effect runtime; tests pass the platform explicitly.
platform: NodeJS.Platform = process.platform,
): [command: string, args: Array<string>] {
// Windows cannot spawn the package manager's vp.cmd shim without a shell.
// Use Vite+'s exported JS entry with this Node instead, keeping paths and
// arguments literal. Resolve here, not from the patch check's temporary cwd.
if (platform === "win32") {
return [
process.execPath,
[NodeURL.fileURLToPath(import.meta.resolve("vite-plus/bin")), ...args],
];
}
return ["vp", [...args]];
}
141 changes: 141 additions & 0 deletions scripts/check-changed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import { expect } from "vite-plus/test";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Option from "effect/Option";
import { assert, describe, it } from "@effect/vitest";
Expand Down Expand Up @@ -264,3 +265,143 @@ it("compares against an explicit ref instead of the closest remote main", async
assert.strictEqual(result.base, project.commits.A);
assert.strictEqual(result.ref, "origin/main");
});

function launcherFixture() {
const root = NodeFS.realpathSync(
NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3 checks & (fixture) ")),
);
const write = (file: string, content: string) => {
const path = NodePath.join(root, file);
NodeFS.mkdirSync(NodePath.dirname(path), { recursive: true });
NodeFS.writeFileSync(path, content);
};
for (const file of [
"scripts/check-changed.ts",
"scripts/check-changed-command.ts",
".github/scripts/ci-changes.cjs",
]) {
write(file, NodeFS.readFileSync(NodePath.join(import.meta.dirname, "..", file), "utf8"));
}
write(".gitignore", "node_modules/\nrecords/\n");
write("pnpm-workspace.yaml", "packages:\n - packages/*\n");
write(
"package.json",
JSON.stringify({ type: "module", scripts: { "knip:check": "knip --include files" } }),
);
write("packages/example/package.json", JSON.stringify({ name: "example" }));
const source = "packages/example/space & (draft) %PATH% ! 研究.ts";
write(source, "export const value = 1;\n");
write(
"node_modules/vite-plus/package.json",
JSON.stringify({ type: "module", exports: { "./bin": "./cli.js" } }),
);
write(
"node_modules/vite-plus/cli.js",
`
import fs from 'node:fs';
import path from 'node:path';
const args = process.argv.slice(2);
fs.writeFileSync(path.join(process.env.RECORDS, process.pid + '.json'), JSON.stringify({ args, cwd: process.cwd() }));
console.log('fixture stdout: ' + args[0]);
console.error('fixture stderr: ' + args[0]);
process.exit(args[0] === process.env.FAIL_STEP ? 23 : 0);
`,
);
// Select the Windows launcher without changing the Effect runtime's host platform.
write(
"scripts/check-changed.ts",
NodeFS.readFileSync(NodePath.join(import.meta.dirname, "check-changed.ts"), "utf8").replace(
"vitePlusCommand(args, platform)",
'vitePlusCommand(args, "win32")',
),
);
for (const name of ["effect", "@effect/platform-node", "@t3tools/shared"]) {
const dest = NodePath.join(root, "node_modules", name);
NodeFS.mkdirSync(NodePath.dirname(dest), { recursive: true });
NodeFS.symlinkSync(
NodeFS.realpathSync(NodePath.join(import.meta.dirname, "node_modules", name)),
dest,
"junction",
);
}
const git = (...args: Array<string>) =>
NodeChildProcess.execFileSync("git", args, { cwd: root, stdio: "pipe", env });
git("init");
git("add", ".");
git(
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.invalid",
"-c",
"commit.gpgsign=false",
"-c",
"core.hooksPath=",
"commit",
"-m",
"fixture",
);
const records = NodePath.join(root, "records");
NodeFS.mkdirSync(records);
return {
root,
source,
write,
run(failStep = "") {
return NodeChildProcess.spawnSync(process.execPath, ["scripts/check-changed.ts", "HEAD"], {
cwd: root,
encoding: "utf8",
env: { ...env, TEMP: NodeOS.tmpdir(), RECORDS: records, FAIL_STEP: failStep },
});
},
calls: (): Array<{ args: Array<string>; cwd: string }> =>
NodeFS.readdirSync(records).map((file) =>
JSON.parse(NodeFS.readFileSync(NodePath.join(records, file), "utf8")),
),
[Symbol.dispose]() {
NodeFS.rmSync(root, { recursive: true, force: true });
},
};
}

it("checks changed source paths through the Windows launcher", () => {
using project = launcherFixture();
project.write(project.source, "export const value = 2;\n");
const result = project.run();
expect(result.status, result.stdout + result.stderr).toBe(0);
const calls = project.calls();
expect(calls.find(({ args }) => args[0] === "lint")).toEqual({
args: ["lint", project.source],
cwd: project.root,
});
expect(calls.some(({ args }) => args[0] === "fmt" && args.includes(project.source))).toBe(true);
expect(calls.some(({ args }) => args[0] === "run" && args.includes("typecheck"))).toBe(true);
expect(calls.some(({ args }) => args[0] === "install")).toBe(false);
expect(result.stdout).toContain("fixture stdout: lint");
expect(result.stdout).toContain("fixture stderr: lint");
});

it.each(["install", "lint"])("reports a failing %s step with its output", (step) => {
using project = launcherFixture();
project.write(project.source, "export const value = 2;\n");
project.write(
"packages/example/package.json",
JSON.stringify({ name: "example", version: "1.0.1" }),
);
const result = project.run(step);
expect(result.status, result.stdout + result.stderr).toBe(1);
const install = project.calls().find(({ args }) => args[0] === "install");
expect(install?.args, result.stdout + result.stderr).toEqual([
"install",
"--lockfile-only",
"--ignore-scripts",
]);
expect(install?.cwd).not.toBe(project.root);
expect(result.stdout + result.stderr).toContain(`fixture stdout: ${step}`);
expect(result.stdout + result.stderr).toContain(`fixture stderr: ${step}`);
expect(result.stdout).toContain(step === "install" ? "FAIL unused patches" : "FAIL lint");
if (step === "lint") expect(result.stdout).toContain("pass unused patches");
expect(
NodeFS.existsSync(NodePath.join(project.root, "node_modules/.cache/check-changed.json")),
).toBe(false);
});
9 changes: 8 additions & 1 deletion scripts/check-changed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { Argument, Command, Flag } from "effect/cli";
import { ChildProcess, ChildProcessSpawner } from "effect/process";

import { decide, readWorkspace } from "../.github/scripts/ci-changes.cjs";
import { vitePlusCommand } from "./check-changed-command.ts";

const GiB = 1024 ** 3;

Expand Down Expand Up @@ -390,8 +391,14 @@ const checkChanged = Effect.fn("checkChanged")(function* (input: {
const vp = (args: ReadonlyArray<string>, cwd = root) =>
Effect.scoped(
Effect.gen(function* () {
const [command, commandArgs] = vitePlusCommand(args, platform);
const handle = yield* spawner.spawn(
ChildProcess.make("vp", args, { cwd, env, extendEnv: true, forceKillAfter: "5 seconds" }),
ChildProcess.make(command, commandArgs, {
cwd,
env,
extendEnv: true,
forceKillAfter: "5 seconds",
}),
);
const [output, exitCode] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(handle.all)), handle.exitCode],
Expand Down
Loading