diff --git a/.changeset/shy-dryers-march.md b/.changeset/shy-dryers-march.md new file mode 100644 index 000000000000..d274c18334e6 --- /dev/null +++ b/.changeset/shy-dryers-march.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +feat: Non-interactive mode + +Continuing the work from https://github.com/cloudflare/wrangler2/pull/325, this detects when wrangler is running inside an environment where "raw" mode is not available on stdin, and disables the features for hot keys and the shortcut bar. This also adds stubs for testing local mode functionality in `local-mode-tests`, and deletes the previous hacky `dev2.test.tsx`. + +Fixes https://github.com/cloudflare/wrangler2/issues/322 diff --git a/package-lock.json b/package-lock.json index faf76506d6b7..3bac361f882a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3200,15 +3200,6 @@ "@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, @@ -11990,6 +11981,10 @@ "node": ">=8.9.0" } }, + "node_modules/local-mode-tests": { + "resolved": "packages/local-mode-tests", + "link": true + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -14358,34 +14353,6 @@ "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, @@ -17351,6 +17318,10 @@ "version": "1.0.0", "license": "ISC" }, + "packages/local-mode-tests": { + "version": "1.0.0", + "license": "ISC" + }, "packages/prerelease-registry": { "version": "0.0.1", "dependencies": { @@ -17389,7 +17360,6 @@ "@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/supports-color": "^8.1.1", @@ -17419,7 +17389,6 @@ "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", "supports-color": "^9.2.1", @@ -19884,15 +19853,6 @@ "@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, @@ -25779,6 +25739,9 @@ "json5": "^2.1.2" } }, + "local-mode-tests": { + "version": "file:packages/local-mode-tests" + }, "localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -27177,28 +27140,6 @@ "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, @@ -28958,7 +28899,6 @@ "@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/supports-color": "^8.1.1", @@ -28992,7 +28932,6 @@ "prompts": "^2.4.2", "react": "^17.0.2", "react-error-boundary": "^3.1.4", - "react-test-renderer": "^17.0.2", "selfsigned": "^2.0.0", "semiver": "^1.1.0", "serve-static": "^1.14.1", diff --git a/packages/local-mode-tests/package.json b/packages/local-mode-tests/package.json new file mode 100644 index 000000000000..ee0116fa10a6 --- /dev/null +++ b/packages/local-mode-tests/package.json @@ -0,0 +1,26 @@ +{ + "name": "local-mode-tests", + "version": "1.0.0", + "private": true, + "description": "", + "main": "index.js", + "scripts": { + "test": "npx jest --forceExit" + }, + "keywords": [], + "author": "", + "license": "ISC", + "jest": { + "restoreMocks": true, + "testTimeout": 30000, + "testRegex": ".*.(test|spec)\\.[jt]sx?$", + "transform": { + "^.+\\.c?(t|j)sx?$": [ + "esbuild-jest", + { + "sourcemap": true + } + ] + } + } +} diff --git a/packages/local-mode-tests/src/index.ts b/packages/local-mode-tests/src/index.ts new file mode 100644 index 000000000000..1f6e34ed9cc6 --- /dev/null +++ b/packages/local-mode-tests/src/index.ts @@ -0,0 +1,5 @@ +export default { + async fetch(_request: Request): Promise { + return new Response("Hello World!"); + }, +}; diff --git a/packages/local-mode-tests/tests/index.test.ts b/packages/local-mode-tests/tests/index.test.ts new file mode 100644 index 000000000000..178aa5a97a75 --- /dev/null +++ b/packages/local-mode-tests/tests/index.test.ts @@ -0,0 +1,46 @@ +import { spawn } from "child_process"; +import { fetch } from "undici"; +import type { ChildProcess } from "child_process"; +import type { Response } from "undici"; + +const waitUntilReady = async (url: string): Promise => { + let response: Response | undefined = undefined; + + while (response === undefined) { + await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); + + try { + response = await fetch(url); + } catch {} + } + + return response as Response; +}; +const isWindows = process.platform === "win32"; + +let wranglerProcess: ChildProcess; + +beforeAll(async () => { + wranglerProcess = spawn("npx", ["wrangler", "dev", "--local"], { + shell: isWindows, + }); +}); + +afterAll(async () => { + await new Promise((resolve, reject) => { + wranglerProcess.once("exit", (code) => { + if (!code) { + resolve(code); + } else { + reject(code); + } + }); + wranglerProcess.kill(); + }); +}); + +it("renders", async () => { + const response = await waitUntilReady("http://localhost:8787/"); + const text = await response.text(); + expect(text).toContain("Hello World!"); +}); diff --git a/packages/local-mode-tests/tsconfig.json b/packages/local-mode-tests/tsconfig.json new file mode 100644 index 000000000000..af50e9574fc1 --- /dev/null +++ b/packages/local-mode-tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "allowJs": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "esnext", + "strict": true, + "noEmit": true, + "skipLibCheck": true + } +} diff --git a/packages/local-mode-tests/wrangler.toml b/packages/local-mode-tests/wrangler.toml new file mode 100644 index 000000000000..292af6fb61cd --- /dev/null +++ b/packages/local-mode-tests/wrangler.toml @@ -0,0 +1,3 @@ +name = "local-mode-tests" +main = "src/index.ts" +compatibility_date = "2022-03-27" diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 1df1d55549c7..3906a0884330 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -56,7 +56,6 @@ "@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/supports-color": "^8.1.1", @@ -86,7 +85,6 @@ "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", "supports-color": "^9.2.1", diff --git a/packages/wrangler/src/__tests__/dev.test.tsx b/packages/wrangler/src/__tests__/dev.test.tsx index 6ffec9cfece5..d4290eb535fb 100644 --- a/packages/wrangler/src/__tests__/dev.test.tsx +++ b/packages/wrangler/src/__tests__/dev.test.tsx @@ -294,7 +294,7 @@ describe("wrangler dev", () => { ); // and the command would pass through - expect((Dev as jest.Mock).mock.calls[0][0].buildCommand).toEqual({ + expect((Dev as jest.Mock).mock.calls[0][0].build).toEqual({ command: "node -e \"console.log('custom build'); require('fs').writeFileSync('index.js', 'export default { fetch(){ return new Response(123) } }')\"", cwd: undefined, diff --git a/packages/wrangler/src/__tests__/dev2.test.tsx b/packages/wrangler/src/__tests__/dev2.test.tsx deleted file mode 100644 index 4d3c6fe61d10..000000000000 --- a/packages/wrangler/src/__tests__/dev2.test.tsx +++ /dev/null @@ -1,154 +0,0 @@ -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, - ip = "localhost", - 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 1460e7656936..8c819387da0b 100644 --- a/packages/wrangler/src/dev/dev.tsx +++ b/packages/wrangler/src/dev/dev.tsx @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import { watch } from "chokidar"; import clipboardy from "clipboardy"; import commandExists from "command-exists"; -import { Box, Text, useApp, useInput } from "ink"; +import { Box, Text, useApp, useInput, useStdin } from "ink"; import React, { useState, useEffect, useRef } from "react"; import { withErrorBoundary, useErrorHandler } from "react-error-boundary"; import onExit from "signal-exit"; @@ -19,11 +19,12 @@ import type { Config } from "../config"; import type { Entry } from "../entry"; import type { AssetPaths } from "../sites"; import type { CfWorkerInit } from "../worker"; +import type { EsbuildBundle } from "./use-esbuild"; export type DevProps = { name?: string; entry: Entry; - port?: number; + port: number; ip: string; inspectorPort: number; rules: Config["rules"]; @@ -40,7 +41,7 @@ export type DevProps = { compatibilityDate: undefined | string; compatibilityFlags: undefined | string[]; usageModel: undefined | "bundled" | "unbound"; - buildCommand: { + build: { command?: undefined | string; cwd?: undefined | string; watch_dir?: undefined | string; @@ -56,11 +57,10 @@ export type DevProps = { }; export function DevImplementation(props: DevProps): JSX.Element { - const port = props.port ?? 8787; const apiToken = props.initialMode === "remote" ? getAPIToken() : undefined; const directory = useTmpDir(); - useCustomBuild(props.entry, props.buildCommand); + useCustomBuild(props.entry, props.build); if (props.public && props.entry.format === "service-worker") { throw new Error( @@ -90,12 +90,32 @@ export function DevImplementation(props: DevProps): JSX.Element { serveAssetsFromWorker: !!props.public, }); + // only load the UI if we're running in a supported environment + const { isRawModeSupported } = useStdin(); + return isRawModeSupported ? ( + + ) : ( + + ); +} + +type InteractiveDevSessionProps = DevProps & { + apiToken: string | undefined; + bundle: EsbuildBundle | undefined; +}; + +function InteractiveDevSession(props: InteractiveDevSessionProps) { const toggles = useHotkeys( { local: props.initialMode === "local", tunnel: false, }, - port, + props.port, props.ip, props.inspectorPort, props.localProtocol @@ -105,44 +125,7 @@ export function DevImplementation(props: DevProps): JSX.Element { return ( <> - {toggles.local ? ( - - ) : ( - - )} + {`B to open a browser, D to open Devtools, L to ${ @@ -154,6 +137,49 @@ export function DevImplementation(props: DevProps): JSX.Element { ); } +type DevSessionProps = InteractiveDevSessionProps & { local: boolean }; + +function DevSession(props: DevSessionProps) { + return props.local ? ( + + ) : ( + + ); +} + export interface DirectorySyncResult { name: string; removeCallback: () => void; diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index 9abc6f22c7af..1f559a0b8864 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -924,7 +924,7 @@ export async function main(argv: string[]): Promise { zone={zone} rules={getRules(config)} legacyEnv={isLegacyEnv(args, config)} - buildCommand={config.build || {}} + build={config.build || {}} initialMode={args.local ? "local" : "remote"} jsxFactory={args["jsx-factory"] || config.jsx_factory} jsxFragment={args["jsx-fragment"] || config.jsx_fragment} @@ -1318,7 +1318,7 @@ export async function main(argv: string[]): Promise { env={args.env} zone={undefined} legacyEnv={isLegacyEnv(args, config)} - buildCommand={config.build || {}} + build={config.build || {}} initialMode={args.local ? "local" : "remote"} jsxFactory={config.jsx_factory} jsxFragment={config.jsx_fragment}