diff --git a/.changeset/lucky-chairs-relate.md b/.changeset/lucky-chairs-relate.md new file mode 100644 index 000000000000..ac272c856a3f --- /dev/null +++ b/.changeset/lucky-chairs-relate.md @@ -0,0 +1,9 @@ +--- +"create-cloudflare": patch +--- + +Fix --variant flag being ignored for pages + +When creating a Pages project using `npm create cloudflare -- --type pages --variant `, +the `--variant` flag was being ignored, causing users to be prompted for variant selection +or defaulting to an unexpected variant. This now correctly passes the variant to the project setup. diff --git a/.changeset/reduce-fs-errors.md b/.changeset/reduce-fs-errors.md new file mode 100644 index 000000000000..4a0c473ce6e4 --- /dev/null +++ b/.changeset/reduce-fs-errors.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +"miniflare": patch +"@cloudflare/vitest-pool-workers": patch +"@cloudflare/workers-utils": patch +"wrangler": patch +--- + +Optimize filesystem operations by using Node.js's throwIfNoEntry: false option + +This reduces the number of system calls made when checking for file existence by avoiding the overhead of throwing and catching errors for missing paths. This is an internal performance optimization with no user-visible behavioral changes. diff --git a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts index db149e4eb051..1d4b83cbfdf8 100644 --- a/packages/create-cloudflare/e2e/tests/cli/cli.test.ts +++ b/packages/create-cloudflare/e2e/tests/cli/cli.test.ts @@ -769,6 +769,45 @@ describe("Create Cloudflare CLI", () => { 'Unknown variant "invalid-variant". Valid variants are: react-ts, react-swc-ts, react, react-swc', ); }); + + test("error when using invalid --variant for React Pages framework", async ({ + logStream, + }) => { + const { errors } = await runC3( + [ + "my-app", + "--framework=react", + "--platform=pages", + "--variant=invalid-variant", + "--no-deploy", + "--git=false", + ], + [], + logStream, + ); + expect(errors).toContain( + 'Unknown variant "invalid-variant". Valid variants are: react-ts, react-swc-ts, react, react-swc', + ); + }); + + test("accepts --variant for React Pages framework without prompting", async ({ + logStream, + }) => { + const { output } = await runC3( + [ + "my-app", + "--framework=react", + "--platform=pages", + "--variant=react-ts", + "--no-deploy", + "--git=false", + ], + [], + logStream, + ); + expect(output).toContain("--template react-ts"); + expect(output).not.toContain("Select a variant"); + }); }); }); diff --git a/packages/create-cloudflare/src/helpers/codemod.ts b/packages/create-cloudflare/src/helpers/codemod.ts index c84c4fa552b1..1b395ebd996b 100644 --- a/packages/create-cloudflare/src/helpers/codemod.ts +++ b/packages/create-cloudflare/src/helpers/codemod.ts @@ -1,4 +1,4 @@ -import { existsSync, lstatSync, readdirSync } from "node:fs"; +import { lstatSync, readdirSync } from "node:fs"; import nodePath, { extname, join } from "node:path"; import * as recast from "recast"; import * as esprimaParser from "recast/parsers/esprima"; @@ -82,11 +82,7 @@ export const transformFile = ( export const loadSnippets = (parentFolder: string) => { const snippetsPath = join(parentFolder, "snippets"); - if (!existsSync(snippetsPath)) { - return {}; - } - - if (!lstatSync(snippetsPath).isDirectory) { + if (!lstatSync(snippetsPath, { throwIfNoEntry: false })?.isDirectory()) { return {}; } @@ -95,7 +91,7 @@ export const loadSnippets = (parentFolder: string) => { return ( files // don't try loading directories - .filter((fileName) => lstatSync(join(snippetsPath, fileName)).isFile) + .filter((fileName) => lstatSync(join(snippetsPath, fileName)).isFile()) // only load js or ts files .filter((fileName) => [".js", ".ts"].includes(extname(fileName))) .reduce((acc, snippetPath) => { diff --git a/packages/create-cloudflare/src/helpers/files.ts b/packages/create-cloudflare/src/helpers/files.ts index b30b5e0924c2..df03b9ff0d4b 100644 --- a/packages/create-cloudflare/src/helpers/files.ts +++ b/packages/create-cloudflare/src/helpers/files.ts @@ -46,12 +46,9 @@ export const removeFile = (path: string) => { export const directoryExists = (path: string): boolean => { try { - const stat = statSync(path); - return stat.isDirectory(); + const stat = statSync(path, { throwIfNoEntry: false }); + return stat?.isDirectory() ?? false; } catch (error) { - if ((error as { code: string }).code === "ENOENT") { - return false; - } throw new Error(error as string); } }; diff --git a/packages/create-cloudflare/src/helpers/global-wrangler-config-path.ts b/packages/create-cloudflare/src/helpers/global-wrangler-config-path.ts index 2474f394c506..4eed86a9e8fe 100644 --- a/packages/create-cloudflare/src/helpers/global-wrangler-config-path.ts +++ b/packages/create-cloudflare/src/helpers/global-wrangler-config-path.ts @@ -5,12 +5,9 @@ import nodePath from "node:path"; import xdgAppPaths from "xdg-app-paths"; function isDirectory(configPath: string) { - try { - return fs.statSync(configPath).isDirectory(); - } catch { - // ignore error - return false; - } + return ( + fs.statSync(configPath, { throwIfNoEntry: false })?.isDirectory() ?? false + ); } export function getGlobalWranglerConfigPath() { diff --git a/packages/create-cloudflare/templates/react/pages/c3.ts b/packages/create-cloudflare/templates/react/pages/c3.ts index e2b2a9ead021..1efb4ed8bee5 100644 --- a/packages/create-cloudflare/templates/react/pages/c3.ts +++ b/packages/create-cloudflare/templates/react/pages/c3.ts @@ -8,13 +8,7 @@ import type { C3Context } from "types"; const { npm } = detectPackageManager(); const generate = async (ctx: C3Context) => { - const variant = await inputPrompt({ - type: "select", - question: "Select a variant:", - label: "variant", - options: variantsOptions, - defaultValue: variantsOptions[0].value, - }); + const variant = await getVariant(ctx); await runFrameworkGenerator(ctx, [ctx.project.name, "--template", variant]); @@ -40,6 +34,26 @@ const variantsOptions = [ }, ]; +async function getVariant(ctx: C3Context) { + if (ctx.args.variant) { + const selected = variantsOptions.find((v) => v.value === ctx.args.variant); + if (!selected) { + throw new Error( + `Unknown variant "${ctx.args.variant}". Valid variants are: ${variantsOptions.map((v) => v.value).join(", ")}`, + ); + } + return selected.value; + } + + return await inputPrompt({ + type: "select", + question: "Select a variant:", + label: "variant", + options: variantsOptions, + defaultValue: variantsOptions[0].value, + }); +} + const config: TemplateConfig = { configVersion: 1, id: "react", diff --git a/packages/miniflare/src/shared/wrangler.ts b/packages/miniflare/src/shared/wrangler.ts index a88e2181ee99..af77b6e6b711 100644 --- a/packages/miniflare/src/shared/wrangler.ts +++ b/packages/miniflare/src/shared/wrangler.ts @@ -4,12 +4,9 @@ import path from "node:path"; import xdgAppPaths from "xdg-app-paths"; function isDirectory(configPath: string) { - try { - return fs.statSync(configPath).isDirectory(); - } catch { - // ignore error - return false; - } + return ( + fs.statSync(configPath, { throwIfNoEntry: false })?.isDirectory() ?? false + ); } export function getGlobalWranglerConfigPath() { diff --git a/packages/vite-plugin-cloudflare/e2e/fixtures/basic/api/index.ts b/packages/vite-plugin-cloudflare/e2e/fixtures/basic/api/index.ts index 690cecefa492..74f062a6dbb0 100644 --- a/packages/vite-plugin-cloudflare/e2e/fixtures/basic/api/index.ts +++ b/packages/vite-plugin-cloudflare/e2e/fixtures/basic/api/index.ts @@ -28,19 +28,26 @@ export default { requestAborted = true; }); - async function sendPing(writable: WritableStream) { - const writer = writable.getWriter(); - const enc = new TextEncoder(); + const { readable, writable } = new IdentityTransformStream(); + // Acquire the writer immediately before returning the Response + // to ensure the stream stays open for writes + const writer = writable.getWriter(); + const enc = new TextEncoder(); - for (let i = 0; i < 6; i++) { - // Send 'ping' every 500ms to keep the connection alive for 3 seconds - await writer.write(enc.encode("ping\r\n")); - await scheduler.wait(500); - } - } + ctx.waitUntil( + (async () => { + try { + for (let i = 0; i < 6; i++) { + // Send 'ping' every 500ms to keep the connection alive for 3 seconds + await writer.write(enc.encode("ping\r\n")); + await scheduler.wait(500); + } + } finally { + await writer.close(); + } + })() + ); - const { readable, writable } = new IdentityTransformStream(); - ctx.waitUntil(sendPing(writable)); return new Response(readable, { headers: { "Content-Type": "text/plain" }, }); diff --git a/packages/vitest-pool-workers/src/pool/module-fallback.ts b/packages/vitest-pool-workers/src/pool/module-fallback.ts index be05e0ab1ccd..ae7faa831c8d 100644 --- a/packages/vitest-pool-workers/src/pool/module-fallback.ts +++ b/packages/vitest-pool-workers/src/pool/module-fallback.ts @@ -68,25 +68,13 @@ const forceModuleTypeRegexp = new RegExp( ); function isFile(filePath: string): boolean { - try { - return fs.statSync(filePath).isFile(); - } catch (e) { - if (isFileNotFoundError(e)) { - return false; - } - throw e; - } + return fs.statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false; } function isDirectory(filePath: string): boolean { - try { - return fs.statSync(filePath).isDirectory(); - } catch (e) { - if (isFileNotFoundError(e)) { - return false; - } - throw e; - } + return ( + fs.statSync(filePath, { throwIfNoEntry: false })?.isDirectory() ?? false + ); } function getParentPaths(filePath: string): string[] { diff --git a/packages/workers-utils/src/fs-helpers.ts b/packages/workers-utils/src/fs-helpers.ts index ab503cd9bb0c..7f6a5b369d69 100644 --- a/packages/workers-utils/src/fs-helpers.ts +++ b/packages/workers-utils/src/fs-helpers.ts @@ -9,10 +9,5 @@ import fs from "node:fs"; * @returns `true` if the path is a directory, `false` otherwise */ export function isDirectory(path: string) { - try { - return fs.statSync(path).isDirectory(); - } catch { - // ignore error - return false; - } + return fs.statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false; } diff --git a/packages/wrangler/src/api/pages/deploy.ts b/packages/wrangler/src/api/pages/deploy.ts index fe795db6e3eb..271fb6cc1336 100644 --- a/packages/wrangler/src/api/pages/deploy.ts +++ b/packages/wrangler/src/api/pages/deploy.ts @@ -144,12 +144,11 @@ export async function deploy({ _routesCustom = readFileSync(join(directory, "_routes.json"), "utf-8"); } catch {} - try { - _workerJSIsDirectory = lstatSync(_workerPath).isDirectory(); - if (!_workerJSIsDirectory) { - _workerJS = readFileSync(_workerPath, "utf-8"); - } - } catch {} + const workerJSStats = lstatSync(_workerPath, { throwIfNoEntry: false }); + _workerJSIsDirectory = workerJSStats?.isDirectory() ?? false; + if (workerJSStats !== undefined && !_workerJSIsDirectory) { + _workerJS = readFileSync(_workerPath, "utf-8"); + } // Grab the bindings from the API, we need these for shims and other such hacky inserts const project = await fetchResult( diff --git a/packages/wrangler/src/autoconfig/c3-vendor/codemod.ts b/packages/wrangler/src/autoconfig/c3-vendor/codemod.ts index 07856355d196..435a202a69b8 100644 --- a/packages/wrangler/src/autoconfig/c3-vendor/codemod.ts +++ b/packages/wrangler/src/autoconfig/c3-vendor/codemod.ts @@ -1,4 +1,4 @@ -import { existsSync, lstatSync, readdirSync, writeFileSync } from "node:fs"; +import { lstatSync, readdirSync, writeFileSync } from "node:fs"; import path, { extname, join } from "node:path"; import { readFileSync } from "@cloudflare/workers-utils"; import * as recast from "recast"; @@ -80,11 +80,7 @@ export const transformFile = ( export const loadSnippets = (parentFolder: string) => { const snippetsPath = join(parentFolder, "snippets"); - if (!existsSync(snippetsPath)) { - return {}; - } - - if (!lstatSync(snippetsPath).isDirectory) { + if (!lstatSync(snippetsPath, { throwIfNoEntry: false })?.isDirectory()) { return {}; } @@ -93,7 +89,7 @@ export const loadSnippets = (parentFolder: string) => { return ( files // don't try loading directories - .filter((fileName) => lstatSync(join(snippetsPath, fileName)).isFile) + .filter((fileName) => lstatSync(join(snippetsPath, fileName)).isFile()) // only load js or ts files .filter((fileName) => [".js", ".ts"].includes(extname(fileName))) .reduce((acc, snippetPath) => { diff --git a/packages/wrangler/src/autoconfig/git.ts b/packages/wrangler/src/autoconfig/git.ts index 49c04db444ef..7a4521f1ae4a 100644 --- a/packages/wrangler/src/autoconfig/git.ts +++ b/packages/wrangler/src/autoconfig/git.ts @@ -8,15 +8,8 @@ import { spinner } from "@cloudflare/cli/interactive"; // we should clean this duplication up function directoryExists(path: string): boolean { - try { - const stat = statSync(path); - return stat.isDirectory(); - } catch (error) { - if ((error as { code: string }).code === "ENOENT") { - return false; - } - throw new Error(error as string); - } + const stat = statSync(path, { throwIfNoEntry: false }); + return stat?.isDirectory() ?? false; } export async function appendToGitIgnore( diff --git a/packages/wrangler/src/miniflare-cli/assets.ts b/packages/wrangler/src/miniflare-cli/assets.ts index ff9877eed5ad..eea8b96b4dba 100644 --- a/packages/wrangler/src/miniflare-cli/assets.ts +++ b/packages/wrangler/src/miniflare-cli/assets.ts @@ -220,8 +220,7 @@ async function generateAssetsFetch( } if ( - existsSync(filepath) && - lstatSync(filepath).isFile() && + lstatSync(filepath, { throwIfNoEntry: false })?.isFile() && !ignoredFiles.includes(filepath) ) { const hash = hashFile(filepath); diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 8a664e2f129b..7218c2ffa7fa 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -414,7 +414,8 @@ export const pagesDevCommand = createCommand({ ? join(directory, singleWorkerScriptPath) : resolve(singleWorkerScriptPath); const usingWorkerDirectory = - existsSync(workerScriptPath) && lstatSync(workerScriptPath).isDirectory(); + lstatSync(workerScriptPath, { throwIfNoEntry: false })?.isDirectory() ?? + false; const usingWorkerScript = existsSync(workerScriptPath); const enableBundling = args.bundle ?? !(args.noBundle ?? config.no_bundle); diff --git a/packages/wrangler/src/pages/functions.ts b/packages/wrangler/src/pages/functions.ts index 316966244de7..43cdf90f26d3 100644 --- a/packages/wrangler/src/pages/functions.ts +++ b/packages/wrangler/src/pages/functions.ts @@ -40,8 +40,9 @@ export const pagesFunctionsOptimizeRoutesCommand = createCommand({ } if ( - !existsSync(routesOutputDirectory) || - !lstatSync(routesOutputDirectory).isDirectory() + !lstatSync(routesOutputDirectory, { + throwIfNoEntry: false, + })?.isDirectory() ) { throw new FatalError( `Oops! Folder ${routesOutputDirectory} does not exist. Please make sure --output-routes-path is a valid file path (for example "/public/_routes.json").`, diff --git a/packages/wrangler/src/r2/helpers/bulk.ts b/packages/wrangler/src/r2/helpers/bulk.ts index 8bf05bd308cd..67fea6b7431a 100644 --- a/packages/wrangler/src/r2/helpers/bulk.ts +++ b/packages/wrangler/src/r2/helpers/bulk.ts @@ -68,8 +68,8 @@ export function validateBulkPutFile( throw new UserError(`The file "${entry.file}" does not exist.`); } - const stat = fs.statSync(entry.file); - if (!stat.isFile()) { + const stat = fs.statSync(entry.file, { throwIfNoEntry: false }); + if (!stat?.isFile()) { throw new UserError(`The path "${entry.file}" is not a file.`); } diff --git a/packages/wrangler/src/r2/object.ts b/packages/wrangler/src/r2/object.ts index 014384fce387..7730085a770f 100644 --- a/packages/wrangler/src/r2/object.ts +++ b/packages/wrangler/src/r2/object.ts @@ -271,20 +271,23 @@ export const r2ObjectPutCommand = createCommand({ let sizeBytes: number; if (file) { try { - const stats = fs.statSync(file); + const stats = fs.statSync(file, { throwIfNoEntry: false }); + if (!stats) { + throw new UserError(`The file "${file}" does not exist.`); + } sizeBytes = stats.size; + objectStream = stream.Readable.toWeb(fs.createReadStream(file)); } catch (err) { - if ((err as NodeJS.ErrnoException).code === "ENOENT") { - throw new UserError(`The file "${file}" does not exist.`); + if (err instanceof UserError) { + throw err; } - const error = new UserError( + throw new UserError( `An error occurred while trying to read the file "${file}": ${ (err as Error).message - }` + }`, + { cause: err } ); - error.cause = err; - throw error; } } else { const buffer = await new Promise((resolve, reject) => { diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 6aaf01507969..a842f4f7f760 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -150,9 +150,11 @@ export const typesCommand = createCommand({ const { envInterface, path: outputPath } = args; if ( - !config.configPath || - !fs.existsSync(config.configPath) || - fs.statSync(config.configPath).isDirectory() + config.configPath == null || + (fs + .statSync(config.configPath, { throwIfNoEntry: false }) + ?.isDirectory() ?? + true) ) { throw new UserError( `No config file detected${args.config ? ` (at ${args.config})` : ""}. This command requires a Wrangler configuration file.`,