@canva/cli 0.0.1-beta.1 → 0.0.1-beta.10

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.
Files changed (79) hide show
  1. package/README.md +184 -108
  2. package/cli.js +446 -359
  3. package/lib/cjs/index.cjs +392 -0
  4. package/lib/esm/index.esm +392 -0
  5. package/package.json +11 -2
  6. package/templates/base/backend/routers/oauth.ts +393 -0
  7. package/templates/base/eslint.config.mjs +5 -277
  8. package/templates/base/package.json +26 -23
  9. package/templates/base/scripts/copy-env.ts +10 -0
  10. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
  11. package/templates/base/utils/backend/bearer_middleware/index.ts +1 -0
  12. package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
  13. package/templates/base/utils/use_add_element.ts +48 -0
  14. package/templates/base/utils/use_feature_support.ts +28 -0
  15. package/templates/base/webpack.config.cjs +3 -1
  16. package/templates/common/.gitignore.template +5 -6
  17. package/templates/common/.nvmrc +1 -0
  18. package/templates/common/.prettierrc +21 -0
  19. package/templates/common/.vscode/extensions.json +6 -0
  20. package/templates/common/README.md +4 -74
  21. package/templates/common/conf/eslint-general.mjs +303 -0
  22. package/templates/common/conf/eslint-i18n.mjs +41 -0
  23. package/templates/common/conf/eslint-local-i18n-rules/index.mjs +181 -0
  24. package/templates/common/jest.config.mjs +29 -2
  25. package/templates/common/jest.setup.ts +19 -0
  26. package/templates/common/utils/backend/base_backend/create.ts +104 -0
  27. package/templates/common/utils/table_wrapper.ts +477 -0
  28. package/templates/common/utils/tests/table_wrapper.tests.ts +252 -0
  29. package/templates/common/utils/use_add_element.ts +48 -0
  30. package/templates/common/utils/use_feature_support.ts +28 -0
  31. package/templates/common/utils/use_overlay_hook.ts +74 -0
  32. package/templates/common/utils/use_selection_hook.ts +37 -0
  33. package/templates/dam/backend/server.ts +0 -7
  34. package/templates/dam/eslint.config.mjs +6 -275
  35. package/templates/dam/package.json +43 -35
  36. package/templates/dam/scripts/copy-env.ts +10 -0
  37. package/templates/dam/src/app.tsx +2 -135
  38. package/templates/dam/webpack.config.cjs +3 -1
  39. package/templates/gen_ai/README.md +1 -1
  40. package/templates/gen_ai/backend/routers/image.ts +3 -3
  41. package/templates/gen_ai/backend/server.ts +0 -7
  42. package/templates/gen_ai/eslint.config.mjs +5 -277
  43. package/templates/gen_ai/package.json +46 -38
  44. package/templates/gen_ai/scripts/copy-env.ts +10 -0
  45. package/templates/gen_ai/src/api/api.ts +1 -39
  46. package/templates/gen_ai/src/app.tsx +16 -10
  47. package/templates/gen_ai/src/components/footer.messages.ts +0 -5
  48. package/templates/gen_ai/src/components/footer.tsx +2 -16
  49. package/templates/gen_ai/src/components/image_grid.tsx +9 -7
  50. package/templates/gen_ai/src/components/index.ts +0 -1
  51. package/templates/gen_ai/src/components/prompt_input.tsx +2 -0
  52. package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
  53. package/templates/gen_ai/src/context/app_context.tsx +4 -26
  54. package/templates/gen_ai/src/context/context.messages.ts +1 -12
  55. package/templates/gen_ai/src/home.tsx +13 -0
  56. package/templates/gen_ai/src/index.tsx +2 -18
  57. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  58. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
  59. package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +1 -0
  60. package/templates/gen_ai/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
  61. package/templates/gen_ai/webpack.config.cjs +3 -1
  62. package/templates/hello_world/eslint.config.mjs +5 -277
  63. package/templates/hello_world/package.json +46 -28
  64. package/templates/hello_world/scripts/copy-env.ts +10 -0
  65. package/templates/hello_world/src/app.tsx +25 -3
  66. package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
  67. package/templates/hello_world/src/tests/app.tests.tsx +86 -0
  68. package/templates/hello_world/utils/use_add_element.ts +48 -0
  69. package/templates/hello_world/utils/use_feature_support.ts +28 -0
  70. package/templates/hello_world/webpack.config.cjs +3 -1
  71. package/templates/dam/backend/database/database.ts +0 -42
  72. package/templates/dam/backend/routers/auth.ts +0 -285
  73. package/templates/gen_ai/backend/routers/auth.ts +0 -285
  74. package/templates/gen_ai/src/components/logged_in_status.tsx +0 -44
  75. package/templates/gen_ai/src/services/auth.tsx +0 -31
  76. package/templates/gen_ai/src/services/index.ts +0 -1
  77. /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/index.ts +0 -0
  78. /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
  79. /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +0 -0
