diff --git a/.changeset/honest-tips-tie.md b/.changeset/honest-tips-tie.md new file mode 100644 index 000000000000..0c93f980e8ad --- /dev/null +++ b/.changeset/honest-tips-tie.md @@ -0,0 +1,34 @@ +--- +"wrangler": patch +--- + +fix: store temporary files in `.wrangler` + +As Wrangler builds your code, it writes intermediate files to a temporary +directory that gets cleaned up on exit. Previously, Wrangler used the OS's +default temporary directory. On Windows, this is usually on the `C:` drive. +If your source code was on a different drive, our bundling tool would generate +invalid source maps, breaking breakpoint debugging. This change ensures +intermediate files are always written to the same drive as sources. It also +ensures unused build outputs are cleaned up when running `wrangler pages dev`. + +This change also means you no longer need to set `cwd` and +`resolveSourceMapLocations` in `.vscode/launch.json` when creating an `attach` +configuration for breakpoint debugging. Your `.vscode/launch.json` should now +look something like... + +```jsonc +{ + "configurations": [ + { + "name": "Wrangler", + "type": "node", + "request": "attach", + "port": 9229, + // These can be omitted, but doing so causes silent errors in the runtime + "attachExistingChildren": false, + "autoAttachChildProcesses": false + } + ] +} +``` diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 13eb13f9ee4f..f6c2101e43c0 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -56,7 +56,7 @@ "check:lint": "eslint .", "check:type": "tsc", "clean": "rimraf wrangler-dist miniflare-dist emitted-types", - "dev": "pnpm run clean && concurrently -c black,blue --kill-others-on-fail false 'pnpm run bundle --watch' 'pnpm run check:type --watch --preserveWatchOutput'", + "dev": "pnpm run clean && concurrently -c black,blue --kill-others-on-fail false \"pnpm run bundle --watch\" \"pnpm run check:type --watch --preserveWatchOutput\"", "emit-types": "tsc -p tsconfig.emit.json && node -r esbuild-register scripts/emit-types.ts", "prepublishOnly": "SOURCEMAPS=false npm run build", "start": "pnpm run bundle && cross-env NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js", @@ -194,7 +194,6 @@ "strip-ansi": "^7.0.1", "supports-color": "^9.2.2", "timeago.js": "^4.0.2", - "tmp-promise": "^3.0.3", "ts-dedent": "^2.2.0", "undici": "5.20.0", "update-check": "^1.5.4", diff --git a/packages/wrangler/src/api/pages/deploy.tsx b/packages/wrangler/src/api/pages/deploy.tsx index b7d691290661..7327b0268b38 100644 --- a/packages/wrangler/src/api/pages/deploy.tsx +++ b/packages/wrangler/src/api/pages/deploy.tsx @@ -1,5 +1,4 @@ import { existsSync, lstatSync, readFileSync } from "node:fs"; -import { tmpdir } from "node:os"; import { join, resolve as resolvePath } from "node:path"; import { cwd } from "node:process"; import { File, FormData } from "undici"; @@ -20,6 +19,7 @@ import { } from "../../pages/functions/buildWorker"; import { validateRoutes } from "../../pages/functions/routes-validation"; import { upload } from "../../pages/upload"; +import { getPagesTmpDir } from "../../pages/utils"; import { validate } from "../../pages/validate"; import { createUploadWorkerBundleContents } from "./create-worker-bundle-contents"; import type { BundleResult } from "../../deployment-bundle/bundle"; @@ -154,7 +154,7 @@ export async function deploy({ const functionsDirectory = customFunctionsDirectory || join(cwd(), "functions"); const routesOutputPath = !existsSync(join(directory, "_routes.json")) - ? join(tmpdir(), `_routes-${Math.random()}.json`) + ? join(getPagesTmpDir(), `_routes-${Math.random()}.json`) : undefined; // Routing configuration displayed in the Functions tab of a deployment in Dash @@ -162,7 +162,7 @@ export async function deploy({ if (!_workerJS && existsSync(functionsDirectory)) { const outputConfigPath = join( - tmpdir(), + getPagesTmpDir(), `functions-filepath-routing-config-${Math.random()}.json` ); @@ -257,7 +257,10 @@ export async function deploy({ }); } else if (_workerJS) { if (bundle) { - const outfile = join(tmpdir(), `./bundledWorker-${Math.random()}.mjs`); + const outfile = join( + getPagesTmpDir(), + `./bundledWorker-${Math.random()}.mjs` + ); workerBundle = await buildRawWorker({ workerScriptPath: _workerPath, outfile, diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index eb0ac5441c40..3c68c7d5c86b 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -3,7 +3,6 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; import { URLSearchParams } from "node:url"; import chalk from "chalk"; -import tmp from "tmp-promise"; import { fetchListResult, fetchResult } from "../cfetch"; import { printBindings } from "../config"; import { bundleWorker } from "../deployment-bundle/bundle"; @@ -27,6 +26,7 @@ import { getMigrationsToUpload } from "../durable"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; import { ParseError } from "../parse"; +import { getWranglerTmpDir } from "../paths"; import { getQueue, putConsumer } from "../queues/client"; import { getWorkersDevSubdomain } from "../routes"; import { syncAssets } from "../sites"; @@ -72,6 +72,7 @@ type Props = { keepVars: boolean | undefined; logpush: boolean | undefined; oldAssetTtl: number | undefined; + projectRoot: string | undefined; }; type RouteObject = ZoneIdRoute | ZoneNameRoute | CustomDomainRoute; @@ -407,7 +408,8 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ); } - const destination = props.outDir ?? (await tmp.dir({ unsafeCleanup: true })); + const destination = + props.outDir ?? getWranglerTmpDir(props.projectRoot, "deploy"); const envName = props.env ?? "production"; const start = Date.now(); @@ -507,6 +509,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m // This could potentially cause issues as we no longer have identical behaviour between dev and deploy? targetConsumer: "deploy", local: false, + projectRoot: props.projectRoot, } ); @@ -693,7 +696,7 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m if (typeof destination !== "string") { // this means we're using a temp dir, // so let's clean up before we proceed - await destination.cleanup(); + destination.remove(); } } diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index d5e393cfad47..144e1735bf7b 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -239,6 +239,7 @@ export async function deployHandler( const configPath = args.config || (args.script && findWranglerToml(path.dirname(args.script))); + const projectRoot = configPath && path.dirname(configPath); const config = readConfig(configPath, args); const entry = await getEntry(args, config, "deploy"); await metrics.sendMetricsEvent( @@ -319,5 +320,6 @@ export async function deployHandler( keepVars: args.keepVars, logpush: args.logpush, oldAssetTtl: args.oldAssetTtl, + projectRoot, }); } diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 43809a7c83db..f848c08e4a5f 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -3,8 +3,7 @@ import * as path from "node:path"; import NodeGlobalsPolyfills from "@esbuild-plugins/node-globals-polyfill"; import NodeModulesPolyfills from "@esbuild-plugins/node-modules-polyfill"; import * as esbuild from "esbuild"; -import tmp from "tmp-promise"; -import { getBasePath } from "../paths"; +import { getBasePath, getWranglerTmpDir } from "../paths"; import { applyMiddlewareLoaderFacade } from "./apply-middleware"; import { isBuildFailure, @@ -85,6 +84,7 @@ export type BundleOptions = { isOutfile?: boolean; forPages?: boolean; local: boolean; + projectRoot: string | undefined; }; /** @@ -122,15 +122,13 @@ export async function bundleWorker( isOutfile, forPages, local, + projectRoot, }: BundleOptions ): Promise { // We create a temporary directory for any one-off files we // need to create. This is separate from the main build // directory (`destination`). - const unsafeTmpDir = await tmp.dir({ unsafeCleanup: true }); - // Make sure we resolve all files relative to the actual temporary directory, - // without symlinks, otherwise `esbuild` will generate invalid source maps. - const tmpDirPath = fs.realpathSync(unsafeTmpDir.path); + const tmpDir = getWranglerTmpDir(projectRoot, "bundle"); const entryFile = entry.file; @@ -234,12 +232,9 @@ export async function bundleWorker( // we need to extract that file to an accessible place before injecting // it in, hence this code here. - const checkedFetchFileToInject = path.join(tmpDirPath, "checked-fetch.js"); + const checkedFetchFileToInject = path.join(tmpDir.path, "checked-fetch.js"); if (checkFetch && !fs.existsSync(checkedFetchFileToInject)) { - fs.mkdirSync(tmpDirPath, { - recursive: true, - }); fs.writeFileSync( checkedFetchFileToInject, fs.readFileSync( @@ -264,7 +259,7 @@ export async function bundleWorker( ) { const result = await applyMiddlewareLoaderFacade( entry, - tmpDirPath, + tmpDir.path, middlewareToLoad, doBindings ); @@ -365,10 +360,16 @@ export async function bundleWorker( } stop = async function () { + tmpDir.remove(); await ctx.dispose(); }; } else { result = await esbuild.build(buildOptions); + // Even when we're not watching, we still want some way of cleaning up the + // temporary directory when we don't need it anymore + stop = async function () { + tmpDir.remove(); + }; } } catch (e) { if (!legacyNodeCompat && isBuildFailure(e)) @@ -405,7 +406,7 @@ export async function bundleWorker( stop, sourceMapPath, sourceMapMetadata: { - tmpDir: tmpDirPath, + tmpDir: tmpDir.path, entryDirectory: entry.directory, }, }; diff --git a/packages/wrangler/src/dev.tsx b/packages/wrangler/src/dev.tsx index 28bf91392a3d..887096020b8f 100644 --- a/packages/wrangler/src/dev.tsx +++ b/packages/wrangler/src/dev.tsx @@ -366,6 +366,7 @@ export async function startDev(args: StartDevOptions) { const configPath = args.config || (args.script && findWranglerToml(path.dirname(args.script))); + const projectRoot = configPath && path.dirname(configPath); let config = readConfig(configPath, args); if (config.configPath) { @@ -476,6 +477,7 @@ export async function startDev(args: StartDevOptions) { firstPartyWorker={configParam.first_party_worker} sendMetrics={configParam.send_metrics} testScheduled={args.testScheduled} + projectRoot={projectRoot} /> ); } @@ -520,6 +522,7 @@ export async function startApiDev(args: StartDevOptions) { const configPath = args.config || (args.script && findWranglerToml(path.dirname(args.script))); + const projectRoot = configPath && path.dirname(configPath); const config = readConfig(configPath, args); const { @@ -616,6 +619,7 @@ export async function startApiDev(args: StartDevOptions) { sendMetrics: configParam.send_metrics, testScheduled: args.testScheduled, disableDevRegistry: args.disableDevRegistry ?? false, + projectRoot, }); } diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index de32921899fb..07674e4214a6 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -1,5 +1,4 @@ import { spawn } from "node:child_process"; -import fs from "node:fs"; import * as path from "node:path"; import * as util from "node:util"; import { watch } from "chokidar"; @@ -9,7 +8,6 @@ import { Box, Text, useApp, useInput, useStdin } from "ink"; import React, { useEffect, useRef, useState } from "react"; import { useErrorHandler, withErrorBoundary } from "react-error-boundary"; import onExit from "signal-exit"; -import tmp from "tmp-promise"; import { fetch } from "undici"; import { runCustomBuild } from "../deployment-bundle/run-custom-build"; import { @@ -20,6 +18,7 @@ import { } from "../dev-registry"; import { logger } from "../logger"; import openInBrowser from "../open-in-browser"; +import { getWranglerTmpDir } from "../paths"; import { openInspector } from "./inspect"; import { Local } from "./local"; import { Remote } from "./remote"; @@ -31,6 +30,7 @@ import type { Entry } from "../deployment-bundle/entry"; import type { CfModule, CfWorkerInit } from "../deployment-bundle/worker"; import type { WorkerRegistry } from "../dev-registry"; import type { EnablePagesAssetsServiceBindingOptions } from "../miniflare-cli/types"; +import type { EphemeralDirectory } from "../paths"; import type { AssetPaths } from "../sites"; /** @@ -164,6 +164,7 @@ export type DevProps = { firstPartyWorker: boolean | undefined; sendMetrics: boolean | undefined; testScheduled: boolean | undefined; + projectRoot: string | undefined; }; export function DevImplementation(props: DevProps): JSX.Element { @@ -248,7 +249,7 @@ type DevSessionProps = DevProps & { function DevSession(props: DevSessionProps) { useCustomBuild(props.entry, props.build); - const directory = useTmpDir(); + const directory = useTmpDir(props.projectRoot); const workerDefinitions = useDevRegistry( props.name, @@ -284,6 +285,7 @@ function DevSession(props: DevSessionProps) { targetConsumer: "dev", testScheduled: props.testScheduled ?? false, experimentalLocal: props.experimentalLocal, + projectRoot: props.projectRoot, }); // TODO(queues) support remote wrangler dev @@ -376,26 +378,14 @@ function DevSession(props: DevSessionProps) { ); } -export interface DirectorySyncResult { - name: string; - removeCallback: () => void; -} - -function useTmpDir(): string | undefined { +function useTmpDir(projectRoot: string | undefined): string | undefined { const [directory, setDirectory] = useState(); const handleError = useErrorHandler(); useEffect(() => { - let dir: DirectorySyncResult | undefined; + let dir: EphemeralDirectory | undefined; try { - // const tmpdir = path.resolve(".wrangler", "tmp"); - // fs.mkdirSync(tmpdir, { recursive: true }); - // dir = tmp.dirSync({ unsafeCleanup: true, tmpdir }); - dir = tmp.dirSync({ unsafeCleanup: true }) as DirectorySyncResult; - // Make sure we resolve all files relative to the actual temporary - // directory, without symlinks, otherwise `esbuild` will generate invalid - // source maps. - const realpath = fs.realpathSync(dir.name); - setDirectory(realpath); + dir = getWranglerTmpDir(projectRoot, "dev"); + setDirectory(dir.path); return; } catch (err) { logger.error( @@ -403,10 +393,8 @@ function useTmpDir(): string | undefined { ); handleError(err); } - return () => { - dir?.removeCallback(); - }; - }, [handleError]); + return () => dir?.remove(); + }, [projectRoot, handleError]); return directory; } diff --git a/packages/wrangler/src/dev/start-server.ts b/packages/wrangler/src/dev/start-server.ts index eab09d66c1a4..128b754dc293 100644 --- a/packages/wrangler/src/dev/start-server.ts +++ b/packages/wrangler/src/dev/start-server.ts @@ -1,9 +1,7 @@ -import fs from "node:fs"; import * as path from "node:path"; import * as util from "node:util"; import chalk from "chalk"; import onExit from "signal-exit"; -import tmp from "tmp-promise"; import { bundleWorker } from "../deployment-bundle/bundle"; import { getBundleType } from "../deployment-bundle/bundle-type"; import { dedupeModulesByName } from "../deployment-bundle/dedupe-modules"; @@ -20,6 +18,7 @@ import { stopWorkerRegistry, } from "../dev-registry"; import { logger } from "../logger"; +import { getWranglerTmpDir } from "../paths"; import { localPropsToConfigBundle, maybeRegisterLocalWorker } from "./local"; import { MiniflareServer } from "./miniflare"; import { startRemoteServer } from "./remote"; @@ -29,7 +28,7 @@ import type { DurableObjectBindings } from "../config/environment"; import type { Entry } from "../deployment-bundle/entry"; import type { CfModule } from "../deployment-bundle/worker"; import type { WorkerRegistry } from "../dev-registry"; -import type { DevProps, DirectorySyncResult } from "./dev"; +import type { DevProps } from "./dev"; import type { LocalProps } from "./local"; import type { EsbuildBundle } from "./use-esbuild"; @@ -53,7 +52,7 @@ export async function startDevServer( } //implement a react-free version of useTmpDir - const directory = setupTempDir(); + const directory = setupTempDir(props.projectRoot); if (!directory) { throw new Error("Failed to create temporary directory."); } @@ -105,6 +104,7 @@ export async function startDevServer( testScheduled: props.testScheduled, local: props.local, doBindings: props.bindings.durable_objects?.bindings ?? [], + projectRoot: props.projectRoot, }); if (props.local) { @@ -178,13 +178,10 @@ export async function startDevServer( } } -function setupTempDir(): string | undefined { +function setupTempDir(projectRoot: string | undefined): string | undefined { try { - const dir: DirectorySyncResult = tmp.dirSync({ unsafeCleanup: true }); - // Make sure we resolve all files relative to the actual temporary - // directory, without symlinks, otherwise `esbuild` will generate invalid - // source maps. - return fs.realpathSync(dir.name); + const dir = getWranglerTmpDir(projectRoot, "dev"); + return dir.path; } catch (err) { logger.error("Failed to create temporary directory to store built files."); } @@ -212,6 +209,7 @@ async function runEsbuild({ testScheduled, local, doBindings, + projectRoot, }: { entry: Entry; destination: string | undefined; @@ -234,6 +232,7 @@ async function runEsbuild({ testScheduled?: boolean; local: boolean; doBindings: DurableObjectBindings; + projectRoot: string | undefined; }): Promise { if (!destination) return; @@ -281,6 +280,7 @@ async function runEsbuild({ local, testScheduled, doBindings, + projectRoot, }) : undefined; diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 808e9079b38e..876dc6963be4 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -56,6 +56,7 @@ export function useEsbuild({ targetConsumer, testScheduled, experimentalLocal, + projectRoot, }: { entry: Entry; destination: string | undefined; @@ -80,6 +81,7 @@ export function useEsbuild({ targetConsumer: "dev" | "deploy"; testScheduled: boolean; experimentalLocal: boolean | undefined; + projectRoot: string | undefined; }): EsbuildBundle | undefined { const [bundle, setBundle] = useState(); const { exit } = useApp(); @@ -182,6 +184,7 @@ export function useEsbuild({ testScheduled, plugins: [onEnd], local, + projectRoot, }) : undefined; @@ -248,6 +251,7 @@ export function useEsbuild({ targetConsumer, testScheduled, experimentalLocal, + projectRoot, ]); return bundle; } diff --git a/packages/wrangler/src/pages/buildFunctions.ts b/packages/wrangler/src/pages/buildFunctions.ts index c1d2fc8abdac..b30273537bf3 100644 --- a/packages/wrangler/src/pages/buildFunctions.ts +++ b/packages/wrangler/src/pages/buildFunctions.ts @@ -8,7 +8,7 @@ import { buildWorkerFromFunctions } from "./functions/buildWorker"; import { generateConfigFromFileTree } from "./functions/filepath-routing"; import { writeRoutesModule } from "./functions/routes"; import { convertRoutesToRoutesJSONSpec } from "./functions/routes-transformation"; -import { realTmpdir, RUNNING_BUILDERS } from "./utils"; +import { getPagesTmpDir, RUNNING_BUILDERS } from "./utils"; import type { BundleResult } from "../deployment-bundle/bundle"; import type { PagesBuildArgs } from "./build"; import type { Config } from "./functions/routes"; @@ -34,6 +34,10 @@ export async function buildFunctions({ legacyNodeCompat, nodejsCompat, local, + routesModule = join( + getPagesTmpDir(), + `./functionsRoutes-${Math.random()}.mjs` + ), }: Partial< Pick< PagesBuildArgs, @@ -54,15 +58,14 @@ export async function buildFunctions({ local: boolean; legacyNodeCompat?: boolean; nodejsCompat?: boolean; + // Allow `routesModule` to be fixed, so we don't create a new file in the + // temporary directory each time + routesModule?: string; }) { RUNNING_BUILDERS.forEach( (runningBuilder) => runningBuilder.stop && runningBuilder.stop() ); - const routesModule = join( - realTmpdir(), - `./functionsRoutes-${Math.random()}.mjs` - ); const baseURL = toUrlPath("/"); const config: Config = await generateConfigFromFileTree({ diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 478b5296319a..95c144a0f0f4 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -26,7 +26,7 @@ import { traverseAndBuildWorkerJSDirectory, } from "./functions/buildWorker"; import { validateRoutes } from "./functions/routes-validation"; -import { CLEANUP, CLEANUP_CALLBACKS, realTmpdir } from "./utils"; +import { CLEANUP, CLEANUP_CALLBACKS, getPagesTmpDir } from "./utils"; import type { CfModule } from "../deployment-bundle/worker"; import type { AdditionalDevProps } from "../dev"; import type { @@ -332,7 +332,10 @@ export const Handler = async ({ // We want to actually run the `_worker.js` script through the bundler // So update the final path to the script that will be uploaded and // change the `runBuild()` function to bundle the `_worker.js`. - scriptPath = join(realTmpdir(), `./bundledWorker-${Math.random()}.mjs`); + scriptPath = join( + getPagesTmpDir(), + `./bundledWorker-${Math.random()}.mjs` + ); runBuild = async () => { try { await buildRawWorker({ @@ -362,7 +365,14 @@ export const Handler = async ({ }); } else if (usingFunctions) { // Try to use Functions - scriptPath = join(realTmpdir(), `./functionsWorker-${Math.random()}.mjs`); + scriptPath = join( + getPagesTmpDir(), + `./functionsWorker-${Math.random()}.mjs` + ); + const routesModule = join( + getPagesTmpDir(), + `./functionsRoutes-${Math.random()}.mjs` + ); if (legacyNodeCompat) { console.warn( @@ -391,6 +401,7 @@ export const Handler = async ({ legacyNodeCompat, nodejsCompat, local: true, + routesModule, }); await metrics.sendMetricsEvent("build pages functions"); }; @@ -500,7 +511,7 @@ export const Handler = async ({ validateRoutes(JSON.parse(routesJSONContents), directory); entrypoint = join( - realTmpdir(), + getPagesTmpDir(), `${Math.random().toString(36).slice(2)}.js` ); await runBuild(scriptPath, entrypoint, routesJSONContents); diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index 680e1a4787fe..3fafa8e94c53 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -3,6 +3,7 @@ import { relative, resolve } from "node:path"; import { bundleWorker } from "../../deployment-bundle/bundle"; import { createModuleCollector } from "../../deployment-bundle/module-collection"; import { getBasePath } from "../../paths"; +import { getPagesProjectRoot } from "../utils"; import { buildNotifierPlugin } from "./buildWorker"; import type { Entry } from "../../deployment-bundle/entry"; import type { Options as WorkerOptions } from "./buildWorker"; @@ -105,5 +106,6 @@ export function buildPluginFromFunctions({ targetConsumer: local ? "dev" : "deploy", forPages: true, local, + projectRoot: getPagesProjectRoot(), }); } diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index 77492747cc4c..9472a4e6ecca 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -11,7 +11,7 @@ import { import { FatalError } from "../../errors"; import { logger } from "../../logger"; import { getBasePath } from "../../paths"; -import { realTmpdir } from "../utils"; +import { getPagesProjectRoot, getPagesTmpDir } from "../utils"; import type { BundleResult } from "../../deployment-bundle/bundle"; import type { Entry } from "../../deployment-bundle/entry"; import type { CfModule } from "../../deployment-bundle/worker"; @@ -35,7 +35,7 @@ export type Options = { export function buildWorkerFromFunctions({ routesModule, - outfile = join(realTmpdir(), `./functionsWorker-${Math.random()}.js`), + outfile = join(getPagesTmpDir(), `./functionsWorker-${Math.random()}.js`), outdir, minify = false, sourcemap = false, @@ -157,6 +157,7 @@ export function buildWorkerFromFunctions({ targetConsumer: local ? "dev" : "deploy", forPages: true, local, + projectRoot: getPagesProjectRoot(), }); } @@ -188,7 +189,7 @@ export type RawOptions = { */ export function buildRawWorker({ workerScriptPath, - outfile = join(realTmpdir(), `./functionsWorker-${Math.random()}.js`), + outfile = join(getPagesTmpDir(), `./functionsWorker-${Math.random()}.js`), outdir, directory, bundle = true, @@ -250,6 +251,7 @@ export function buildRawWorker({ targetConsumer: local ? "dev" : "deploy", forPages: true, local, + projectRoot: getPagesProjectRoot(), }); } @@ -279,7 +281,10 @@ export async function traverseAndBuildWorkerJSDirectory({ ] ); - const outfile = join(realTmpdir(), `./bundledWorker-${Math.random()}.mjs`); + const outfile = join( + getPagesTmpDir(), + `./bundledWorker-${Math.random()}.mjs` + ); const bundleResult = await buildRawWorker({ workerScriptPath: entrypoint, bundle: true, diff --git a/packages/wrangler/src/pages/utils.ts b/packages/wrangler/src/pages/utils.ts index ea5ef323cafa..f2b55fa677af 100644 --- a/packages/wrangler/src/pages/utils.ts +++ b/packages/wrangler/src/pages/utils.ts @@ -1,5 +1,6 @@ -import fs from "node:fs"; -import os from "node:os"; +import path from "node:path"; +import { findUpSync } from "find-up"; +import { getWranglerTmpDir } from "../paths"; import type { BundleResult } from "../deployment-bundle/bundle"; export const RUNNING_BUILDERS: BundleResult[] = []; @@ -21,13 +22,54 @@ export function isUrl(maybeUrl?: string): maybeUrl is string { } } -let realTmpdirCache: string | undefined; +// Wrangler's tests change `process.cwd()` to be a different temporary directory +// for each test. We want to invalidate our `projectRootCache` and `tmpDirCache` +// if this changes, so store the `cwd` and `projectRoot` used to compute each +// cache, and recompute when requested if it's different. + +let projectRootCacheCwd: string | undefined; +let projectRootCache: string | undefined; + +let tmpDirCacheProjectRoot: string | undefined; +let tmpDirCache: string | undefined; + /** - * Returns the realpath of the temporary directory without symlinks. On macOS, - * `os.tmpdir()` will return a symlink. Running `esbuild` and outputting to - * paths in this symlinked-directory results in invalid relative URLs in source - * maps. Resolving symlinks first ensures we always generate valid source maps. + * Returns the "project root" for the current process. Normally, this would be + * the directory containing the config file, but Pages doesn't really have a + * config file, so we use the closest directory containing a `package.json` + * instead. If no `package.json` file could be found, we just use the current + * working directory. */ -export function realTmpdir(): string { - return (realTmpdirCache ??= fs.realpathSync(os.tmpdir())); +export function getPagesProjectRoot(): string { + const cwd = process.cwd(); + if (projectRootCache !== undefined && projectRootCacheCwd === cwd) { + return projectRootCache; + } + const packagePath = findUpSync("package.json"); + projectRootCache = packagePath ? path.dirname(packagePath) : process.cwd(); + projectRootCacheCwd = cwd; + return projectRootCache; +} + +/** + * Returns the temporary directory to use for the current process. This uses + * `getWranglerTmpDir()` to create a temporary directory in the project's + * `.wrangler` folder to avoid issues with different drive letters on Windows. + * + * Normally, we'd create a temporary directory at program startup as required, + * but Pages uses a temporary directory in lots of places (including default + * arguments for functions), so passing it around would be a bit messy. We also + * want to minimise the number of temporary directories we create. Pages has + * code to append random identifiers at the end of files names it creates, so + * reusing the directory is fine. + */ +export function getPagesTmpDir(): string { + const projectRoot = getPagesProjectRoot(); + if (tmpDirCache !== undefined && tmpDirCacheProjectRoot === projectRoot) { + return tmpDirCache; + } + const tmpDir = getWranglerTmpDir(getPagesProjectRoot(), "pages"); + tmpDirCache = tmpDir.path; + tmpDirCacheProjectRoot = projectRoot; + return tmpDirCache; } diff --git a/packages/wrangler/src/paths.ts b/packages/wrangler/src/paths.ts index a24bc91ffad1..241b717ca999 100644 --- a/packages/wrangler/src/paths.ts +++ b/packages/wrangler/src/paths.ts @@ -1,5 +1,7 @@ import { assert } from "node:console"; -import { relative, basename, resolve } from "node:path"; +import fs from "node:fs"; +import path from "node:path"; +import onExit from "signal-exit"; type DiscriminatedPath = string & { _discriminator: Discriminator; @@ -39,10 +41,10 @@ export function toUrlPath(filePath: string): UrlPath { * * */ export function readableRelative(to: string) { - const relativePath = relative(process.cwd(), to); + const relativePath = path.relative(process.cwd(), to); if ( // No directory nesting, return as-is - basename(relativePath) === relativePath || + path.basename(relativePath) === relativePath || // Outside current directory relativePath.startsWith(".") ) { @@ -67,5 +69,44 @@ declare const __RELATIVE_PACKAGE_PATH__: string; */ export function getBasePath(): string { // eslint-disable-next-line no-restricted-globals - return resolve(__dirname, __RELATIVE_PACKAGE_PATH__); + return path.resolve(__dirname, __RELATIVE_PACKAGE_PATH__); +} + +/** + * A short-lived directory. Automatically removed when the process exits, but + * can be removed earlier by calling `remove()`. + */ +export interface EphemeralDirectory { + path: string; + remove(): void; +} + +/** + * Gets a temporary directory in the project's `.wrangler` folder with the + * specified prefix. We create temporary directories in `.wrangler` as opposed + * to the OS's temporary directory to avoid issues with different drive letters + * on Windows. For example, when `esbuild` outputs a file to a different drive + * than the input sources, the generated source maps are incorrect. + */ +export function getWranglerTmpDir( + projectRoot: string | undefined, + prefix: string +): EphemeralDirectory { + projectRoot ??= process.cwd(); + const tmpRoot = path.join(projectRoot, ".wrangler", "tmp"); + fs.mkdirSync(tmpRoot, { recursive: true }); + + const tmpPrefix = path.join(tmpRoot, `${prefix}-`); + const tmpDir = fs.realpathSync(fs.mkdtempSync(tmpPrefix)); + + const removeDir = () => fs.rmSync(tmpDir, { recursive: true, force: true }); + const removeExitListener = onExit(removeDir); + + return { + path: tmpDir, + remove() { + removeExitListener(); + removeDir(); + }, + }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03ea2d39cb0e..ab27544b27b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1321,9 +1321,6 @@ importers: timeago.js: specifier: ^4.0.2 version: 4.0.2 - tmp-promise: - specifier: ^3.0.3 - version: 3.0.3 ts-dedent: specifier: ^2.2.0 version: 2.2.0 @@ -16281,25 +16278,12 @@ packages: engines: {node: '>=12'} dev: true - /tmp-promise@3.0.3: - resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} - dependencies: - tmp: 0.2.1 - dev: true - /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} dependencies: os-tmpdir: 1.0.2 - /tmp@0.2.1: - resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} - engines: {node: '>=8.17.0'} - dependencies: - rimraf: 3.0.2 - dev: true - /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true