wrangler 0.0.7 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wrangler",
3
- "version": "0.0.7",
3
+ "version": "0.0.12",
4
4
  "author": "wrangler@cloudflare.com",
5
5
  "description": "Command-line interface for all things Cloudflare Workers",
6
6
  "bin": {
@@ -37,9 +37,10 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "esbuild": "0.14.1",
40
- "miniflare": "2.0.0-rc.5",
40
+ "miniflare": "2.2.0",
41
41
  "path-to-regexp": "^6.2.0",
42
- "semiver": "^1.1.0"
42
+ "semiver": "^1.1.0",
43
+ "xxhash-addon": "^1.4.0"
43
44
  },
44
45
  "optionalDependencies": {
45
46
  "fsevents": "~2.3.2"
@@ -47,8 +48,8 @@
47
48
  "devDependencies": {
48
49
  "@babel/types": "^7.16.0",
49
50
  "@iarna/toml": "^2.2.5",
50
- "@types/mime": "^2.0.3",
51
51
  "@types/estree": "^0.0.50",
52
+ "@types/mime": "^2.0.3",
52
53
  "@types/react": "^17.0.37",
53
54
  "@types/serve-static": "^1.13.10",
54
55
  "@types/signal-exit": "^3.0.1",
@@ -59,17 +60,20 @@
59
60
  "chokidar": "^3.5.2",
60
61
  "clipboardy": "^3.0.0",
61
62
  "command-exists": "^1.2.9",
63
+ "devtools-protocol": "^0.0.955664",
62
64
  "execa": "^6.0.0",
63
65
  "faye-websocket": "^0.11.4",
64
66
  "finalhandler": "^1.1.2",
65
67
  "find-up": "^6.2.0",
66
68
  "formdata-node": "^4.3.1",
69
+ "ignore": "^5.2.0",
67
70
  "ink": "^3.2.0",
68
71
  "ink-select-input": "^4.2.1",
69
72
  "ink-table": "^3.0.0",
73
+ "ink-testing-library": "^2.1.0",
70
74
  "ink-text-input": "^4.0.2",
71
75
  "mime": "^3.0.0",
72
- "node-fetch": "^3.1.0",
76
+ "node-fetch": "3.1.1",
73
77
  "open": "^8.4.0",
74
78
  "react": "^17.0.2",
75
79
  "react-error-boundary": "^3.1.4",
@@ -95,7 +99,8 @@
95
99
  "bundle": "node -r esbuild-register scripts/bundle.ts",
96
100
  "build": "npm run clean && npm run bundle",
97
101
  "start": "npm run bundle && NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js",
98
- "test": "CF_API_TOKEN=some-api-token CF_ACCOUNT_ID=some-account-id jest --silent=false --verbose=true"
102
+ "test": "CF_API_TOKEN=some-api-token CF_ACCOUNT_ID=some-account-id jest --silent=false --verbose=true",
103
+ "test-watch": "npm run test -- --runInBand --testTimeout=50000 --watch"
99
104
  },
100
105
  "engines": {
101
106
  "node": ">=16.7.0"
@@ -6,6 +6,7 @@ type Options = {
6
6
  outfile: string;
7
7
  minify?: boolean;
8
8
  sourcemap?: boolean;
9
+ fallbackService?: string;
9
10
  watch?: boolean;
10
11
  onEnd?: () => void;
11
12
  };
@@ -15,6 +16,7 @@ export function buildWorker({
15
16
  outfile = "bundle.js",
16
17
  minify = false,
17
18
  sourcemap = false,
19
+ fallbackService = "ASSETS",
18
20
  watch = false,
19
21
  onEnd = () => {},
20
22
  }: Options) {
@@ -31,6 +33,9 @@ export function buildWorker({
31
33
  sourcemap,
32
34
  watch,
33
35
  allowOverwrite: true,
36
+ define: {
37
+ __FALLBACK_SERVICE__: JSON.stringify(fallbackService),
38
+ },
34
39
  plugins: [
35
40
  {
36
41
  name: "wrangler notifier and monitor",
@@ -1,8 +1,15 @@
1
1
  import { compareRoutes } from "./filepath-routing";
2
2
 
3
3
  describe("compareRoutes()", () => {
4
+ test("routes / last", () => {
5
+ expect(compareRoutes("/", "/foo")).toBeGreaterThanOrEqual(1);
6
+ expect(compareRoutes("/", "/:foo")).toBeGreaterThanOrEqual(1);
7
+ expect(compareRoutes("/", "/:foo*")).toBeGreaterThanOrEqual(1);
8
+ });
9
+
4
10
  test("routes with fewer segments come after those with more segments", () => {
5
- expect(compareRoutes("/foo", "/foo/bar")).toBe(1);
11
+ expect(compareRoutes("/foo", "/foo/bar")).toBeGreaterThanOrEqual(1);
12
+ expect(compareRoutes("/foo", "/foo/bar/cat")).toBeGreaterThanOrEqual(1);
6
13
  });
7
14
 
8
15
  test("routes with wildcard segments come after those without", () => {
@@ -3,9 +3,8 @@ import fs from "fs/promises";
3
3
  import { transform } from "esbuild";
4
4
  import * as acorn from "acorn";
5
5
  import * as acornWalk from "acorn-walk";
6
- import type { Config } from "./routes";
7
- import type { Identifier } from "estree";
8
- import type { ExportNamedDeclaration } from "@babel/types";
6
+ import type { Config, RouteConfig } from "./routes";
7
+ import type { ExportNamedDeclaration, Identifier } from "estree";
9
8
 
10
9
  type Arguments = {
11
10
  baseDir: string;
@@ -16,10 +15,7 @@ export async function generateConfigFromFileTree({
16
15
  baseDir,
17
16
  baseURL,
18
17
  }: Arguments) {
19
- let routeEntries: [
20
- string,
21
- { [key in "module" | "middleware"]?: string[] }
22
- ][] = [] as any;
18
+ let routeEntries: [string, RouteConfig][] = [];
23
19
 
24
20
  if (!baseURL.startsWith("/")) {
25
21
  baseURL = `/${baseURL}`;
@@ -31,7 +27,7 @@ export async function generateConfigFromFileTree({
31
27
 
32
28
  await forEachFile(baseDir, async (filepath) => {
33
29
  const ext = path.extname(filepath);
34
- if (/\.(mjs|js|ts)/.test(ext)) {
30
+ if (/^\.(mjs|js|ts|tsx|jsx)$/.test(ext)) {
35
31
  // transform the code to ensure we're working with vanilla JS + ESM
36
32
  const { code } = await transform(await fs.readFile(filepath, "utf-8"), {
37
33
  loader: ext === ".ts" ? "ts" : "js",
@@ -43,8 +39,10 @@ export async function generateConfigFromFileTree({
43
39
  sourceType: "module",
44
40
  });
45
41
  acornWalk.simple(ast, {
46
- ExportNamedDeclaration(_node) {
47
- const node: ExportNamedDeclaration = _node as any;
42
+ ExportNamedDeclaration(_node: unknown) {
43
+ // This dynamic cast assumes that the AST generated by acornWalk will generate nodes that
44
+ // are compatible with the eslint AST nodes.
45
+ const node = _node as ExportNamedDeclaration;
48
46
 
49
47
  // this is an array because multiple things can be exported from a single statement
50
48
  // i.e. `export {foo, bar}` or `export const foo = "f", bar = "b"`
@@ -54,7 +52,7 @@ export async function generateConfigFromFileTree({
54
52
  const declaration = node.declaration;
55
53
 
56
54
  // `export async function onRequest() {...}`
57
- if (declaration.type === "FunctionDeclaration") {
55
+ if (declaration.type === "FunctionDeclaration" && declaration.id) {
58
56
  exportNames.push(declaration.id.name);
59
57
  }
60
58
 
@@ -157,14 +155,14 @@ export async function generateConfigFromFileTree({
157
155
  // more specific routes aren't occluded from matching due to
158
156
  // less specific routes appearing first in the route list.
159
157
  export function compareRoutes(a: string, b: string) {
160
- function parseRoutePath(routePath: string) {
161
- let [method, segmentedPath] = routePath.split(" ");
162
- if (!segmentedPath) {
163
- segmentedPath = method;
164
- method = null;
165
- }
166
-
167
- const segments = segmentedPath.slice(1).split("/");
158
+ function parseRoutePath(routePath: string): [string | null, string[]] {
159
+ const parts = routePath.split(" ", 2);
160
+ // split() will guarantee at least one element.
161
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
162
+ const segmentedPath = parts.pop()!;
163
+ const method = parts.pop() ?? null;
164
+
165
+ const segments = segmentedPath.slice(1).split("/").filter(Boolean);
168
166
  return [method, segments];
169
167
  }
170
168
 
@@ -206,7 +204,7 @@ async function forEachFile<T>(
206
204
  const searchPaths = [baseDir];
207
205
  const returnValues: T[] = [];
208
206
 
209
- while (searchPaths.length) {
207
+ while (isNotEmpty(searchPaths)) {
210
208
  const cwd = searchPaths.shift();
211
209
  const dir = await fs.readdir(cwd, { withFileTypes: true });
212
210
  for (const entry of dir) {
@@ -221,3 +219,10 @@ async function forEachFile<T>(
221
219
 
222
220
  return returnValues;
223
221
  }
222
+
223
+ interface NonEmptyArray<T> extends Array<T> {
224
+ shift(): T;
225
+ }
226
+ function isNotEmpty<T>(array: T[]): array is NonEmptyArray<T> {
227
+ return array.length > 0;
228
+ }
@@ -68,7 +68,7 @@ export const validIdentifierRegex = new RegExp(
68
68
  "u"
69
69
  );
70
70
 
71
- export const isValidIdentifer = (identifier: string) =>
71
+ export const isValidIdentifier = (identifier: string) =>
72
72
  validIdentifierRegex.test(identifier);
73
73
 
74
74
  export const normalizeIdentifier = (identifier: string) =>
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import fs from "fs/promises";
3
- import { isValidIdentifer, normalizeIdentifier } from "./identifiers";
3
+ import { isValidIdentifier, normalizeIdentifier } from "./identifiers";
4
4
 
5
5
  export const HTTP_METHODS = [
6
6
  "HEAD",
@@ -15,7 +15,7 @@ export type HTTPMethod = typeof HTTP_METHODS[number];
15
15
  export function isHTTPMethod(
16
16
  maybeHTTPMethod: string
17
17
  ): maybeHTTPMethod is HTTPMethod {
18
- return HTTP_METHODS.includes(maybeHTTPMethod as any);
18
+ return (HTTP_METHODS as readonly string[]).includes(maybeHTTPMethod);
19
19
  }
20
20
 
21
21
  export type RoutesCollection = Array<{
@@ -27,14 +27,16 @@ export type RoutesCollection = Array<{
27
27
 
28
28
  export type Config = {
29
29
  routes?: RoutesConfig;
30
- schedules?: any;
30
+ schedules?: unknown;
31
31
  };
32
32
 
33
33
  export type RoutesConfig = {
34
- [route: string]: {
35
- middleware?: string | string[];
36
- module?: string | string[];
37
- };
34
+ [route: string]: RouteConfig;
35
+ };
36
+
37
+ export type RouteConfig = {
38
+ middleware?: string | string[];
39
+ module?: string | string[];
38
40
  };
39
41
 
40
42
  type ImportMap = Map<
@@ -91,7 +93,7 @@ export function parseConfig(config: Config, baseDir: string) {
91
93
  }
92
94
 
93
95
  // ensure the module name (if provided) is a valid identifier to guard against injection attacks
94
- if (name !== "default" && !isValidIdentifer(name)) {
96
+ if (name !== "default" && !isValidIdentifier(name)) {
95
97
  throw new Error(`Invalid module identifier "${name}"`);
96
98
  }
97
99
 
@@ -112,7 +114,7 @@ export function parseConfig(config: Config, baseDir: string) {
112
114
  });
113
115
  }
114
116
 
115
- for (const [route, props] of Object.entries(config.routes)) {
117
+ for (const [route, props] of Object.entries(config.routes ?? {})) {
116
118
  let [_methods, routePath] = route.split(" ");
117
119
  if (!routePath) {
118
120
  routePath = _methods;
@@ -2,11 +2,11 @@ import { match } from "path-to-regexp";
2
2
  import type { HTTPMethod } from "./routes";
3
3
 
4
4
  /* TODO: Grab these from @cloudflare/workers-types instead */
5
- type Params<P extends string = any> = Record<P, string | string[]>;
5
+ type Params<P extends string = string> = Record<P, string | string[]>;
6
6
 
7
7
  type EventContext<Env, P extends string, Data> = {
8
8
  request: Request;
9
- waitUntil: (promise: Promise<any>) => void;
9
+ waitUntil: (promise: Promise<unknown>) => void;
10
10
  next: (input?: Request | string, init?: RequestInit) => Promise<Response>;
11
11
  env: Env & { ASSETS: { fetch: typeof fetch } };
12
12
  params: Params<P>;
@@ -15,7 +15,7 @@ type EventContext<Env, P extends string, Data> = {
15
15
 
16
16
  declare type PagesFunction<
17
17
  Env = unknown,
18
- P extends string = any,
18
+ P extends string = string,
19
19
  Data extends Record<string, unknown> = Record<string, unknown>
20
20
  > = (context: EventContext<Env, P, Data>) => Response | Promise<Response>;
21
21
  /* end @cloudflare/workers-types */
@@ -29,22 +29,24 @@ type RouteHandler = {
29
29
 
30
30
  // inject `routes` via ESBuild
31
31
  declare const routes: RouteHandler[];
32
+ // define `__FALLBACK_SERVICE__` via ESBuild
33
+ declare const __FALLBACK_SERVICE__: string;
32
34
 
33
35
  // expect an ASSETS fetcher binding pointing to the asset-server stage
34
- type Env = {
35
- [name: string]: any;
36
- ASSETS: { fetch(url: string, init: RequestInit): Promise<Response> };
36
+ type FetchEnv = {
37
+ [name: string]: { fetch: typeof fetch };
38
+ ASSETS: { fetch: typeof fetch };
37
39
  };
38
40
 
39
41
  type WorkerContext = {
40
- waitUntil: (promise: Promise<any>) => void;
42
+ waitUntil: (promise: Promise<unknown>) => void;
41
43
  };
42
44
 
43
- function* executeRequest(request: Request, env: Env) {
45
+ function* executeRequest(request: Request, _env: FetchEnv) {
44
46
  const requestPath = new URL(request.url).pathname;
45
47
 
46
- // First, iterate through the routes and execute "middlewares" on partial route matches
47
- for (const route of routes) {
48
+ // First, iterate through the routes (backwards) and execute "middlewares" on partial route matches
49
+ for (const route of [...routes].reverse()) {
48
50
  if (
49
51
  route.methods.length &&
50
52
  !route.methods.includes(request.method as HTTPMethod)
@@ -85,16 +87,10 @@ function* executeRequest(request: Request, env: Env) {
85
87
  break;
86
88
  }
87
89
  }
88
-
89
- // Finally, yield to the asset-server
90
- return {
91
- handler: () => env.ASSETS.fetch(request.url, request),
92
- params: {} as Params,
93
- };
94
90
  }
95
91
 
96
92
  export default {
97
- async fetch(request: Request, env: Env, workerContext: WorkerContext) {
93
+ async fetch(request: Request, env: FetchEnv, workerContext: WorkerContext) {
98
94
  const handlerIterator = executeRequest(request, env);
99
95
  const data = {}; // arbitrary data the user can set between functions
100
96
  const next = async (input?: RequestInfo, init?: RequestInit) => {
@@ -102,10 +98,11 @@ export default {
102
98
  request = new Request(input, init);
103
99
  }
104
100
 
105
- const { value } = handlerIterator.next();
106
- if (value) {
107
- const { handler, params } = value;
108
- const context: EventContext<unknown, any, any> = {
101
+ const result = handlerIterator.next();
102
+ // Note we can't use `!result.done` because this doesn't narrow to the correct type
103
+ if (result.done == false) {
104
+ const { handler, params } = result.value;
105
+ const context = {
109
106
  request: new Request(request.clone()),
110
107
  next,
111
108
  params,
@@ -121,6 +118,12 @@ export default {
121
118
  [101, 204, 205, 304].includes(response.status) ? null : response.body,
122
119
  response
123
120
  );
121
+ } else if (__FALLBACK_SERVICE__) {
122
+ // There are no more handlers so finish with the fallback service (`env.ASSETS.fetch` in Pages' case)
123
+ return env[__FALLBACK_SERVICE__].fetch(request);
124
+ } else {
125
+ // There was not fallback service so actually make the request to the origin.
126
+ return fetch(request);
124
127
  }
125
128
  };
126
129
 
@@ -0,0 +1,67 @@
1
+ import { render } from "ink-testing-library";
2
+ import patchConsole from "patch-console";
3
+ import React from "react";
4
+ import Dev from "../dev";
5
+ import type { DevProps } from "../dev";
6
+
7
+ describe("Dev component", () => {
8
+ let restoreConsole;
9
+ beforeEach(() => (restoreConsole = patchConsole(() => {})));
10
+ afterEach(() => restoreConsole());
11
+
12
+ it("should throw if format is service-worker and there is a public directory", () => {
13
+ const { lastFrame } = renderDev({
14
+ format: "service-worker",
15
+ accountId: "some-account-id",
16
+ public: "some/public/path",
17
+ });
18
+ expect(lastFrame()?.split("\n").slice(0, 2).join("\n"))
19
+ .toMatchInlineSnapshot(`
20
+ "Something went wrong:
21
+ Error: You cannot use the service worker format with a \`public\` directory."
22
+ `);
23
+ });
24
+ });
25
+
26
+ /**
27
+ * Helper function to make it easier to setup and render the `Dev` component.
28
+ *
29
+ * All the `Dev` props are optional here, with sensible defaults for testing.
30
+ */
31
+ function renderDev({
32
+ name,
33
+ entry = "some/entry.ts",
34
+ port,
35
+ format,
36
+ accountId,
37
+ initialMode = "remote",
38
+ jsxFactory,
39
+ jsxFragment,
40
+ bindings = {},
41
+ public: publicDir,
42
+ assetPaths,
43
+ compatibilityDate,
44
+ compatibilityFlags,
45
+ usageModel,
46
+ buildCommand = {},
47
+ }: Partial<DevProps>) {
48
+ return render(
49
+ <Dev
50
+ name={name}
51
+ entry={entry}
52
+ port={port}
53
+ buildCommand={buildCommand}
54
+ format={format}
55
+ initialMode={initialMode}
56
+ jsxFactory={jsxFactory}
57
+ jsxFragment={jsxFragment}
58
+ accountId={accountId}
59
+ assetPaths={assetPaths}
60
+ public={publicDir}
61
+ compatibilityDate={compatibilityDate}
62
+ compatibilityFlags={compatibilityFlags}
63
+ usageModel={usageModel}
64
+ bindings={bindings}
65
+ />
66
+ );
67
+ }
@@ -17,6 +17,7 @@ describe("wrangler", () => {
17
17
 
18
18
  Commands:
19
19
  wrangler init [name] 📥 Create a wrangler.toml configuration file
20
+ wrangler whoami 🕵️ Retrieve your user info and test your auth config
20
21
  wrangler dev <filename> 👂 Start a local server for developing your worker
21
22
  wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
22
23
  wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
@@ -49,6 +50,7 @@ describe("wrangler", () => {
49
50
 
50
51
  Commands:
51
52
  wrangler init [name] 📥 Create a wrangler.toml configuration file
53
+ wrangler whoami 🕵️ Retrieve your user info and test your auth config
52
54
  wrangler dev <filename> 👂 Start a local server for developing your worker
53
55
  wrangler publish [script] 🆙 Publish your Worker to Cloudflare.
54
56
  wrangler tail [name] 🦚 Starts a log tailing session for a deployed Worker.
@@ -1,6 +1,10 @@
1
+ import { mockFetchInternal } from "./mock-cfetch";
1
2
  import { confirm, prompt } from "../dialogs";
3
+ import { fetchInternal } from "../cfetch/internal";
4
+
5
+ jest.mock("../cfetch/internal");
6
+ (fetchInternal as jest.Mock).mockImplementation(mockFetchInternal);
2
7
 
3
- jest.mock("../cfetch", () => jest.requireActual("./mock-cfetch"));
4
8
  jest.mock("../dialogs");
5
9
 
6
10
  // By default (if not configured by mockConfirm()) calls to `confirm()` should throw.