@@ -2,7 +2,6 @@ export * from "./app_error";
2
2
  export * from "./footer";
3
3
  export * from "./image_grid";
4
4
  export * from "./loading_results";
5
- export * from "./logged_in_status";
6
5
  export * from "./prompt_input";
7
6
  export * from "./report_box";
8
7
  export * from "./remaining_credits";
@@ -19,6 +19,8 @@ const MIN_INPUT_ROWS = 3;
19
19
  /**
20
20
  * Array of example prompts that could be used to generate interesting pictures with an AI.
21
21
  * Consider fetching these prompts from a server or API call for dynamic and varied content.
22
+ * These would need to be localised, but that is left out here as the method would depend on
23
+ * the specific implementation or API used.
22
24
  */
23
25
  const examplePrompts: string[] = [
24
26
  "Cats ruling a parallel universe",
@@ -0,0 +1,43 @@
1
+ /* eslint-disable formatjs/no-literal-string-in-jsx */
2
+ import { TestAppUiProvider } from "@canva/app-ui-kit";
3
+ import { TestAppI18nProvider } from "@canva/app-i18n-kit";
4
+ import type { RenderResult } from "@testing-library/react";
5
+ import { fireEvent, render } from "@testing-library/react";
6
+ import React from "react";
7
+ import { RemainingCredits } from "../remaining_credits";
8
+ import { requestOpenExternalUrl } from "@canva/platform";
9
+
10
+ function renderInTestProvider(node: React.ReactNode): RenderResult {
11
+ return render(
12
+ // In a test environment, you should wrap your apps in `TestAppI18nProvider` and `TestAppUiProvider`, rather than `AppI18nProvider` and `AppUiProvider`
13
+ <TestAppI18nProvider>
14
+ <TestAppUiProvider>{node}</TestAppUiProvider>,
15
+ </TestAppI18nProvider>,
16
+ );
17
+ }
18
+
19
+ // This test demonstrates how to test code that uses functions from the Canva Apps SDK
20
+ // For more information on testing with the Canva Apps SDK, see https://www.canva.dev/docs/apps/testing/
21
+ describe("Remaining Credit Tests", () => {
22
+ const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
23
+
24
+ beforeEach(() => {
25
+ jest.resetAllMocks();
26
+ });
27
+
28
+ it("should call requestOpenExternalUrl when the link is clicked", () => {
29
+ // assert that the mock is in the expected clean state
30
+ expect(mockRequestOpenExternalUrl).not.toHaveBeenCalled();
31
+
32
+ const result = renderInTestProvider(<RemainingCredits />);
33
+
34
+ // get a reference to the link to purchase more credits
35
+ const purchaseMoreLink = result.getByRole("button");
36
+
37
+ // programmatically simulate clicking the button
38
+ fireEvent.click(purchaseMoreLink);
39
+
40
+ // we expect that requestOpenExternalUrl has been called
41
+ expect(mockRequestOpenExternalUrl).toHaveBeenCalled();
42
+ });
43
+ });
@@ -1,12 +1,10 @@
1
1
  import { createContext, useEffect, useState } from "react";
2
2
  import { useIntl } from "react-intl";
3
- import type { ImageType, LoggedInState } from "src/api";
4
- import { checkAuthenticationStatus, getRemainingCredits } from "src/api";
3
+ import type { ImageType } from "src/api";
4
+ import { getRemainingCredits } from "src/api";
5
5
  import { ContextMessages as Messages } from "./context.messages";
6
6
 
7
7
  export interface AppContextType {
8
- loggedInState: LoggedInState;
9
- setLoggedInState: (value: LoggedInState) => void;
10
8
  appError: string;
11
9
  setAppError: (value: string) => void;
12
10
  creditsError: string;
@@ -28,8 +26,6 @@ export interface AppContextType {
28
26
  }
29
27
 
30
28
  export const AppContext = createContext<AppContextType>({
31
- loggedInState: "not_authenticated",
32
- setLoggedInState: () => {},
33
29
  appError: "",
34
30
  setAppError: () => {},
35
31
  creditsError: "",
@@ -65,8 +61,6 @@ export const ContextProvider = ({
65
61
  }: {
66
62
  children: React.ReactNode;
67
63
  }): JSX.Element => {
68
- const [loggedInState, setLoggedInState] =
69
- useState<LoggedInState>("not_authenticated");
70
64
  const [appError, setAppError] = useState<string>("");
71
65
  const [loadingApp, setLoadingApp] = useState<boolean>(true); // set to true to prevent ui flash on load
72
66
  const [isLoadingImages, setIsLoadingImages] = useState<boolean>(false);
@@ -95,17 +89,6 @@ export const ContextProvider = ({
95
89
  // eslint-disable-next-line no-console
96
90
  console.error("Error fetching remaining credits:", error);
97
91
  }
98
-
99
- // Fetch login status
100
- try {
101
- checkAuthenticationStatus();
102
- } catch (error) {
103
- setAppError(
104
- intl.formatMessage(Messages.appErrorGetLoggedInStatusFailed),
105
- );
106
- // eslint-disable-next-line no-console
107
- console.error("Error fetching login status:", error);
108
- }
109
92
  } catch (error) {
110
93
  setAppError(intl.formatMessage(Messages.appErrorGeneral));
111
94
  // eslint-disable-next-line no-console
@@ -125,13 +108,10 @@ export const ContextProvider = ({
125
108
  return;
126
109
  }
127
110
 
128
- const errorMessage =
129
- loggedInState === "authenticated"
130
- ? intl.formatMessage(Messages.alertNotEnoughCreditsLoggedIn)
131
- : intl.formatMessage(Messages.alertNotEnoughCreditsLoggedOut);
111
+ const errorMessage = intl.formatMessage(Messages.alertNotEnoughCredits);
132
112
 
133
113
  setCreditsError(errorMessage);
134
- }, [loadingApp, remainingCredits, loggedInState]);
114
+ }, [loadingApp, remainingCredits]);
135
115
 
136
116
  const setPromptInputHandler = (value: string) => {
137
117
  if (
@@ -148,8 +128,6 @@ export const ContextProvider = ({
148
128
  };
149
129
 
150
130
  const value: AppContextType = {
151
- loggedInState,
152
- setLoggedInState,
153
131
  appError,
154
132
  setAppError,
155
133
  creditsError,
@@ -12,11 +12,6 @@ export const ContextMessages = defineMessages({
12
12
  description:
13
13
  "A message to indicate that there was a failure to get the number of credits the user has",
14
14
  },
15
- appErrorGetLoggedInStatusFailed: {
16
- defaultMessage: "Retrieving logged in status has failed.",
17
- description:
18
- "A message to indicate that due to an unexpected problem, the app was unable to determine if the user is logged in",
19
- },
20
15
 
21
16
  /** Messages related to prompts and user input validation. */
22
17
  promptMissingErrorMessage: {
@@ -26,16 +21,10 @@ export const ContextMessages = defineMessages({
26
21
  },
27
22
 
28
23
  /** Messages related to credits, including their availability and purchasing options. */
29
- alertNotEnoughCreditsLoggedIn: {
24
+ alertNotEnoughCredits: {
30
25
  defaultMessage:
31
26
  "You don’t have enough credits left to generate an image. Please purchase more.",
32
27
  description:
33
28
  "A message to indicate that the user doesn't have enough credits to generate an image, and will need to buy more to continue",
34
29
  },
35
- alertNotEnoughCreditsLoggedOut: {
36
- defaultMessage:
37
- "You don’t have enough credits left to generate an image. Please sign up or log in to purchase more.",
38
- description:
39
- "A message to indicate that the user doesn't have enough credits to generate an image, and will need to sign up or log in to buy more",
40
- },
41
30
  });
@@ -0,0 +1,13 @@
1
+ import { Outlet } from "react-router-dom";
2
+ import { Rows } from "@canva/app-ui-kit";
3
+ import { Footer } from "./components";
4
+ import * as styles from "styles/components.css";
5
+
6
+ export const Home = () => (
7
+ <div className={styles.scrollContainer}>
8
+ <Rows spacing="3u">
9
+ <Outlet />
10
+ <Footer />
11
+ </Rows>
12
+ </div>
13
+ );
@@ -1,27 +1,11 @@
1
1
  import { createRoot } from "react-dom/client";
2
- import { createHashRouter, RouterProvider } from "react-router-dom";
3
- import { ErrorBoundary } from "react-error-boundary";
4
- import { AppUiProvider } from "@canva/app-ui-kit";
5
- import { routes } from "./routes";
6
- import { ErrorPage } from "./pages";
7
- import { ContextProvider } from "./context";
8
2
  import "@canva/app-ui-kit/styles.css";
9
- import { AppI18nProvider } from "@canva/app-i18n-kit";
3
+ import { App } from "./app";
10
4
 
11
5
  const root = createRoot(document.getElementById("root") as Element);
12
6
 
13
7
  function render() {
14
- root.render(
15
- <AppI18nProvider>
16
- <AppUiProvider>
17
- <ErrorBoundary fallback={<ErrorPage />}>
18
- <ContextProvider>
19
- <RouterProvider router={createHashRouter(routes)} />
20
- </ContextProvider>
21
- </ErrorBoundary>
22
- </AppUiProvider>
23
- </AppI18nProvider>,
24
- );
8
+ root.render(<App />);
25
9
  }
26
10
 
27
11
  render();
@@ -1,4 +1,4 @@
1
- import { App } from "src/app";
1
+ import { Home } from "src/home";
2
2
  import { ErrorPage, GeneratePage, ResultsPage } from "src/pages";
3
3
 
4
4
  export enum Paths {
@@ -9,7 +9,7 @@ export enum Paths {
9
9
  export const routes = [
10
10
  {
11
11
  path: Paths.HOME,
12
- element: <App />,
12
+ element: <Home />,
13
13
  errorElement: <ErrorPage />,
14
14
  children: [
15
15
  {
@@ -0,0 +1,101 @@
1
+ /* eslint-disable no-console */
2
+ import * as debug from "debug";
3
+ import type { Request, Response, NextFunction } from "express";
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ import Express from "express-serve-static-core";
6
+
7
+ /**
8
+ * Prefix your start command with `DEBUG=express:middleware:bearer` to enable debug logging
9
+ * for this middleware
10
+ */
11
+ const debugLogger = debug("express:middleware:bearer");
12
+
13
+ /**
14
+ * Augment the Express request context to include the appId/userId/brandId fields decoded
15
+ * from the JWT.
16
+ */
17
+ declare module "express-serve-static-core" {
18
+ export interface Request {
19
+ user_id: string;
20
+ }
21
+ }
22
+
23
+ const sendUnauthorizedResponse = (res: Response, message?: string) =>
24
+ res.status(401).json({ error: "unauthorized", message });
25
+
26
+ /**
27
+ * An Express.js middleware verifying a Bearer token.
28
+ * This middleware extracts the token from the `Authorization` header.
29
+ *
30
+ * @param getTokenFromRequest - A function that extracts a token from the request. If a token isn't found, throw a `JWTAuthorizationError`.
31
+ * @returns An Express.js middleware for verifying and decoding JWTs.
32
+ */
33
+ export function createBearerMiddleware(
34
+ tokenToUser: (access_token: string) => Promise<string | undefined>,
35
+ getTokenFromRequest: GetTokenFromRequest = getTokenFromHttpHeader,
36
+ ): (req: Request, res: Response, next: NextFunction) => void {
37
+ return async (req, res, next) => {
38
+ try {
39
+ debugLogger(`processing token for '${req.url}'`);
40
+
41
+ const token = await getTokenFromRequest(req);
42
+ const user = await tokenToUser(token);
43
+
44
+ if (!user) {
45
+ throw new AuthorizationError("Token is invalid");
46
+ }
47
+
48
+ req.user_id = user;
49
+
50
+ next();
51
+ } catch (e) {
52
+ if (e instanceof AuthorizationError) {
53
+ return sendUnauthorizedResponse(res, e.message);
54
+ }
55
+
56
+ next(e);
57
+ }
58
+ };
59
+ }
60
+
61
+ export type GetTokenFromRequest = (req: Request) => Promise<string> | string;
62
+
63
+ export const getTokenFromHttpHeader: GetTokenFromRequest = (
64
+ req: Request,
65
+ ): string => {
66
+ // The names of a HTTP header bearing the JWT, and a scheme
67
+ const headerName = "Authorization";
68
+ const schemeName = "Bearer";
69
+
70
+ const header = req.header(headerName);
71
+ if (!header) {
72
+ throw new AuthorizationError(`Missing the "${headerName}" header`);
73
+ }
74
+
75
+ if (!header.match(new RegExp(`^${schemeName}\\s+[^\\s]+$`, "i"))) {
76
+ console.trace(
77
+ `jwtMiddleware: failed to match token in "${headerName}" header`,
78
+ );
79
+ throw new AuthorizationError(
80
+ `Missing a "${schemeName}" token in the "${headerName}" header`,
81
+ );
82
+ }
83
+
84
+ const token = header.replace(new RegExp(`^${schemeName}\\s+`, "i"), "");
85
+
86
+ return token;
87
+ };
88
+
89
+ /**
90
+ * A class representing JWT validation errors in the JWT middleware.
91
+ * The error message provided to the constructor will be forwarded to the
92
+ * API consumer trying to access a JWT-protected endpoint.
93
+ * @private
94
+ */
95
+ export class AuthorizationError extends Error {
96
+ constructor(message: string) {
97
+ super(message);
98
+
99
+ Object.setPrototypeOf(this, AuthorizationError.prototype);
100
+ }
101
+ }
@@ -0,0 +1 @@
1
+ export { createBearerMiddleware } from "./bearer_middleware";
@@ -0,0 +1,192 @@
1
+ /* eslint-disable @typescript-eslint/no-require-imports */
2
+ import type { NextFunction, Request, Response } from "express";
3
+ import type {
4
+ createBearerMiddleware,
5
+ GetTokenFromRequest,
6
+ } from "../bearer_middleware";
7
+
8
+ type Middleware = (req: Request, res: Response, next: NextFunction) => void;
9
+
10
+ describe("createBearerMiddleware", () => {
11
+ let fakeGetTokenFromRequest: jest.MockedFn<GetTokenFromRequest>;
12
+ let verify: jest.MockedFn<(token: string) => Promise<string | undefined>>;
13
+
14
+ let req: Request;
15
+ let res: Response;
16
+ let next: jest.MockedFn<() => void>;
17
+
18
+ let AuthorizationError: typeof Error;
19
+ let createBearerMiddlewareFn: typeof createBearerMiddleware;
20
+ let bearerMiddleware: Middleware;
21
+
22
+ beforeEach(() => {
23
+ jest.resetAllMocks();
24
+ jest.resetModules();
25
+
26
+ fakeGetTokenFromRequest = jest.fn();
27
+ verify = jest.fn();
28
+
29
+ const middlewareModule = require("../bearer_middleware");
30
+ createBearerMiddlewareFn = middlewareModule.createBearerMiddleware;
31
+ AuthorizationError = middlewareModule.AuthorizationError;
32
+ });
33
+
34
+ describe("When called", () => {
35
+ beforeEach(() => {
36
+ req = {
37
+ header: (_name: string) => undefined,
38
+ } as Request;
39
+
40
+ res = {
41
+ status: jest.fn().mockReturnThis(),
42
+ json: jest.fn().mockReturnThis(),
43
+ send: jest.fn().mockReturnThis(),
44
+ } as unknown as Response;
45
+
46
+ next = jest.fn();
47
+
48
+ bearerMiddleware = createBearerMiddlewareFn(
49
+ verify,
50
+ fakeGetTokenFromRequest,
51
+ );
52
+ });
53
+
54
+ describe("When `getTokenFromRequest` throws an exception ('Fake error')", () => {
55
+ beforeEach(() => {
56
+ fakeGetTokenFromRequest.mockRejectedValue(
57
+ new AuthorizationError("Fake error"),
58
+ );
59
+ });
60
+
61
+ it(`Does not call next() and returns HTTP 401 with error = "unauthorized" and message = "Fake error"`, async () => {
62
+ expect.assertions(8);
63
+
64
+ expect(fakeGetTokenFromRequest).not.toHaveBeenCalled();
65
+ await bearerMiddleware(req, res, next);
66
+
67
+ expect(fakeGetTokenFromRequest).toHaveBeenCalledTimes(1);
68
+ expect(fakeGetTokenFromRequest).toHaveBeenLastCalledWith(req);
69
+
70
+ expect(res.status).toHaveBeenCalledTimes(1);
71
+ expect(res.status).toHaveBeenLastCalledWith(401);
72
+
73
+ expect(res.json).toHaveBeenCalledTimes(1);
74
+ expect(res.json).toHaveBeenLastCalledWith({
75
+ error: "unauthorized",
76
+ message: "Fake error",
77
+ });
78
+
79
+ expect(next).not.toHaveBeenCalled();
80
+ });
81
+ });
82
+
83
+ describe("When the middleware cannot verify the token", () => {
84
+ beforeEach(() => {
85
+ fakeGetTokenFromRequest.mockReturnValue("TOKEN");
86
+
87
+ verify.mockImplementation(() => Promise.resolve(undefined));
88
+ });
89
+
90
+ it(`Does not call next() and returns HTTP 401 with error = "unauthorized" and message = "Token is invalid"`, async () => {
91
+ expect.assertions(5);
92
+
93
+ await bearerMiddleware(req, res, next);
94
+
95
+ expect(res.status).toHaveBeenCalledTimes(1);
96
+ expect(res.status).toHaveBeenLastCalledWith(401);
97
+
98
+ expect(res.json).toHaveBeenCalledTimes(1);
99
+ expect(res.json).toHaveBeenLastCalledWith({
100
+ error: "unauthorized",
101
+ message: "Token is invalid",
102
+ });
103
+
104
+ expect(next).not.toHaveBeenCalled();
105
+ });
106
+ });
107
+ });
108
+ });
109
+
110
+ describe("getTokenFromHttpHeader", () => {
111
+ let getHeader: jest.MockedFn<(name: string) => string | undefined>;
112
+ let req: Request;
113
+ let getTokenFromHttpHeader: (req: Request) => string;
114
+ let AuthorizationError: typeof Error;
115
+
116
+ beforeEach(() => {
117
+ getHeader = jest.fn();
118
+ req = {
119
+ header: (name: string) => getHeader(name),
120
+ } as Request;
121
+
122
+ const bearerMiddlewareModule = require("../bearer_middleware");
123
+ getTokenFromHttpHeader = bearerMiddlewareModule.getTokenFromHttpHeader;
124
+ AuthorizationError = bearerMiddlewareModule.AuthorizationError;
125
+ });
126
+
127
+ describe("When the 'Authorization' header is missing", () => {
128
+ beforeEach(() => {
129
+ getHeader.mockReturnValue(undefined);
130
+ });
131
+
132
+ it(`Throws a AuthorizationError with message = 'Missing the "Authorization" header'`, async () => {
133
+ expect.assertions(3);
134
+
135
+ expect(() => getTokenFromHttpHeader(req)).toThrow(
136
+ new AuthorizationError('Missing the "Authorization" header'),
137
+ );
138
+ expect(getHeader).toHaveBeenCalledTimes(1);
139
+ expect(getHeader).toHaveBeenLastCalledWith("Authorization");
140
+ });
141
+ });
142
+
143
+ describe("When the 'Authorization' header doesn't have a Bearer scheme", () => {
144
+ beforeEach(() => {
145
+ getHeader.mockReturnValue("Beerer FAKE_TOKEN");
146
+ });
147
+
148
+ it(`Throws a AuthorizationError with message = 'Missing a "Bearer" token in the "Authorization" header''`, async () => {
149
+ expect.assertions(3);
150
+
151
+ expect(() => getTokenFromHttpHeader(req)).toThrow(
152
+ new AuthorizationError(
153
+ 'Missing a "Bearer" token in the "Authorization" header',
154
+ ),
155
+ );
156
+ expect(getHeader).toHaveBeenCalledTimes(1);
157
+ expect(getHeader).toHaveBeenLastCalledWith("Authorization");
158
+ });
159
+ });
160
+
161
+ describe("When the 'Authorization' Bearer scheme header doesn't have a token", () => {
162
+ beforeEach(() => {
163
+ getHeader.mockReturnValue("Bearer ");
164
+ });
165
+
166
+ it(`Throws a AuthorizationError with message = 'Missing a "Bearer" token in the "Authorization" header'`, async () => {
167
+ expect.assertions(3);
168
+
169
+ expect(() => getTokenFromHttpHeader(req)).toThrow(
170
+ new AuthorizationError(
171
+ 'Missing a "Bearer" token in the "Authorization" header',
172
+ ),
173
+ );
174
+ expect(getHeader).toHaveBeenCalledTimes(1);
175
+ expect(getHeader).toHaveBeenLastCalledWith("Authorization");
176
+ });
177
+ });
178
+
179
+ describe("When the 'Authorization' Bearer scheme header has a token", () => {
180
+ beforeEach(() => {
181
+ getHeader.mockReturnValue("Bearer TOKEN");
182
+ });
183
+
184
+ it(`Returns the token`, async () => {
185
+ expect.assertions(3);
186
+
187
+ expect(getTokenFromHttpHeader(req)).toEqual("TOKEN");
188
+ expect(getHeader).toHaveBeenCalledTimes(1);
189
+ expect(getHeader).toHaveBeenLastCalledWith("Authorization");
190
+ });
191
+ });
192
+ });
@@ -4,6 +4,7 @@ const TerserPlugin = require("terser-webpack-plugin");
4
4
  const { DefinePlugin, optimize } = require("webpack");
5
5
  const chalk = require("chalk");
6
6
  const { transform } = require("@formatjs/ts-transformer");
7
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
7
8
 
8
9
  /**
9
10
  *
@@ -173,7 +174,8 @@ function buildConfig({
173
174
  }),
174
175
  // Apps can only submit a single JS file via the developer portal
175
176
  new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
176
- ],
177
+ mode === "development" && new ReactRefreshWebpackPlugin(),
178
+ ].filter(Boolean),
177
179
  ...buildDevConfig(devConfig),
178
180
  };
179
181
  }