@canva/cli 0.0.1-beta.7 → 0.0.1-beta.9

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 (32) hide show
  1. package/cli.js +322 -301
  2. package/package.json +1 -1
  3. package/templates/base/package.json +12 -12
  4. package/templates/base/webpack.config.cjs +3 -1
  5. package/templates/common/conf/eslint-i18n.mjs +3 -0
  6. package/templates/common/conf/eslint-local-i18n-rules/index.mjs +181 -0
  7. package/templates/common/jest.config.mjs +29 -2
  8. package/templates/common/jest.setup.ts +19 -0
  9. package/templates/common/utils/backend/base_backend/create.ts +104 -0
  10. package/templates/common/utils/backend/jwt_middleware/index.ts +1 -0
  11. package/templates/common/utils/backend/jwt_middleware/jwt_middleware.ts +229 -0
  12. package/templates/common/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +630 -0
  13. package/templates/common/utils/table_wrapper.ts +477 -0
  14. package/templates/common/utils/tests/table_wrapper.tests.ts +252 -0
  15. package/templates/common/utils/use_add_element.ts +48 -0
  16. package/templates/common/utils/use_feature_support.ts +28 -0
  17. package/templates/common/utils/use_overlay_hook.ts +74 -0
  18. package/templates/common/utils/use_selection_hook.ts +37 -0
  19. package/templates/dam/package.json +26 -20
  20. package/templates/dam/webpack.config.cjs +3 -1
  21. package/templates/gen_ai/package.json +28 -22
  22. package/templates/gen_ai/src/app.tsx +16 -10
  23. package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
  24. package/templates/gen_ai/src/home.tsx +13 -0
  25. package/templates/gen_ai/src/index.tsx +2 -18
  26. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  27. package/templates/gen_ai/webpack.config.cjs +3 -1
  28. package/templates/hello_world/package.json +34 -19
  29. package/templates/hello_world/src/app.tsx +20 -0
  30. package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
  31. package/templates/hello_world/src/tests/app.tests.tsx +86 -0
  32. package/templates/hello_world/webpack.config.cjs +3 -1
@@ -1,13 +1,19 @@
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";
1
+ import { createHashRouter, RouterProvider } from "react-router-dom";
2
+ import { ContextProvider } from "./context";
3
+ import { routes } from "./routes";
4
+ import { AppI18nProvider } from "@canva/app-i18n-kit";
5
+ import { AppUiProvider } from "@canva/app-ui-kit";
6
+ import { ErrorBoundary } from "react-error-boundary";
7
+ import { ErrorPage } from "./pages";
5
8
 
6
9
  export const App = () => (
7
- <div className={styles.scrollContainer}>
8
- <Rows spacing="3u">
9
- <Outlet />
10
- <Footer />
11
- </Rows>
12
- </div>
10
+ <AppI18nProvider>
11
+ <AppUiProvider>
12
+ <ErrorBoundary fallback={<ErrorPage />}>
13
+ <ContextProvider>
14
+ <RouterProvider router={createHashRouter(routes)} />
15
+ </ContextProvider>
16
+ </ErrorBoundary>
17
+ </AppUiProvider>
18
+ </AppI18nProvider>
13
19
  );
@@ -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
+ });
@@ -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
  {
@@ -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
  }
@@ -12,55 +12,70 @@
12
12
  "lint:fix": "eslint . --fix",
13
13
  "lint:types": "tsc",
14
14
  "start": "ts-node ./scripts/start/start.ts",
15
- "test": "jest --no-cache --passWithNoTests",
15
+ "test": "jest --no-cache",
16
16
  "test:watch": "jest --watchAll",
17
+ "test:update": "npm run test -- -u",
17
18
  "postinstall": "ts-node ./scripts/copy-env.ts"
18
19
  },
