From 9c90d2fab49408f4a73caf982c4a564da54ac623 Mon Sep 17 00:00:00 2001 From: Dustin Lacewell Date: Tue, 1 Sep 2026 23:20:14 -0500 Subject: [PATCH] fix(scan): use fileURLToPath for the entrypoint scan root scanQwikEntrypoints used config.root.pathname as the grep cwd and as the base for resolve(). On Windows that value is "/C:/project/", which is not a valid filesystem path: spawn fails with ENOENT, the error is swallowed, no entrypoints are found, the standalone client build is skipped, and every QRL reports Q14 during prerender. Convert the root URL with fileURLToPath so the cwd and the resolved absolute paths are native on every platform. Add unit tests that scan a real temp directory. --- libs/qwikdev-astro/src/scan.ts | 5 ++- libs/qwikdev-astro/tests/unit/scan.test.ts | 37 ++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/libs/qwikdev-astro/src/scan.ts b/libs/qwikdev-astro/src/scan.ts index d60ff1f3..e46beecf 100644 --- a/libs/qwikdev-astro/src/scan.ts +++ b/libs/qwikdev-astro/src/scan.ts @@ -1,5 +1,6 @@ import { execFile } from "node:child_process"; import { relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import type { AstroConfig } from "astro"; import { QWIK_ENTRYPOINT_PATTERN, SCAN_EXTENSIONS } from "./constants"; @@ -47,7 +48,9 @@ export async function scanQwikEntrypoints( filter: (id: string) => boolean, debug?: boolean ): Promise> { - const rootDir = config.root.pathname; + // fileURLToPath, not .pathname: on Windows .pathname is "/C:/project/", + // which is not a valid cwd (spawn fails with ENOENT) or resolve() base. + const rootDir = fileURLToPath(config.root); const stdout = await grepQwikFiles(rootDir); if (!stdout) return new Set(); diff --git a/libs/qwikdev-astro/tests/unit/scan.test.ts b/libs/qwikdev-astro/tests/unit/scan.test.ts index 318822cb..8367c12f 100644 --- a/libs/qwikdev-astro/tests/unit/scan.test.ts +++ b/libs/qwikdev-astro/tests/unit/scan.test.ts @@ -1,5 +1,9 @@ -import { describe, expect, it } from "vitest"; -import { createQwikFileFilter, resolveQwikPaths } from "../../src/scan"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createQwikFileFilter, resolveQwikPaths, scanQwikEntrypoints } from "../../src/scan"; describe("resolveQwikPaths", () => { function makeConfig(overrides: { adapter?: { name: string } } = {}) { @@ -33,6 +37,35 @@ describe("resolveQwikPaths", () => { }); }); +describe("scanQwikEntrypoints", () => { + let root: string; + + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), "qwik-astro-scan-")); + mkdirSync(join(root, "src")); + mkdirSync(join(root, "node_modules", "dep"), { recursive: true }); + writeFileSync(join(root, "src", "counter.tsx"), 'import { component$ } from "@qwik.dev/core";\n'); + writeFileSync(join(root, "src", "plain.tsx"), "export const x = 1;\n"); + writeFileSync(join(root, "node_modules", "dep", "index.js"), 'require("@qwik.dev/core");\n'); + }); + + afterEach(() => { + rmSync(root, { recursive: true, force: true }); + }); + + it("finds entrypoints as native absolute paths from a file URL root", async () => { + const config = { root: pathToFileURL(`${root}/`) } as any; + const entrypoints = await scanQwikEntrypoints(config, () => true); + expect([...entrypoints]).toEqual([join(root, "src", "counter.tsx")]); + }); + + it("respects the filter", async () => { + const config = { root: pathToFileURL(`${root}/`) } as any; + const entrypoints = await scanQwikEntrypoints(config, () => false); + expect(entrypoints.size).toBe(0); + }); +}); + describe("createQwikFileFilter", () => { it("always returns true for non-transform hooks", () => { const filter = createQwikFileFilter(() => true);