Skip to content

Commit d638d3e

Browse files
committed
fix: auto-install @playwright/test when consumer project doesn't have it
Most projects integrating git-glimpse won't have @playwright/test as a dependency (e.g. game/backend projects). The action previously relied on the consumer's node_modules, failing with ERESOLVE for those projects. Adds ensurePlaywright() in packages/core/src/recorder/ensure-playwright.ts: - First tries to resolve @playwright/test from the consumer's project (preserving existing behaviour for projects that already have it pinned) - Falls back to auto-installing @playwright/test into ~/.cache/git-glimpse/playwright - Also ensures Chromium browser binaries are installed via playwright CLI Both playwright-runner.ts and fallback.ts now use ensurePlaywright() instead of a bare createRequire(process.cwd()) call. https://claude.ai/code/session_01XDxRzi588C73zsj1xPwmEb
1 parent c2e9ebb commit d638d3e

File tree

5 files changed

+224
-82
lines changed

5 files changed

+224
-82
lines changed

packages/action/dist/index.js

Lines changed: 134 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -60835,7 +60835,7 @@ var require_dist_node14 = __commonJS({
6083560835
// src/index.ts
6083660836
var core = __toESM(require_core());
6083760837
var github = __toESM(require_github());
60838-
var import_node_child_process2 = require("node:child_process");
60838+
var import_node_child_process3 = require("node:child_process");
6083960839

6084060840
// ../../node_modules/.pnpm/@anthropic-ai+sdk@0.24.3/node_modules/@anthropic-ai/sdk/error.mjs
6084160841
var error_exports = {};
@@ -65516,15 +65516,67 @@ async function generateDemoScript(client, analysis, rawDiff, baseUrl, config, ge
6551665516
}
6551765517

6551865518
// ../core/dist/recorder/playwright-runner.js
65519+
var import_node_fs3 = require("node:fs");
65520+
var import_node_path2 = require("node:path");
65521+
65522+
// ../core/dist/recorder/ensure-playwright.js
6551965523
var import_node_fs2 = require("node:fs");
6552065524
var import_node_path = require("node:path");
65525+
var import_node_os = require("node:os");
65526+
var import_node_child_process = require("node:child_process");
6552165527
var import_node_module = require("node:module");
65528+
async function ensurePlaywright() {
65529+
try {
65530+
const req2 = (0, import_node_module.createRequire)((0, import_node_path.join)(process.cwd(), "package.json"));
65531+
return req2("@playwright/test");
65532+
} catch {
65533+
}
65534+
const installDir = resolveInstallDir();
65535+
const playwrightPkgPath = (0, import_node_path.join)(installDir, "node_modules", "@playwright", "test", "package.json");
65536+
if (!(0, import_node_fs2.existsSync)(playwrightPkgPath)) {
65537+
console.info("[git-glimpse] @playwright/test not found in consumer project. Installing...");
65538+
console.info(`[git-glimpse] Install directory: ${installDir}`);
65539+
(0, import_node_child_process.execFileSync)("npm", ["install", "--prefix", installDir, "--no-save", "@playwright/test"], {
65540+
stdio: "inherit"
65541+
});
65542+
console.info("[git-glimpse] @playwright/test installed successfully.");
65543+
}
65544+
const req = (0, import_node_module.createRequire)((0, import_node_path.join)(installDir, "package.json"));
65545+
const pw = req("@playwright/test");
65546+
await ensureChromium(installDir);
65547+
return pw;
65548+
}
65549+
function resolveInstallDir() {
65550+
try {
65551+
const dir = (0, import_node_path.join)((0, import_node_os.homedir)(), ".cache", "git-glimpse", "playwright");
65552+
return dir;
65553+
} catch {
65554+
return (0, import_node_path.join)((0, import_node_os.tmpdir)(), "git-glimpse-playwright");
65555+
}
65556+
}
65557+
async function ensureChromium(installDir) {
65558+
const msPlaywrightCache = (0, import_node_path.join)((0, import_node_os.homedir)(), ".cache", "ms-playwright");
65559+
if ((0, import_node_fs2.existsSync)(msPlaywrightCache)) {
65560+
const entries = await import("node:fs").then((fs2) => fs2.readdirSync(msPlaywrightCache).filter((e2) => e2.startsWith("chromium")));
65561+
if (entries.length > 0) {
65562+
return;
65563+
}
65564+
}
65565+
console.info("[git-glimpse] Installing Playwright Chromium browser...");
65566+
const playwrightCli = (0, import_node_path.join)(installDir, "node_modules", ".bin", "playwright");
65567+
(0, import_node_child_process.execFileSync)(playwrightCli, ["install", "chromium", "--with-deps"], {
65568+
stdio: "inherit"
65569+
});
65570+
console.info("[git-glimpse] Chromium installed.");
65571+
}
65572+
65573+
// ../core/dist/recorder/playwright-runner.js
6552265574
async function runScriptAndRecord(options) {
6552365575
const { script, baseUrl, recording, outputDir } = options;
65524-
if (!(0, import_node_fs2.existsSync)(outputDir)) {
65525-
(0, import_node_fs2.mkdirSync)(outputDir, { recursive: true });
65576+
if (!(0, import_node_fs3.existsSync)(outputDir)) {
65577+
(0, import_node_fs3.mkdirSync)(outputDir, { recursive: true });
6552665578
}
65527-
const { chromium } = (0, import_node_module.createRequire)((0, import_node_path.join)(process.cwd(), "package.json"))("@playwright/test");
65579+
const { chromium } = await ensurePlaywright();
6552865580
const browser = await chromium.launch({ headless: true });
6552965581
const startTime = Date.now();
6553065582
try {
@@ -65617,11 +65669,11 @@ function buildMouseClickOverlayEvalScript() {
6561765669
}
6561865670
async function executeScript(script, page, _baseUrl) {
6561965671
const { writeFileSync: writeFileSync2, unlinkSync: unlinkSync2 } = await import("node:fs");
65620-
const { tmpdir } = await import("node:os");
65672+
const { tmpdir: tmpdir2 } = await import("node:os");
6562165673
const { pathToFileURL: pathToFileURL2 } = await import("node:url");
6562265674
const { transform } = await Promise.resolve().then(() => __toESM(require_dist2(), 1));
6562365675
const { code } = transform(script, { transforms: ["typescript"] });
65624-
const tmpPath = (0, import_node_path.join)(tmpdir(), `git-glimpse-script-${Date.now()}.mjs`);
65676+
const tmpPath = (0, import_node_path2.join)(tmpdir2(), `git-glimpse-script-${Date.now()}.mjs`);
6562565677
writeFileSync2(tmpPath, code, "utf-8");
6562665678
try {
6562765679
const mod = await import(pathToFileURL2(tmpPath).href);
@@ -65635,23 +65687,23 @@ async function executeScript(script, page, _baseUrl) {
6563565687
}
6563665688
async function resolveVideoPath(outputDir) {
6563765689
const { readdirSync: readdirSync2, statSync: statSync3 } = await import("node:fs");
65638-
const files = readdirSync2(outputDir).filter((f2) => f2.endsWith(".webm")).map((f2) => ({ name: f2, mtime: statSync3((0, import_node_path.join)(outputDir, f2)).mtimeMs })).sort((a2, b2) => b2.mtime - a2.mtime);
65690+
const files = readdirSync2(outputDir).filter((f2) => f2.endsWith(".webm")).map((f2) => ({ name: f2, mtime: statSync3((0, import_node_path2.join)(outputDir, f2)).mtimeMs })).sort((a2, b2) => b2.mtime - a2.mtime);
6563965691
const latest = files[0];
6564065692
if (!latest)
6564165693
throw new Error(`No video file found in ${outputDir}`);
65642-
return (0, import_node_path.join)(outputDir, latest.name);
65694+
return (0, import_node_path2.join)(outputDir, latest.name);
6564365695
}
6564465696

6564565697
// ../core/dist/recorder/post-processor.js
65646-
var import_node_child_process = require("node:child_process");
65647-
var import_node_fs3 = require("node:fs");
65648-
var import_node_path2 = require("node:path");
65649-
var import_node_os = __toESM(require("node:os"), 1);
65698+
var import_node_child_process2 = require("node:child_process");
65699+
var import_node_fs4 = require("node:fs");
65700+
var import_node_path3 = require("node:path");
65701+
var import_node_os2 = __toESM(require("node:os"), 1);
6565065702
async function postProcess(options) {
6565165703
const { inputPath, outputDir, format } = options;
6565265704
const ffmpegPath = resolveFfmpegPath();
65653-
const outputName = (0, import_node_path2.basename)(inputPath, ".webm") + "." + format;
65654-
const outputPath = (0, import_node_path2.join)(outputDir, outputName);
65705+
const outputName = (0, import_node_path3.basename)(inputPath, ".webm") + "." + format;
65706+
const outputPath = (0, import_node_path3.join)(outputDir, outputName);
6565565707
if (format === "gif") {
6565665708
await convertToGif(ffmpegPath, inputPath, outputPath, options.viewport);
6565765709
} else if (format === "mp4") {
@@ -65665,7 +65717,7 @@ async function postProcess(options) {
6566565717
async function convertToGif(ffmpegPath, input, output, viewport) {
6566665718
const targetWidth = Math.min(viewport.width / 2, 960);
6566765719
const palettePath = output.replace(".gif", "-palette.png");
65668-
(0, import_node_child_process.execFileSync)(ffmpegPath, [
65720+
(0, import_node_child_process2.execFileSync)(ffmpegPath, [
6566965721
"-i",
6567065722
input,
6567165723
"-vf",
@@ -65675,7 +65727,7 @@ async function convertToGif(ffmpegPath, input, output, viewport) {
6567565727
"-y",
6567665728
palettePath
6567765729
]);
65678-
(0, import_node_child_process.execFileSync)(ffmpegPath, [
65730+
(0, import_node_child_process2.execFileSync)(ffmpegPath, [
6567965731
"-i",
6568065732
input,
6568165733
"-i",
@@ -65688,11 +65740,11 @@ async function convertToGif(ffmpegPath, input, output, viewport) {
6568865740
output
6568965741
]);
6569065742
const { unlinkSync: unlinkSync2 } = await import("node:fs");
65691-
if ((0, import_node_fs3.existsSync)(palettePath))
65743+
if ((0, import_node_fs4.existsSync)(palettePath))
6569265744
unlinkSync2(palettePath);
6569365745
}
6569465746
async function convertToMp4(ffmpegPath, input, output) {
65695-
(0, import_node_child_process.execFileSync)(ffmpegPath, [
65747+
(0, import_node_child_process2.execFileSync)(ffmpegPath, [
6569665748
"-i",
6569765749
input,
6569865750
"-c:v",
@@ -65710,7 +65762,7 @@ async function convertToMp4(ffmpegPath, input, output) {
6571065762
]);
6571165763
}
6571265764
async function trimWebm(ffmpegPath, input, output) {
65713-
(0, import_node_child_process.execFileSync)(ffmpegPath, [
65765+
(0, import_node_child_process2.execFileSync)(ffmpegPath, [
6571465766
"-i",
6571565767
input,
6571665768
"-c",
@@ -65726,7 +65778,7 @@ async function getFileSizeMB(filePath) {
6572665778
}
6572765779
function resolveFfmpegPath() {
6572865780
try {
65729-
(0, import_node_child_process.execFileSync)("ffmpeg", ["-version"], { stdio: "ignore" });
65781+
(0, import_node_child_process2.execFileSync)("ffmpeg", ["-version"], { stdio: "ignore" });
6573065782
return "ffmpeg";
6573165783
} catch {
6573265784
}
@@ -65742,45 +65794,44 @@ function findPlaywrightCacheDir() {
6574265794
const envPath = process.env["PLAYWRIGHT_BROWSERS_PATH"];
6574365795
if (envPath && envPath !== "0")
6574465796
return envPath;
65745-
const home = import_node_os.default.homedir();
65797+
const home = import_node_os2.default.homedir();
6574665798
if (process.platform === "linux") {
6574765799
const xdgCache = process.env["XDG_CACHE_HOME"];
65748-
return xdgCache ? (0, import_node_path2.join)(xdgCache, "ms-playwright") : (0, import_node_path2.join)(home, ".cache", "ms-playwright");
65800+
return xdgCache ? (0, import_node_path3.join)(xdgCache, "ms-playwright") : (0, import_node_path3.join)(home, ".cache", "ms-playwright");
6574965801
}
6575065802
if (process.platform === "darwin") {
65751-
return (0, import_node_path2.join)(home, "Library", "Caches", "ms-playwright");
65803+
return (0, import_node_path3.join)(home, "Library", "Caches", "ms-playwright");
6575265804
}
6575365805
if (process.platform === "win32") {
65754-
const localAppData = process.env["LOCALAPPDATA"] ?? (0, import_node_path2.join)(home, "AppData", "Local");
65755-
return (0, import_node_path2.join)(localAppData, "ms-playwright");
65806+
const localAppData = process.env["LOCALAPPDATA"] ?? (0, import_node_path3.join)(home, "AppData", "Local");
65807+
return (0, import_node_path3.join)(localAppData, "ms-playwright");
6575665808
}
6575765809
return null;
6575865810
}
6575965811
function scanForFfmpeg(cacheDir) {
65760-
if (!(0, import_node_fs3.existsSync)(cacheDir))
65812+
if (!(0, import_node_fs4.existsSync)(cacheDir))
6576165813
return null;
65762-
const entries = (0, import_node_fs3.readdirSync)(cacheDir);
65814+
const entries = (0, import_node_fs4.readdirSync)(cacheDir);
6576365815
const ffmpegDir = entries.find((e2) => e2.startsWith("ffmpeg"));
6576465816
if (!ffmpegDir)
6576565817
return null;
65766-
const dir = (0, import_node_path2.join)(cacheDir, ffmpegDir);
65818+
const dir = (0, import_node_path3.join)(cacheDir, ffmpegDir);
6576765819
for (const name of ["ffmpeg-linux", "ffmpeg-mac", "ffmpeg-win64.exe", "ffmpeg"]) {
65768-
const candidate = (0, import_node_path2.join)(dir, name);
65769-
if ((0, import_node_fs3.existsSync)(candidate))
65820+
const candidate = (0, import_node_path3.join)(dir, name);
65821+
if ((0, import_node_fs4.existsSync)(candidate))
6577065822
return candidate;
6577165823
}
6577265824
return null;
6577365825
}
6577465826

6577565827
// ../core/dist/recorder/fallback.js
65776-
var import_node_fs4 = require("node:fs");
65777-
var import_node_path3 = require("node:path");
65778-
var import_node_module2 = require("node:module");
65828+
var import_node_fs5 = require("node:fs");
65829+
var import_node_path4 = require("node:path");
6577965830
async function takeScreenshots(baseUrl, routes, recording, outputDir) {
65780-
if (!(0, import_node_fs4.existsSync)(outputDir)) {
65781-
(0, import_node_fs4.mkdirSync)(outputDir, { recursive: true });
65831+
if (!(0, import_node_fs5.existsSync)(outputDir)) {
65832+
(0, import_node_fs5.mkdirSync)(outputDir, { recursive: true });
6578265833
}
65783-
const { chromium } = (0, import_node_module2.createRequire)((0, import_node_path3.join)(process.cwd(), "package.json"))("@playwright/test");
65834+
const { chromium } = await ensurePlaywright();
6578465835
const browser = await chromium.launch({ headless: true });
6578565836
const screenshots = [];
6578665837
try {
@@ -65795,7 +65846,7 @@ async function takeScreenshots(baseUrl, routes, recording, outputDir) {
6579565846
await page.goto(url);
6579665847
await page.waitForLoadState("networkidle");
6579765848
await page.waitForTimeout(1e3);
65798-
const screenshotPath = (0, import_node_path3.join)(outputDir, `screenshot-${sanitizeRoute(route.route)}.png`);
65849+
const screenshotPath = (0, import_node_path4.join)(outputDir, `screenshot-${sanitizeRoute(route.route)}.png`);
6579965850
await page.screenshot({ path: screenshotPath, fullPage: false });
6580065851
screenshots.push(screenshotPath);
6580165852
}
@@ -65894,8 +65945,8 @@ async function runPipeline(options) {
6589465945

6589565946
// ../core/dist/config/loader.js
6589665947
var import_node_url = require("node:url");
65897-
var import_node_fs5 = require("node:fs");
65898-
var import_node_path4 = require("node:path");
65948+
var import_node_fs6 = require("node:fs");
65949+
var import_node_path5 = require("node:path");
6589965950

6590065951
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
6590165952
var external_exports = {};
@@ -70003,30 +70054,30 @@ var DEFAULT_TRIGGER = {
7000370054

7000470055
// ../core/dist/config/loader.js
7000570056
async function importConfigFile(filePath) {
70006-
if ((0, import_node_path4.extname)(filePath) !== ".ts") {
70057+
if ((0, import_node_path5.extname)(filePath) !== ".ts") {
7000770058
const mod = await import((0, import_node_url.pathToFileURL)(filePath).href);
7000870059
return mod.default ?? mod;
7000970060
}
7001070061
const { transform } = await Promise.resolve().then(() => __toESM(require_dist2(), 1));
70011-
const source = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
70062+
const source = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
7001270063
const { code } = transform(source, { transforms: ["typescript"] });
70013-
const tmpFile = (0, import_node_path4.resolve)((0, import_node_path4.dirname)(filePath), `.git-glimpse-config-${Date.now()}.mjs`);
70064+
const tmpFile = (0, import_node_path5.resolve)((0, import_node_path5.dirname)(filePath), `.git-glimpse-config-${Date.now()}.mjs`);
7001470065
try {
70015-
(0, import_node_fs5.writeFileSync)(tmpFile, code);
70066+
(0, import_node_fs6.writeFileSync)(tmpFile, code);
7001670067
const mod = await import((0, import_node_url.pathToFileURL)(tmpFile).href);
7001770068
return mod.default ?? mod;
7001870069
} finally {
7001970070
try {
70020-
(0, import_node_fs5.unlinkSync)(tmpFile);
70071+
(0, import_node_fs6.unlinkSync)(tmpFile);
7002170072
} catch {
7002270073
}
7002370074
}
7002470075
}
7002570076
async function loadConfig(configPath) {
7002670077
const candidates = configPath ? [configPath] : ["git-glimpse.config.ts", "git-glimpse.config.js", "git-glimpse.config.mjs"];
7002770078
for (const candidate of candidates) {
70028-
const fullPath = (0, import_node_path4.resolve)(process.cwd(), candidate);
70029-
if ((0, import_node_fs5.existsSync)(fullPath)) {
70079+
const fullPath = (0, import_node_path5.resolve)(process.cwd(), candidate);
70080+
if ((0, import_node_fs6.existsSync)(fullPath)) {
7003070081
const raw = await importConfigFile(fullPath);
7003170082
return parseConfig(raw);
7003270083
}
@@ -70147,14 +70198,14 @@ ${script}
7014770198
}
7014870199

7014970200
// ../core/dist/publisher/storage.js
70150-
var import_node_fs6 = require("node:fs");
70151-
var import_node_path5 = require("node:path");
70201+
var import_node_fs7 = require("node:fs");
70202+
var import_node_path6 = require("node:path");
7015270203
async function uploadToGitHubAssets(token, owner, repo, filePath) {
7015370204
const { Octokit: Octokit2 } = await Promise.resolve().then(() => __toESM(require_dist_node14(), 1));
7015470205
const octokit = new Octokit2({ auth: token });
70155-
const fileBuffer = (0, import_node_fs6.readFileSync)(filePath);
70156-
const fileName = (0, import_node_path5.basename)(filePath);
70157-
const size = (0, import_node_fs6.statSync)(filePath).size;
70206+
const fileBuffer = (0, import_node_fs7.readFileSync)(filePath);
70207+
const fileName = (0, import_node_path6.basename)(filePath);
70208+
const size = (0, import_node_fs7.statSync)(filePath).size;
7015870209
const release = await octokit.rest.repos.createRelease({
7015970210
owner,
7016070211
repo,
@@ -70269,11 +70320,36 @@ function evaluateTrigger(opts) {
7026970320
};
7027070321
}
7027170322

70323+
// src/resolve-base-url.ts
70324+
function resolveBaseUrl(config, previewUrlOverride) {
70325+
const previewUrl = previewUrlOverride ?? config.app.previewUrl;
70326+
if (previewUrl) {
70327+
const resolved = process.env[previewUrl];
70328+
if (resolved === void 0) {
70329+
if (previewUrl.startsWith("http")) return { url: previewUrl };
70330+
return {
70331+
error: `app.previewUrl is set to "${previewUrl}" but it doesn't look like a URL and no env var with that name was found. Set it to a full URL (e.g. "https://my-preview.vercel.app") or an env var name that is available in this workflow job.`
70332+
};
70333+
}
70334+
if (!resolved.startsWith("http")) {
70335+
return {
70336+
error: `Env var "${previewUrl}" was found but its value "${resolved}" is not a valid URL. Expected a value starting with "http".`
70337+
};
70338+
}
70339+
return { url: resolved };
70340+
}
70341+
if (config.app.readyWhen?.url) {
70342+
const u2 = new URL(config.app.readyWhen.url);
70343+
return { url: u2.origin };
70344+
}
70345+
return { url: "http://localhost:3000" };
70346+
}
70347+
7027270348
// src/index.ts
7027370349
function streamCommand(cmd, args) {
7027470350
return new Promise((resolve2, reject) => {
7027570351
const chunks = [];
70276-
const proc = (0, import_node_child_process2.spawn)(cmd, args, { shell: false });
70352+
const proc = (0, import_node_child_process3.spawn)(cmd, args, { shell: false });
7027770353
proc.stdout.on("data", (chunk) => chunks.push(chunk));
7027870354
proc.stderr.on("data", (chunk) => chunks.push(chunk));
7027970355
proc.on("error", reject);
@@ -70385,17 +70461,16 @@ async function run() {
7038570461
core.setOutput("success", "false");
7038670462
return;
7038770463
}
70388-
const baseUrl = resolveBaseUrl(config, previewUrlInput);
70389-
if (!baseUrl) {
70390-
core.setFailed(
70391-
"No base URL available. Set app.previewUrl or app.startCommand + app.readyWhen in config."
70392-
);
70464+
const baseUrlResult = resolveBaseUrl(config, previewUrlInput);
70465+
if (!baseUrlResult.url) {
70466+
core.setFailed(baseUrlResult.error);
7039370467
return;
7039470468
}
70469+
const baseUrl = baseUrlResult.url;
7039570470
if (config.setup) {
7039670471
core.info(`Running setup: ${config.setup}`);
7039770472
const parts = config.setup.split(" ");
70398-
(0, import_node_child_process2.execFileSync)(parts[0], parts.slice(1), { stdio: "inherit" });
70473+
(0, import_node_child_process3.execFileSync)(parts[0], parts.slice(1), { stdio: "inherit" });
7039970474
}
7040070475
let appProcess = null;
7040170476
if (config.app.startCommand && !config.app.previewUrl) {
@@ -70447,22 +70522,10 @@ ${result.errors.join("\n")}`);
7044770522
appProcess?.kill();
7044870523
}
7044970524
}
70450-
function resolveBaseUrl(config, previewUrlOverride) {
70451-
const previewUrl = previewUrlOverride ?? config.app.previewUrl;
70452-
if (previewUrl) {
70453-
const resolved = process.env[previewUrl] ?? previewUrl;
70454-
return resolved.startsWith("http") ? resolved : null;
70455-
}
70456-
if (config.app.readyWhen?.url) {
70457-
const u2 = new URL(config.app.readyWhen.url);
70458-
return u2.origin;
70459-
}
70460-
return "http://localhost:3000";
70461-
}
7046270525
async function startApp(startCommand, readyUrl) {
7046370526
const parts = startCommand.split(" ");
7046470527
core.info(`Starting app: ${startCommand}`);
70465-
const proc = (0, import_node_child_process2.spawn)(parts[0], parts.slice(1), { stdio: "inherit", shell: false });
70528+
const proc = (0, import_node_child_process3.spawn)(parts[0], parts.slice(1), { stdio: "inherit", shell: false });
7046670529
await waitForUrl(readyUrl, 3e4);
7046770530
core.info("App is ready");
7046870531
return proc;

0 commit comments

Comments
 (0)