19
20
  "dependencies": {
20
- "@canva/app-i18n-kit": "^1.0.1",
21
- "@canva/app-ui-kit": "^4.3.0",
22
- "@canva/design": "^2.2.1",
23
- "@canva/error": "^2.0.0",
24
- "@canva/platform": "^2.0.0",
21
+ "@canva/app-i18n-kit": "^1.0.2",
22
+ "@canva/app-ui-kit": "^4.4.0",
23
+ "@canva/asset": "^2.1.0",
24
+ "@canva/design": "^2.3.0",
25
+ "@canva/error": "^2.1.0",
26
+ "@canva/platform": "^2.1.0",
27
+ "@canva/user": "^2.1.0",
25
28
  "react": "18.3.1",
26
29
  "react-dom": "18.3.1",
27
30
  "react-intl": "6.8.7"
28
31
  },
29
32
  "devDependencies": {
30
- "@eslint/eslintrc": "3.1.0",
31
- "@eslint/js": "9.14.0",
32
- "@formatjs/cli": "6.3.8",
33
- "@formatjs/ts-transformer": "3.13.22",
33
+ "@eslint/eslintrc": "3.2.0",
34
+ "@eslint/js": "9.16.0",
35
+ "@formatjs/cli": "6.3.14",
36
+ "@formatjs/ts-transformer": "3.13.26",
34
37
  "@ngrok/ngrok": "1.4.1",
38
+ "@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
35
39
  "@svgr/webpack": "8.1.0",
40
+ "@testing-library/react": "16.1.0",
41
+ "@types/express": "4.17.21",
42
+ "@types/express-serve-static-core": "4.19.6",
36
43
  "@types/jest": "29.5.14",
44
+ "@types/jsonwebtoken": "9.0.7",
37
45
  "@types/node": "20.10.0",
38
- "@types/node-fetch": "2.6.11",
46
+ "@types/node-fetch": "2.6.12",
39
47
  "@types/node-forge": "1.3.11",
40
48
  "@types/nodemon": "1.19.6",
41
49
  "@types/react": "18.3.12",
42
50
  "@types/react-dom": "18.3.1",
43
51
  "@types/webpack-env": "1.18.5",
44
- "@typescript-eslint/eslint-plugin": "8.13.0",
45
- "@typescript-eslint/parser": "8.13.0",
52
+ "@typescript-eslint/eslint-plugin": "8.18.0",
53
+ "@typescript-eslint/parser": "8.18.0",
46
54
  "chalk": "4.1.2",
47
55
  "cli-table3": "0.6.5",
48
56
  "css-loader": "7.1.2",
49
57
  "css-modules-typescript-loader": "4.0.1",
50
58
  "cssnano": "7.0.6",
51
- "debug": "4.3.7",
52
- "dotenv": "16.4.5",
53
- "eslint": "9.14.0",
54
- "eslint-plugin-formatjs": "5.2.2",
59
+ "debug": "4.4.0",
60
+ "dotenv": "16.4.7",
61
+ "eslint": "9.16.0",
62
+ "eslint-plugin-formatjs": "5.2.8",
55
63
  "eslint-plugin-jest": "28.9.0",
56
64
  "eslint-plugin-react": "7.37.2",
65
+ "express": "4.21.2",
66
+ "express-basic-auth": "1.2.1",
57
67
  "jest": "29.7.0",
68
+ "jest-css-modules-transform": "4.4.2",
69
+ "jest-environment-jsdom": "29.7.0",
70
+ "jsonwebtoken": "9.0.2",
71
+ "jwks-rsa": "3.1.0",
58
72
  "mini-css-extract-plugin": "2.9.2",
59
73
  "node-fetch": "3.3.2",
60
74
  "node-forge": "1.3.1",
61
75
  "nodemon": "3.0.1",
62
76
  "postcss-loader": "8.1.1",
63
- "prettier": "3.3.3",
77
+ "prettier": "3.4.2",
78
+ "react-refresh": "0.16.0",
64
79
  "style-loader": "4.0.0",
65
80
  "terser-webpack-plugin": "5.3.10",
66
81
  "ts-jest": "29.2.5",
@@ -68,7 +83,7 @@
68
83
  "ts-node": "10.9.2",
69
84
  "typescript": "5.5.4",
70
85
  "url-loader": "4.1.1",
71
- "webpack": "5.96.1",
86
+ "webpack": "5.97.1",
72
87
  "webpack-cli": "5.1.4",
73
88
  "webpack-dev-server": "5.1.0",
74
89
  "yargs": "17.7.2"
@@ -1,8 +1,11 @@
1
1
  import { Button, Rows, Text } from "@canva/app-ui-kit";
2
+ import { requestOpenExternalUrl } from "@canva/platform";
2
3
  import { FormattedMessage, useIntl } from "react-intl";
3
4
  import * as styles from "styles/components.css";
4
5
  import { useAddElement } from "utils/use_add_element";
5
6
 
7
+ export const DOCS_URL = "https://www.canva.dev/docs/apps/";
8
+
6
9
  export const App = () => {
7
10
  const addElement = useAddElement();
8
11
 
@@ -13,6 +16,16 @@ export const App = () => {
13
16
  });
14
17
  };
