diff --git a/package-lock.json b/package-lock.json index 6b42b652c55b..927d64d0082e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3200,6 +3200,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "dev": true, @@ -14338,6 +14347,34 @@ "react-dom": ">=16.8" } }, + "node_modules/react-shallow-renderer": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", + "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0" + } + }, + "node_modules/react-test-renderer": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", + "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^17.0.2", + "react-shallow-renderer": "^16.13.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, "node_modules/read-package-json-fast": { "version": "2.0.3", "dev": true, @@ -17314,6 +17351,7 @@ "@types/mime": "^2.0.3", "@types/prompts": "^2.0.14", "@types/react": "^17.0.37", + "@types/react-test-renderer": "^17.0.1", "@types/serve-static": "^1.13.10", "@types/signal-exit": "^3.0.1", "@types/ws": "^8.2.1", @@ -17342,6 +17380,7 @@ "prompts": "^2.4.2", "react": "^17.0.2", "react-error-boundary": "^3.1.4", + "react-test-renderer": "^17.0.2", "serve-static": "^1.14.1", "signal-exit": "^3.0.6", "tmp-promise": "^3.0.3", @@ -19784,6 +19823,15 @@ "@types/react": "*" } }, + "@types/react-test-renderer": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", + "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/responselike": { "version": "1.0.0", "dev": true, @@ -26989,6 +27037,28 @@ "react-router": "6.2.1" } }, + "react-shallow-renderer": { + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", + "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0" + } + }, + "react-test-renderer": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", + "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "react-is": "^17.0.2", + "react-shallow-renderer": "^16.13.1", + "scheduler": "^0.20.2" + } + }, "read-package-json-fast": { "version": "2.0.3", "dev": true, @@ -28798,6 +28868,7 @@ "@types/mime": "^2.0.3", "@types/prompts": "^2.0.14", "@types/react": "^17.0.37", + "@types/react-test-renderer": "^17.0.1", "@types/serve-static": "^1.13.10", "@types/signal-exit": "^3.0.1", "@types/ws": "^8.2.1", @@ -28830,7 +28901,8 @@ "prompts": "^2.4.2", "react": "^17.0.2", "react-error-boundary": "^3.1.4", - "selfsigned": "*", + "react-test-renderer": "^17.0.2", + "selfsigned": "^2.0.0", "semiver": "^1.1.0", "serve-static": "^1.14.1", "signal-exit": "^3.0.6", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 887ddbe957cc..cd56eea176c2 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -56,6 +56,7 @@ "@types/mime": "^2.0.3", "@types/prompts": "^2.0.14", "@types/react": "^17.0.37", + "@types/react-test-renderer": "^17.0.1", "@types/serve-static": "^1.13.10", "@types/signal-exit": "^3.0.1", "@types/ws": "^8.2.1", @@ -83,6 +84,7 @@ "open": "^8.4.0", "prompts": "^2.4.2", "react": "^17.0.2", + "react-test-renderer": "^17.0.2", "react-error-boundary": "^3.1.4", "serve-static": "^1.14.1", "signal-exit": "^3.0.6", @@ -122,7 +124,8 @@ "node_modules/(?!find-up|locate-path|p-locate|p-limit|yocto-queue|path-exists|execa|strip-final-newline|npm-run-path|path-key|onetime|mimic-fn|human-signals|is-stream|get-port)" ], "moduleNameMapper": { - "clipboardy": "/src/__tests__/helpers/clipboardy-mock.js" + "clipboardy": "/src/__tests__/helpers/clipboardy-mock.js", + "miniflare/cli": "/../../node_modules/miniflare/dist/src/cli.js" }, "transform": { "^.+\\.c?(t|j)sx?$": [ diff --git a/packages/wrangler/src/__tests__/dev2.test.tsx b/packages/wrangler/src/__tests__/dev2.test.tsx new file mode 100644 index 000000000000..1e9bb3170627 --- /dev/null +++ b/packages/wrangler/src/__tests__/dev2.test.tsx @@ -0,0 +1,152 @@ +import * as fs from "node:fs"; +import React from "react"; +import TestRenderer from "react-test-renderer"; +import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; +import { unsetAllMocks } from "./helpers/mock-cfetch"; +import { mockConsoleMethods } from "./helpers/mock-console"; +import { runInTempDir } from "./helpers/run-in-tmp"; +import type { DevProps, default as DevType } from "../dev/dev"; + +function sleep(period = 100): Promise { + return new Promise((resolve) => setTimeout(resolve, period)); +} + +// we use this ["mock"] form to avoid esbuild-jest from rewriting it +jest["mock"]("../proxy", () => { + return { + usePreviewServer() {}, + waitForPortToBeAvailable() {}, + }; +}); +jest["mock"]("../inspect", () => { + return () => {}; +}); + +const Dev: typeof DevType = jest.requireActual("../dev/dev").DevImplementation; + +mockAccountId(); +mockApiToken(); +runInTempDir(); +mockConsoleMethods(); +afterEach(() => { + unsetAllMocks(); +}); + +describe("dev", () => { + it("should render", async () => { + fs.writeFileSync("./index.js", `export default {}`); + + const testRenderer = await renderDev({ + entry: { + file: "./index.js", + directory: process.cwd(), + format: "modules", + }, + }); + expect(testRenderer.toJSON()).toMatchInlineSnapshot(` + + + B to open a browser, D to open Devtools, L to turn off local mode, X to exit + + + `); + await TestRenderer.act(async () => { + // TODO: get rid of these sleep statements + await sleep(50); + testRenderer.unmount(); + await sleep(50); + }); + await sleep(50); + }); +}); + +async function renderDev({ + name, + entry = { file: "index.js", directory: "", format: "modules" }, + port, + inspectorPort = 9229, + accountId, + legacyEnv = true, + initialMode = "local", + jsxFactory, + jsxFragment, + localProtocol = "http", + upstreamProtocol = "https", + rules = [], + bindings = { + kv_namespaces: [], + vars: {}, + durable_objects: { bindings: [] }, + r2_buckets: [], + wasm_modules: undefined, + text_blobs: undefined, + unsafe: [], + }, + public: publicDir, + assetPaths, + compatibilityDate, + compatibilityFlags, + usageModel, + buildCommand = {}, + enableLocalPersistence = false, + env, + zone, +}: Partial): Promise { + let instance: TestRenderer.ReactTestRenderer | undefined; + await TestRenderer.act(async () => { + instance = TestRenderer.create( + + ); + }); + return instance as TestRenderer.ReactTestRenderer; +} diff --git a/packages/wrangler/src/dev/dev.tsx b/packages/wrangler/src/dev/dev.tsx index a315ffa0f611..2cd85274ec95 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -19,7 +19,6 @@ import type { Config } from "../config"; import type { Entry } from "../entry"; import type { AssetPaths } from "../sites"; import type { CfWorkerInit } from "../worker"; -import type { DirectoryResult } from "tmp-promise"; export type DevProps = { name?: string; @@ -55,7 +54,7 @@ export type DevProps = { | undefined; }; -function Dev(props: DevProps): JSX.Element { +export function DevImplementation(props: DevProps): JSX.Element { const port = props.port ?? 8787; const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined; const directory = useTmpDir(); @@ -149,35 +148,29 @@ function Dev(props: DevProps): JSX.Element { ); } +export interface DirectorySyncResult { + name: string; + removeCallback: () => void; +} + function useTmpDir(): string | undefined { - const [directory, setDirectory] = useState(); + const [directory, setDirectory] = useState(); const handleError = useErrorHandler(); useEffect(() => { - let dir: DirectoryResult; - async function create() { - try { - dir = await tmp.dir({ unsafeCleanup: true }); - setDirectory(dir); - return; - } catch (err) { - console.error("failed to create tmp dir"); - throw err; - } - } - create().catch((err) => { - // we want to break here - // we can't do much without a temp dir anyway + let dir: DirectorySyncResult | undefined; + try { + dir = tmp.dirSync({ unsafeCleanup: true }); + setDirectory(dir); + return; + } catch (err) { + console.error("failed to create tmp dir"); handleError(err); - }); + } return () => { - dir.cleanup().catch(() => { - // extremely unlikely, - // but it's 2022 after all - console.error("failed to cleanup tmp dir"); - }); + dir?.removeCallback(); }; }, [handleError]); - return directory?.path; + return directory?.name; } function useCustomBuild( @@ -261,7 +254,7 @@ function useTunnel(toggle: boolean) { ]); tunnel.current.on("close", (code) => { - if (code !== 0) { + if (code) { console.log(`Tunnel process exited with code ${code}`); } }); @@ -350,7 +343,7 @@ function useHotkeys( function ErrorFallback(props: { error: Error }) { const { exit } = useApp(); - useEffect(() => exit(new Error())); + useEffect(() => exit(props.error)); return ( <> Something went wrong: @@ -359,4 +352,6 @@ function ErrorFallback(props: { error: Error }) { ); } -export default withErrorBoundary(Dev, { FallbackComponent: ErrorFallback }); +export default withErrorBoundary(DevImplementation, { + FallbackComponent: ErrorFallback, +}); diff --git a/packages/wrangler/src/dev/local.tsx b/packages/wrangler/src/dev/local.tsx index d9ee676cc4e1..88dfd833800a 100644 --- a/packages/wrangler/src/dev/local.tsx +++ b/packages/wrangler/src/dev/local.tsx @@ -180,7 +180,7 @@ function useLocalWorker(props: { console.log(`⬣ Listening at http://localhost:${port}`); local.current.on("close", (code) => { - if (code !== null) { + if (code) { console.log(`miniflare process exited with code ${code}`); } }); @@ -201,7 +201,7 @@ function useLocalWorker(props: { }); local.current.on("exit", (code) => { - if (code !== 0) { + if (code) { console.error(`miniflare process exited with code ${code}`); } }); diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 39196e5e66d1..a614a2f6238b 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -84,7 +84,9 @@ export function useEsbuild({ // on build errors anyway so this is a no-op error handler }); - return stopWatching; + return () => { + stopWatching?.(); + }; }, [ entry, destination, diff --git a/packages/wrangler/src/module-collection.ts b/packages/wrangler/src/module-collection.ts index d6e8d9ea9b75..556ef03ce905 100644 --- a/packages/wrangler/src/module-collection.ts +++ b/packages/wrangler/src/module-collection.ts @@ -126,6 +126,12 @@ export default function createModuleCollector(props: { ), }, async (args: esbuild.OnResolveArgs) => { + if ( + args.kind !== "import-statement" && + args.kind !== "require-call" + ) { + return; + } // In the future, this will simply throw an error console.warn( `Deprecation warning: detected a legacy module import in "./${path.relative(