15
18
 
19
+ const openExternalUrl = async (url: string) => {
20
+ const response = await requestOpenExternalUrl({
21
+ url,
22
+ });
23
+
24
+ if (response.status === "aborted") {
25
+ // user decided not to navigate to the link
26
+ }
27
+ };
28
+
16
29
  const intl = useIntl();
17
30
 
18
31
  return (
@@ -37,6 +50,13 @@ export const App = () => {
37
50
  "Button text to do something cool. Creates a new text element when pressed.",
38
51
  })}
39
52
  </Button>
53
+ <Button variant="secondary" onClick={() => openExternalUrl(DOCS_URL)}>
54
+ {intl.formatMessage({
55
+ defaultMessage: "Open Canva Apps SDK docs",
56
+ description:
57
+ "Button text to open Canva Apps SDK docs. Opens an external URL when pressed.",
58
+ })}
59
+ </Button>
40
60
  </Rows>
41
61
  </div>
42
62
  );
@@ -0,0 +1,45 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`Hello World Tests should have a consistent snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="scrollContainer"
7
+ >
8
+ <div
9
+ class="jv_R6g"
10
+ style="--NZu1Zw: 16px;"
11
+ >
12
+ <p
13
+ class="fFOiLQ _5Ob_nQ fM_HdA"
14
+ >
15
+ To make changes to this app, edit the
16
+ <code>
17
+ src/app.tsx
18
+ </code>
19
+ file, then close and reopen the app in the editor to preview the changes.
20
+ </p>
21
+ <button
22
+ class="_1QoxDw Qkd66A tYI0Vw o4TrkA zKTE_w Qkd66A tYI0Vw lsXp_w ubW6qw cwOZMg zQlusQ uRvRjQ"
23
+ type="button"
24
+ >
25
+ <span
26
+ class="_38oWvQ"
27
+ >
28
+ Do something cool
29
+ </span>
30
+ </button>
31
+ <button
32
+ class="_1QoxDw Qkd66A tYI0Vw o4TrkA NT2yCg Qkd66A tYI0Vw lsXp_w cwOZMg zQlusQ uRvRjQ"
33
+ type="button"
34
+ >
35
+ <span
36
+ class="_38oWvQ"
37
+ >
38
+ Open Canva Apps SDK docs
39
+ </span>
40
+ </button>
41
+ </div>
42
+ </div>
43
+ ,
44
+ </div>
45
+ `;
@@ -0,0 +1,86 @@
1
+ /* eslint-disable formatjs/no-literal-string-in-jsx */
2
+ import React from "react";
3
+ import type { RenderResult } from "@testing-library/react";
4
+ import { fireEvent, render } from "@testing-library/react";
5
+ import { TestAppI18nProvider } from "@canva/app-i18n-kit";
6
+ import { TestAppUiProvider } from "@canva/app-ui-kit";
7
+ import { requestOpenExternalUrl } from "@canva/platform";
8
+ import { useAddElement } from "utils/use_add_element";
9
+ import { App, DOCS_URL } from "../app";
10
+
11
+ function renderInTestProvider(node: React.ReactNode): RenderResult {
12
+ return render(
13
+ // In a test environment, you should wrap your apps in `TestAppI18nProvider` and `TestAppUiProvider`, rather than `AppI18nProvider` and `AppUiProvider`
14
+ <TestAppI18nProvider>
15
+ <TestAppUiProvider>{node}</TestAppUiProvider>,
16
+ </TestAppI18nProvider>,
17
+ );
18
+ }
19
+
20
+ jest.mock("utils/use_add_element");
21
+
22
+ // This test demonstrates how to test code that uses functions from the Canva Apps SDK
23
+ // For more information on testing with the Canva Apps SDK, see https://www.canva.dev/docs/apps/testing/
24
+ describe("Hello World Tests", () => {
25
+ // Mocking the useAddElement hook
26
+ const mockAddElement = jest.fn();
27
+ const mockAddUseElement = jest.mocked(useAddElement);
28
+ const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
29
+
30
+ beforeEach(() => {
31
+ jest.resetAllMocks();
32
+ mockAddUseElement.mockReturnValue(mockAddElement);
33
+ mockRequestOpenExternalUrl.mockResolvedValue({ status: "completed" });
34
+ });
35
+
36
+ // this test uses a mock in place of the useAddElement hook
37
+ it("should add a text element when the button is clicked", () => {
38
+ // assert that the mocks are in the expected clean state
39
+ expect(mockAddUseElement).not.toHaveBeenCalled();
40
+ expect(mockAddElement).not.toHaveBeenCalled();
41
+
42
+ const result = renderInTestProvider(<App />);
43
+
44
+ // the hook should have been called in the render process but not the callback
45
+ expect(mockAddUseElement).toHaveBeenCalled();
46
+ expect(mockAddElement).not.toHaveBeenCalled();
47
+
48
+ // get a reference to the do something cool button element
49
+ const doSomethingCoolBtn = result.getByRole("button", {
50
+ name: "Do something cool",
51
+ });
52
+
53
+ // programmatically simulate clicking the button
54
+ fireEvent.click(doSomethingCoolBtn);
55
+
56
+ // we expect that addElement has been called by the button's click handler
57
+ expect(mockAddElement).toHaveBeenCalled();
58
+ });
59
+
60
+ // this test uses a mock in place of the @canva/platform requestOpenExternalUrl function
61
+ it("should call `requestOpenExternalUrl` when the button is clicked", () => {
62
+ expect(mockRequestOpenExternalUrl).not.toHaveBeenCalled();
63
+
64
+ const result = renderInTestProvider(<App />);
65
+
66
+ // get a reference to the Apps SDK button by name
67
+ const sdkButton = result.getByRole("button", {
68
+ name: "Open Canva Apps SDK docs",
69
+ });
70
+
71
+ expect(mockRequestOpenExternalUrl).not.toHaveBeenCalled();
72
+ fireEvent.click(sdkButton);
73
+ expect(mockRequestOpenExternalUrl).toHaveBeenCalled();
74
+
75
+ // assert that the requestOpenExternalUrl function was called with the expected arguments
76
+ expect(mockRequestOpenExternalUrl.mock.calls[0][0]).toEqual({
77
+ url: DOCS_URL,
78
+ });
79
+ });
80
+
81
+ // this test demonstrates the use of a snapshot test
82
+ it("should have a consistent snapshot", () => {
83
+ const result = renderInTestProvider(<App />);
84
+ expect(result.container).toMatchSnapshot();
85
+ });
86
+ });
@@ -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
  }