@canva/cli 0.0.1-beta.8 → 1.0.0

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 (150) hide show
  1. package/README.md +131 -31
  2. package/cli.js +852 -427
  3. package/lib/cjs/index.cjs +11 -0
  4. package/lib/esm/index.mjs +11 -0
  5. package/package.json +19 -4
  6. package/templates/base/backend/routers/auth.ts +4 -4
  7. package/templates/base/backend/routers/oauth.ts +3 -3
  8. package/templates/base/eslint.config.mjs +2 -25
  9. package/templates/base/package.json +29 -26
  10. package/templates/base/scripts/ssl/ssl.ts +1 -1
  11. package/templates/base/scripts/start/app_runner.ts +42 -5
  12. package/templates/base/scripts/start/context.ts +12 -6
  13. package/templates/base/scripts/start/start.ts +12 -1
  14. package/templates/base/scripts/start/tests/start.tests.ts +61 -0
  15. package/templates/base/utils/backend/base_backend/create.ts +3 -3
  16. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +2 -2
  17. package/templates/base/utils/backend/jwt_middleware/jwt_middleware.ts +4 -4
  18. package/templates/base/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +1 -1
  19. package/templates/base/utils/use_add_element.ts +1 -1
  20. package/templates/base/utils/use_feature_support.ts +1 -1
  21. package/templates/{dam/webpack.config.cjs → base/webpack.config.ts} +51 -67
  22. package/templates/common/.env.template +3 -3
  23. package/templates/common/README.md +16 -17
  24. package/templates/common/jest.config.mjs +29 -2
  25. package/templates/common/jest.setup.ts +20 -0
  26. package/templates/common/utils/backend/base_backend/create.ts +3 -3
  27. package/templates/common/utils/backend/jwt_middleware/jwt_middleware.ts +4 -4
  28. package/templates/common/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +1 -1
  29. package/templates/common/utils/tests/table_wrapper.tests.ts +1 -1
  30. package/templates/common/utils/use_add_element.ts +1 -1
  31. package/templates/common/utils/use_feature_support.ts +1 -1
  32. package/templates/dam/backend/routers/dam.ts +24 -20
  33. package/templates/dam/backend/server.ts +1 -1
  34. package/templates/dam/canva-app.json +16 -0
  35. package/templates/dam/eslint.config.mjs +2 -28
  36. package/templates/dam/package.json +43 -40
  37. package/templates/dam/scripts/ssl/ssl.ts +1 -1
  38. package/templates/dam/scripts/start/app_runner.ts +42 -5
  39. package/templates/dam/scripts/start/context.ts +12 -6
  40. package/templates/dam/scripts/start/start.ts +12 -1
  41. package/templates/dam/scripts/start/tests/start.tests.ts +61 -0
  42. package/templates/dam/src/app.tsx +24 -3
  43. package/templates/dam/src/config.ts +212 -87
  44. package/templates/dam/src/index.tsx +1 -1
  45. package/templates/dam/utils/backend/base_backend/create.ts +3 -3
  46. package/templates/dam/utils/backend/jwt_middleware/jwt_middleware.ts +4 -4
  47. package/templates/dam/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +1 -1
  48. package/templates/{base/webpack.config.cjs → dam/webpack.config.ts} +51 -67
  49. package/templates/data_connector/README.md +84 -0
  50. package/templates/data_connector/canva-app.json +21 -0
  51. package/templates/data_connector/declarations/declarations.d.ts +29 -0
  52. package/templates/data_connector/eslint.config.mjs +14 -0
  53. package/templates/data_connector/package.json +91 -0
  54. package/templates/data_connector/scripts/ssl/ssl.ts +131 -0
  55. package/templates/data_connector/scripts/start/app_runner.ts +201 -0
  56. package/templates/data_connector/scripts/start/context.ts +171 -0
  57. package/templates/data_connector/scripts/start/start.ts +46 -0
  58. package/templates/data_connector/scripts/start/tests/start.tests.ts +61 -0
  59. package/templates/data_connector/src/api/connect_client.ts +6 -0
  60. package/templates/data_connector/src/api/data_source.ts +96 -0
  61. package/templates/data_connector/src/api/data_sources/designs.tsx +296 -0
  62. package/templates/data_connector/src/api/data_sources/index.ts +4 -0
  63. package/templates/data_connector/src/api/data_sources/templates.tsx +329 -0
  64. package/templates/data_connector/src/api/fetch_data_table.ts +55 -0
  65. package/templates/data_connector/src/api/index.ts +4 -0
  66. package/templates/data_connector/src/api/oauth.ts +8 -0
  67. package/templates/data_connector/src/api/tests/data_source.test.tsx +99 -0
  68. package/templates/data_connector/src/app.tsx +20 -0
  69. package/templates/data_connector/src/components/app_error.tsx +15 -0
  70. package/templates/data_connector/src/components/footer.tsx +26 -0
  71. package/templates/data_connector/src/components/header.tsx +40 -0
  72. package/templates/data_connector/src/components/index.ts +3 -0
  73. package/templates/data_connector/src/components/inputs/messages.tsx +99 -0
  74. package/templates/data_connector/src/components/inputs/search_filter.tsx +108 -0
  75. package/templates/data_connector/src/components/inputs/select_field.tsx +26 -0
  76. package/templates/data_connector/src/context/app_context.tsx +124 -0
  77. package/templates/data_connector/src/context/index.ts +2 -0
  78. package/templates/data_connector/src/context/use_app_context.ts +17 -0
  79. package/templates/data_connector/src/entrypoint.tsx +70 -0
  80. package/templates/data_connector/src/home.tsx +21 -0
  81. package/templates/data_connector/src/index.tsx +68 -0
  82. package/templates/data_connector/src/pages/data_source_config.tsx +9 -0
  83. package/templates/data_connector/src/pages/error.tsx +37 -0
  84. package/templates/data_connector/src/pages/index.ts +4 -0
  85. package/templates/data_connector/src/pages/login.tsx +145 -0
  86. package/templates/data_connector/src/pages/select_source.tsx +24 -0
  87. package/templates/data_connector/src/routes/index.ts +2 -0
  88. package/templates/data_connector/src/routes/protected_route.tsx +25 -0
  89. package/templates/data_connector/src/routes/routes.tsx +46 -0
  90. package/templates/data_connector/src/utils/data_params.ts +17 -0
  91. package/templates/data_connector/src/utils/data_table.ts +115 -0
  92. package/templates/data_connector/src/utils/fetch_result.ts +36 -0
  93. package/templates/data_connector/src/utils/index.ts +2 -0
  94. package/templates/data_connector/src/utils/tests/data_table.test.ts +133 -0
  95. package/templates/data_connector/styles/components.css +38 -0
  96. package/templates/data_connector/tsconfig.json +54 -0
  97. package/templates/{gen_ai/webpack.config.cjs → data_connector/webpack.config.ts} +51 -67
  98. package/templates/gen_ai/backend/routers/image.ts +8 -8
  99. package/templates/gen_ai/backend/server.ts +1 -1
  100. package/templates/gen_ai/canva-app.json +20 -0
  101. package/templates/gen_ai/eslint.config.mjs +2 -25
  102. package/templates/gen_ai/package.json +47 -42
  103. package/templates/gen_ai/scripts/ssl/ssl.ts +1 -1
  104. package/templates/gen_ai/scripts/start/app_runner.ts +42 -5
  105. package/templates/gen_ai/scripts/start/context.ts +12 -6
  106. package/templates/gen_ai/scripts/start/start.ts +12 -1
  107. package/templates/gen_ai/scripts/start/tests/start.tests.ts +61 -0
  108. package/templates/gen_ai/src/app.tsx +16 -10
  109. package/templates/gen_ai/src/components/footer.tsx +4 -4
  110. package/templates/gen_ai/src/components/image_grid.tsx +1 -1
  111. package/templates/gen_ai/src/components/loading_results.tsx +8 -8
  112. package/templates/gen_ai/src/components/prompt_input.tsx +3 -3
  113. package/templates/gen_ai/src/components/remaining_credits.tsx +1 -1
  114. package/templates/gen_ai/src/components/report_box.tsx +1 -1
  115. package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
  116. package/templates/gen_ai/src/home.tsx +13 -0
  117. package/templates/gen_ai/src/index.tsx +2 -18
  118. package/templates/gen_ai/src/pages/error.tsx +2 -2
  119. package/templates/gen_ai/src/pages/results.tsx +1 -1
  120. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  121. package/templates/gen_ai/utils/backend/base_backend/create.ts +3 -3
  122. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +2 -2
  123. package/templates/{hello_world/webpack.config.cjs → gen_ai/webpack.config.ts} +51 -67
  124. package/templates/hello_world/canva-app.json +16 -0
  125. package/templates/hello_world/eslint.config.mjs +2 -25
  126. package/templates/hello_world/package.json +42 -37
  127. package/templates/hello_world/scripts/copy_env.ts +10 -0
  128. package/templates/hello_world/scripts/ssl/ssl.ts +1 -1
  129. package/templates/hello_world/scripts/start/app_runner.ts +42 -5
  130. package/templates/hello_world/scripts/start/context.ts +12 -6
  131. package/templates/hello_world/scripts/start/start.ts +12 -1
  132. package/templates/hello_world/scripts/start/tests/start.tests.ts +61 -0
  133. package/templates/hello_world/src/app.tsx +21 -1
  134. package/templates/hello_world/src/index.tsx +1 -1
  135. package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
  136. package/templates/hello_world/src/tests/app.tests.tsx +86 -0
  137. package/templates/hello_world/utils/use_add_element.ts +1 -1
  138. package/templates/hello_world/utils/use_feature_support.ts +1 -1
  139. package/templates/hello_world/webpack.config.ts +254 -0
  140. package/templates/optional/.cursor/mcp.json +8 -0
  141. package/templates/optional/.vscode/mcp.json +9 -0
  142. package/templates/optional/AGENTS.md +76 -0
  143. package/templates/optional/CLAUDE.md +76 -0
  144. package/templates/common/conf/eslint-general.mjs +0 -303
  145. package/templates/common/conf/eslint-i18n.mjs +0 -38
  146. /package/templates/base/scripts/{copy-env.ts → copy_env.ts} +0 -0
  147. /package/templates/dam/scripts/{copy-env.ts → copy_env.ts} +0 -0
  148. /package/templates/{gen_ai/scripts/copy-env.ts → data_connector/scripts/copy_env.ts} +0 -0
  149. /package/templates/{hello_world/scripts/copy-env.ts → gen_ai/scripts/copy_env.ts} +0 -0
  150. /package/templates/{common → optional}/.vscode/extensions.json +0 -0
@@ -77,7 +77,7 @@ You need to bypass the invalid security certificate warning every time you start
77
77
 
78
78
  </details>
79
79
 
80
- ### (Optional) Step 3: Enable Hot Module Replacement
80
+ ### Step 3 (Optional): Enable Hot Module Replacement
81
81
 
82
82
  By default, every time you make a change to an app, you have to reload the entire app to see the results of those changes. If you enable [Hot Module Replacement](https://webpack.js.org/concepts/hot-module-replacement/) (HMR), changes will be reflected without a full reload, which significantly speeds up the development loop.
83
83
 
@@ -86,23 +86,21 @@ By default, every time you make a change to an app, you have to reload the entir
86
86
  To enable HMR:
87
87
 
88
88
  1. Navigate to an app via the [Your apps](https://www.canva.com/developers/apps).
89
- 2. Select **Configure your app**.
90
- 3. Copy the value from the **App origin** field. This value is unique to each app and cannot be customized.
91
- 4. In the root directory, open the `.env` file.
92
- 5. Set the `CANVA_APP_ORIGIN` environment variable to the value copied from the **App origin** field:
89
+ 1. Select **Configuration** -> **Credentials** -> **.env file**.
90
+ 1. Copy the `.env` file contents.
91
+ 1. Paste the contents into the starter kit's `.env` file. For example:
93
92
 
94
93
  ```bash
95
- CANVA_APP_ORIGIN=# YOUR APP ORIGIN GOES HERE
94
+ CANVA_APP_ORIGIN=https://app-aabbccddeeff.canva-apps.com
95
+ CANVA_HMR_ENABLED=true
96
96
  ```
97
97
 
98
- 6. Set the `CANVA_HMR_ENABLED` environment variable to `true`:
98
+ 1. Restart the local development server.
99
+ 1. Reload the app manually to ensure that HMR takes effect.
99
100
 
100
- ```bash
101
- CANVA_HMR_ENABLED=true
102
- ```
101
+ ### Step 4 (Optional): Setup the Canva Dev MCP Server
103
102
 
104
- 7. Restart the local development server.
105
- 8. Reload the app manually to ensure that HMR takes effect.
103
+ If you're using AI coding tools, such as Cursor or Claude Code, you can connect to the Canva Dev MCP Server to supercharge your development workflow. See this [setup guide](https://www.canva.dev/docs/apps/mcp-server/) to get started.
106
104
 
107
105
  ## Running an app's backend
108
106
 
@@ -111,21 +109,22 @@ Some templates provide an example backend. This backend is defined in the templa
111
109
  To run templates that have a backend:
112
110
 
113
111
  1. Navigate to the [Your apps](https://www.canva.com/developers/apps) page.
114
- 2. Copy the ID of an app from the **App ID** column.
115
- 3. In the starter kit's `.env` file, set `CANVA_APP_ID` to the ID of the app.
112
+ 1. Select the app you want to run the example with.
113
+ 1. Copy your environment variables from **Configuration** -> **Credentials** -> **.env file**.
114
+ 1. Paste the contents into the starter kit's `.env` file.
116
115
 
117
116
  For example:
118
117
 
119
118
  ```bash
120
119
  CANVA_APP_ID=AABBccddeeff
121
- CANVA_APP_ORIGIN=#
120
+ CANVA_APP_ORIGIN=https://app-aabbccddeeff.canva-apps.com
122
121
  CANVA_BACKEND_PORT=3001
123
122
  CANVA_FRONTEND_PORT=8080
124
123
  CANVA_BACKEND_HOST=http://localhost:3001
125
- CANVA_HMR_ENABLED=FALSE
124
+ CANVA_HMR_ENABLED=TRUE
126
125
  ```
127
126
 
128
- 4. Start the app:
127
+ 1. Start the app:
129
128
 
130
129
  ```bash
131
130
  npm start
@@ -1,8 +1,35 @@
1
+ import { pathsToModuleNameMapper } from "ts-jest";
2
+ import tsconfig from "./tsconfig.json" with { type: "json" };
3
+
4
+ const { compilerOptions } = tsconfig;
5
+
1
6
  /** @type {import('ts-jest').JestConfigWithTsJest} */
2
7
 
3
8
  export default {
4
9
  preset: "ts-jest",
5
- testEnvironment: "node",
6
- testRegex: "(/tests/.*|(\\.|/)(tests))\\.ts?$",
10
+ testEnvironment: "jsdom",
11
+ testRegex: "(/tests/.*|(\\.|/)(tests))\\.tsx?$",
7
12
  modulePathIgnorePatterns: ["./internal/", "./node_modules/"],
13
+ modulePaths: [compilerOptions.baseUrl],
14
+ moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
15
+ transform: {
16
+ ".+\\.(css)$": "jest-css-modules-transform",
17
+ "^.+\\.tsx?$": [
18
+ "ts-jest",
19
+ {
20
+ astTransformers: {
21
+ before: [
22
+ {
23
+ path: "@formatjs/ts-transformer/ts-jest-integration",
24
+ options: {
25
+ overrideIdFn: "[sha512:contenthash:base64:6]",
26
+ ast: true,
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ ],
33
+ },
34
+ setupFiles: ["<rootDir>/jest.setup.ts"],
8
35
  };
@@ -0,0 +1,20 @@
1
+ // Import testing sub-packages
2
+ import * as asset from "@canva/asset/test";
3
+ import * as design from "@canva/design/test";
4
+ import * as error from "@canva/error/test";
5
+ import * as platform from "@canva/platform/test";
6
+ import * as user from "@canva/user/test";
7
+
8
+ // Initialize the test environments
9
+ asset.initTestEnvironment();
10
+ design.initTestEnvironment();
11
+ error.initTestEnvironment();
12
+ platform.initTestEnvironment();
13
+ user.initTestEnvironment();
14
+
15
+ // Once they're initialized, mock the SDKs
16
+ jest.mock("@canva/asset");
17
+ jest.mock("@canva/design");
18
+ jest.mock("@canva/platform");
19
+ jest.mock("@canva/user");
20
+ // n.b. @canva/error should not be mocked - use it to simulate API error responses from other mocks by throwing CanvaError
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable no-console */
2
+ import debug from "debug";
2
3
  import * as express from "express";
4
+ import type { NextFunction, Request, Response } from "express";
5
+ import * as fs from "fs";
3
6
  import * as http from "http";
4
7
  import * as https from "https";
5
- import * as fs from "fs";
6
- import type { Request, Response, NextFunction } from "express";
7
- import debug from "debug";
8
8
 
9
9
  const serverDebug = debug("server");
10
10
 
@@ -1,11 +1,11 @@
1
1
  /* eslint-disable no-console */
2
2
  import * as chalk from "chalk";
3
3
  import * as debug from "debug";
4
- import type { Request, Response, NextFunction } from "express";
5
- import * as jwt from "jsonwebtoken";
6
- import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";
4
+ import type { NextFunction, Request, Response } from "express";
7
5
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
6
  import Express from "express-serve-static-core";
7
+ import * as jwt from "jsonwebtoken";
8
+ import { JwksClient, SigningKeyNotFoundError } from "jwks-rsa";
9
9
 
10
10
  /**
11
11
  * Prefix your start command with `DEBUG=express:middleware:jwt` to enable debug logging
@@ -110,7 +110,7 @@ export function createJwtMiddleware(
110
110
  return sendUnauthorizedResponse(res);
111
111
  }
112
112
 
113
- req.canva = {
113
+ req["canva"] = {
114
114
  appId: payload.aud,
115
115
  brandId: payload.brandId,
116
116
  userId: payload.userId,
@@ -446,7 +446,7 @@ describe("createJwtMiddleware", () => {
446
446
  expect(res.status).not.toHaveBeenCalled();
447
447
  expect(res.json).not.toHaveBeenCalled();
448
448
 
449
- expect(req.canva).toEqual({
449
+ expect(req["canva"]).toEqual({
450
450
  userId: FAKE_USER_ID,
451
451
  brandId: FAKE_BRAND_ID,
452
452
  appId: FAKE_APP_ID,
@@ -1,5 +1,5 @@
1
- import { TableWrapper } from "../table_wrapper";
2
1
  import type { Cell, TableElement } from "@canva/design";
2
+ import { TableWrapper } from "../table_wrapper";
3
3
 
4
4
  describe("TableWrapper", () => {
5
5
  describe("create", () => {
@@ -7,9 +7,9 @@ import type {
7
7
  VideoElement,
8
8
  } from "@canva/design";
9
9
  import { addElementAtCursor, addElementAtPoint } from "@canva/design";
10
- import { useFeatureSupport } from "./use_feature_support";
11
10
  import { features } from "@canva/platform";
12
11
  import { useEffect, useState } from "react";
12
+ import { useFeatureSupport } from "./use_feature_support";
13
13
 
14
14
  type AddElementParams =
15
15
  | ImageElement
@@ -1,6 +1,6 @@
1
1
  import { features } from "@canva/platform";
2
2
  import type { Feature } from "@canva/platform";
3
- import { useState, useEffect } from "react";
3
+ import { useEffect, useState } from "react";
4
4
 
5
5
  /**
6
6
  * This hook allows re-rendering of a React component whenever
@@ -1,10 +1,6 @@
1
- import * as express from "express";
1
+ import type { Container, Resource } from "@canva/app-components";
2
2
  import * as crypto from "crypto";
3
- import type {
4
- Container,
5
- FindResourcesRequest,
6
- Resource,
7
- } from "@canva/app-components";
3
+ import * as express from "express";
8
4
 
9
5
  /**
10
6
  * Generates a unique hash for a url.
@@ -21,35 +17,43 @@ export async function generateHash(message: string) {
21
17
  }
22
18
 
23
19
  const imageUrls = [
24
- "https://cdn.pixabay.com/photo/2023/09/16/18/26/hummingbird-8257355_1280.jpg",
25
- "https://cdn.pixabay.com/photo/2023/12/10/03/00/peacock-8440548_1280.jpg",
26
- "https://cdn.pixabay.com/photo/2023/12/20/07/04/mountains-8459056_1280.jpg",
27
- "https://cdn.pixabay.com/photo/2023/11/26/07/29/sparrow-8413000_1280.jpg",
28
- "https://cdn.pixabay.com/photo/2023/12/12/16/11/mountain-8445543_1280.jpg",
20
+ "https://images.pexels.com/photos/1495580/pexels-photo-1495580.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
21
+ "https://images.pexels.com/photos/3943197/pexels-photo-3943197.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
22
+ "https://images.pexels.com/photos/7195267/pexels-photo-7195267.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
23
+ "https://images.pexels.com/photos/2904142/pexels-photo-2904142.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
24
+ "https://images.pexels.com/photos/5403478/pexels-photo-5403478.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2",
29
25
  ];
30
-
31
26
  export const createDamRouter = () => {
32
27
  const router = express.Router();
33
28
 
34
29
  /**
35
30
  * This endpoint returns the data for your app.
36
- *
37
- * TODO: you must change this path to `/api/resources/find` before
38
- * submitting to ensure a user is authenticated
39
31
  */
40
32
  router.post("/resources/find", async (req, res) => {
41
33
  // You should modify these lines to return data from your
42
34
  // digital asset manager (DAM) based on the findResourcesRequest
43
- const findResourcesRequest: FindResourcesRequest = req.body;
44
- const { types } = findResourcesRequest;
35
+ const {
36
+ types,
37
+ continuation,
38
+ locale,
39
+ // other available fields from the `FindResourcesRequest`
40
+ // containerTypes,
41
+ // limit,
42
+ // filters,
43
+ // query,
44
+ // sort,
45
+ // tab,
46
+ // containerId,
47
+ // parentContainerType,
48
+ } = req.body;
45
49
 
46
50
  let resources: Resource[] = [];
47
51
  if (types.includes("IMAGE")) {
48
52
  resources = await Promise.all(
49
53
  Array.from({ length: 40 }, async (_, i) => ({
50
- id: await generateHash(imageUrls[i % imageUrls.length]),
54
+ id: await generateHash(i + ""),
51
55
  mimeType: "image/jpeg",
52
- name: "My new thing",
56
+ name: `My new thing in ${locale}`, // Use the `locale` value from the request if your backend supports i18n
53
57
  type: "IMAGE",
54
58
  thumbnail: {
55
59
  url: imageUrls[i % imageUrls.length],
@@ -78,7 +82,7 @@ export const createDamRouter = () => {
78
82
 
79
83
  res.send({
80
84
  resources,
81
- continuation: +(findResourcesRequest.continuation || 0) + 1,
85
+ continuation: +(continuation || 0) + 1,
82
86
  });
83
87
  });
84
88
 
@@ -1,5 +1,5 @@
1
- import * as express from "express";
2
1
  import * as cors from "cors";
2
+ import * as express from "express";
3
3
  import { createBaseServer } from "../utils/backend/base_backend/create";
4
4
  import { createDamRouter } from "./routers/dam";
5
5
 
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://www.canva.dev/schemas/app/v1/manifest-schema.json",
3
+ "manifest_schema_version": 1,
4
+ "runtime": {
5
+ "permissions": [
6
+ {
7
+ "name": "canva:design:content:read",
8
+ "type": "mandatory"
9
+ },
10
+ {
11
+ "name": "canva:design:content:write",
12
+ "type": "mandatory"
13
+ }
14
+ ]
15
+ }
16
+ }
@@ -1,17 +1,4 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import js from "@eslint/js";
4
- import { FlatCompat } from "@eslint/eslintrc";
5
- import general from "./conf/eslint-general.mjs";
6
- // import i18n from "./conf/eslint-i18n.mjs";
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
- const compat = new FlatCompat({
11
- baseDirectory: __dirname,
12
- recommendedConfig: js.configs.recommended,
13
- allConfig: js.configs.all,
14
- });
1
+ import canvaPlugin from "@canva/app-eslint-plugin";
15
2
 
16
3
  export default [
17
4
  {
@@ -20,21 +7,8 @@ export default [
20
7
  "**/dist",
21
8
  "**/*.d.ts",
22
9
  "**/*.d.tsx",
23
- "**/sdk",
24
- "**/internal",
25
10
  "**/*.config.*",
26
11
  ],
27
12
  },
28
- ...compat.extends(
29
- "eslint:recommended",
30
- "plugin:@typescript-eslint/recommended",
31
- "plugin:@typescript-eslint/eslint-recommended",
32
- "plugin:@typescript-eslint/strict",
33
- "plugin:@typescript-eslint/stylistic",
34
- "plugin:react/recommended",
35
- "plugin:jest/recommended",
36
- ),
37
- ...general,
38
- // This template is not yet translated, enable these rules when it is
39
- // ...i18n,
13
+ ...canvaPlugin.configs.apps,
40
14
  ];
@@ -4,7 +4,7 @@
4
4
  "description": "An example app leveraging @canva/app-components to develop a digital asset management (DAM) app.",
5
5
  "scripts": {
6
6
  "extract": "formatjs extract \"src/**/*.{ts,tsx}\" --out-file dist/messages_en.json",
7
- "build": "webpack --config webpack.config.cjs --mode production && npm run extract",
7
+ "build": "webpack --config webpack.config.ts --mode production && npm run extract",
8
8
  "format": "prettier '**/*.{css,ts,tsx}' --no-config --write",
9
9
  "format:check": "prettier '**/*.{css,ts,tsx}' --no-config --check --ignore-path",
10
10
  "format:file": "prettier $1 --no-config --write",
@@ -12,18 +12,20 @@
12
12
  "lint:fix": "eslint . --fix",
13
13
  "lint:types": "tsc",
14
14
  "start": "ts-node ./scripts/start/start.ts",
15
+ "start:preview": "npm run start -- --preview",
15
16
  "test": "jest --no-cache",
16
17
  "test:watch": "jest --watchAll",
17
- "postinstall": "ts-node ./scripts/copy-env.ts"
18
+ "postinstall": "ts-node ./scripts/copy_env.ts"
18
19
  },
19
20
  "dependencies": {
20
- "@canva/app-components": "^1.0.0-beta.29",
21
- "@canva/app-i18n-kit": "^1.0.1",
22
- "@canva/app-ui-kit": "^4.3.0",
23
- "@canva/asset": "^2.0.0",
24
- "@canva/design": "^2.2.1",
25
- "@canva/platform": "^2.0.0",
26
- "@canva/user": "^2.0.0",
21
+ "@canva/app-components": "^1.3.1",
22
+ "@canva/app-i18n-kit": "^1.0.3",
23
+ "@canva/app-ui-kit": "^4.10.0",
24
+ "@canva/asset": "^2.2.1",
25
+ "@canva/design": "^2.7.2",
26
+ "@canva/error": "^2.1.0",
27
+ "@canva/platform": "^2.2.0",
28
+ "@canva/user": "^2.1.1",
27
29
  "cookie-parser": "1.4.7",
28
30
  "cors": "2.8.5",
29
31
  "react": "18.3.1",
@@ -31,62 +33,63 @@
31
33
  "react-intl": "6.8.7"
32
34
  },
33
35
  "devDependencies": {
34
- "@eslint/eslintrc": "3.1.0",
35
- "@eslint/js": "9.14.0",
36
- "@formatjs/cli": "6.3.8",
37
- "@formatjs/ts-transformer": "3.13.22",
38
- "@ngrok/ngrok": "1.4.1",
36
+ "@canva/app-eslint-plugin": "^1.0.0-beta.3",
37
+ "@canva/cli": ">= 0.0.1-beta.13",
38
+ "@formatjs/cli": "6.7.2",
39
+ "@formatjs/ts-transformer": "3.14.0",
40
+ "@ngrok/ngrok": "1.5.1",
41
+ "@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
39
42
  "@svgr/webpack": "8.1.0",
40
- "@types/cors": "2.8.17",
43
+ "@testing-library/react": "16.3.0",
44
+ "@types/cors": "2.8.19",
41
45
  "@types/debug": "4.1.12",
42
46
  "@types/express": "4.17.21",
43
- "@types/express-serve-static-core": "4.19.6",
47
+ "@types/express-serve-static-core": "5.0.6",
44
48
  "@types/jest": "29.5.14",
45
- "@types/jsonwebtoken": "9.0.7",
46
- "@types/node": "20.10.0",
47
- "@types/node-fetch": "2.6.11",
49
+ "@types/jsonwebtoken": "9.0.10",
50
+ "@types/node": "20.19.2",
51
+ "@types/node-fetch": "2.6.12",
48
52
  "@types/node-forge": "1.3.11",
49
53
  "@types/nodemon": "1.19.6",
50
54
  "@types/prompts": "2.4.9",
51
55
  "@types/react": "18.3.12",
52
56
  "@types/react-dom": "18.3.1",
53
- "@types/webpack-env": "1.18.5",
54
- "@typescript-eslint/eslint-plugin": "8.13.0",
55
- "@typescript-eslint/parser": "8.13.0",
57
+ "@types/webpack-env": "1.18.8",
56
58
  "chalk": "4.1.2",
57
59
  "cli-table3": "0.6.5",
58
60
  "css-loader": "7.1.2",
59
61
  "css-modules-typescript-loader": "4.0.1",
60
- "cssnano": "7.0.6",
61
- "debug": "4.3.7",
62
- "dotenv": "16.4.5",
63
- "eslint": "9.14.0",
64
- "eslint-plugin-formatjs": "5.2.2",
65
- "eslint-plugin-jest": "28.9.0",
66
- "eslint-plugin-react": "7.37.2",
67
- "exponential-backoff": "3.1.1",
68
- "express": "4.21.1",
62
+ "cssnano": "7.0.7",
63
+ "debug": "4.4.1",
64
+ "dotenv": "16.6.0",
65
+ "exponential-backoff": "3.1.2",
66
+ "express": "4.21.2",
69
67
  "express-basic-auth": "1.2.1",
70
68
  "jest": "29.7.0",
69
+ "jest-css-modules-transform": "4.4.2",
70
+ "jest-environment-jsdom": "29.7.0",
71
71
  "jsonwebtoken": "9.0.2",
72
- "jwks-rsa": "3.1.0",
72
+ "jwks-rsa": "3.2.0",
73
73
  "mini-css-extract-plugin": "2.9.2",
74
74
  "node-fetch": "3.3.2",
75
75
  "node-forge": "1.3.1",
76
76
  "nodemon": "3.0.1",
77
+ "open": "8.4.2",
77
78
  "postcss-loader": "8.1.1",
78
- "prettier": "3.3.3",
79
+ "prettier": "3.6.2",
79
80
  "prompts": "2.4.2",
81
+ "react-refresh": "0.17.0",
80
82
  "style-loader": "4.0.0",
81
- "terser-webpack-plugin": "5.3.10",
82
- "ts-jest": "29.2.5",
83
- "ts-loader": "9.5.1",
83
+ "terser-webpack-plugin": "5.3.14",
84
+ "tree-kill": "1.2.2",
85
+ "ts-jest": "29.4.0",
86
+ "ts-loader": "9.5.2",
84
87
  "ts-node": "10.9.2",
85
- "typescript": "5.5.4",
88
+ "typescript": "5.8.2",
86
89
  "url-loader": "4.1.1",
87
- "webpack": "5.96.1",
88
- "webpack-cli": "5.1.4",
89
- "webpack-dev-server": "5.1.0",
90
+ "webpack": "5.99.9",
91
+ "webpack-cli": "6.0.1",
92
+ "webpack-dev-server": "5.2.2",
90
93
  "yargs": "17.7.2"
91
94
  }
92
95
  }
@@ -1,7 +1,7 @@
1
1
  import * as crypto from "crypto";
2
+ import * as fs from "fs/promises";
2
3
  import { pki } from "node-forge";
3
4
  import * as path from "path";
4
- import * as fs from "fs/promises";
5
5
 
6
6
  const SSL_CERT_DIR = path.resolve(process.cwd(), "..", "..", ".ssl");
7
7
  const CERT_FILE = path.resolve(SSL_CERT_DIR, "certificate.pem");
@@ -1,14 +1,16 @@
1
1
  /* eslint-disable no-console */
2
- import type { Context } from "./context";
3
- import * as chalk from "chalk";
4
- import { buildConfig } from "../../webpack.config.cjs";
2
+ import { generatePreviewUrl } from "@canva/cli";
5
3
  import * as ngrok from "@ngrok/ngrok";
6
- import * as nodemon from "nodemon";
4
+ import * as chalk from "chalk";
7
5
  import * as Table from "cli-table3";
6
+ import * as nodemon from "nodemon";
7
+ import * as open from "open";
8
8
  import * as webpack from "webpack";
9
9
  import * as WebpackDevServer from "webpack-dev-server";
10
+ import { buildConfig } from "../../webpack.config";
10
11
  import type { Certificate } from "../ssl/ssl";
11
12
  import { createOrRetrieveCertificate } from "../ssl/ssl";
13
+ import type { Context } from "./context";
12
14
 
13
15
  export const infoChalk = chalk.blue.bold;
14
16
  export const warnChalk = chalk.bgYellow.bold;
@@ -46,12 +48,18 @@ export class AppRunner {
46
48
  }
47
49
  }
48
50
 
49
- const table = new Table();
51
+ const table = new Table({
52
+ colWidths: [30, 80],
53
+ wordWrap: true,
54
+ wrapOnWordBoundary: true,
55
+ });
50
56
 
51
57
  const server = await this.runWebpackDevServer(ctx, table, cert);
52
58
 
53
59
  await this.maybeRunBackendServer(ctx, table, cert, server);
54
60
 
61
+ await this.generateAndOpenPreviewUrl(ctx.openPreview, table);
62
+
55
63
  console.log(table.toString(), "\n");
56
64
 
57
65
  console.log(
@@ -161,4 +169,33 @@ export class AppRunner {
161
169
 
162
170
  return server;
163
171
  };
172
+
173
+ /**
174
+ * Calls the Canva CLI to generate a preview URL for the app
175
+ */
176
+ private readonly generateAndOpenPreviewUrl = async (
177
+ openPreview: boolean,
178
+ table: Table.Table,
179
+ ) => {
180
+ const previewCellHeader = { content: "Preview your app in Canva" };
181
+
182
+ const generatePreviewResult = await generatePreviewUrl();
183
+
184
+ if (!generatePreviewResult.success) {
185
+ table.push([
186
+ previewCellHeader,
187
+ { content: warnChalk(generatePreviewResult.message) },
188
+ ]);
189
+ return;
190
+ }
191
+
192
+ table.push([
193
+ previewCellHeader,
194
+ { content: "Preview URL", href: generatePreviewResult.data },
195
+ ]);
196
+
197
+ if (openPreview) {
198
+ open(generatePreviewResult.data);
199
+ }
200
+ };
164
201
  }
@@ -3,8 +3,10 @@ import * as path from "path";
3
3
 
4
4
  interface CliArgs {
5
5
  example?: string;
6
- useHttps?: boolean;
7
- ngrok?: boolean;
6
+ useHttps: boolean;
7
+ ngrok: boolean;
8
+ preview: boolean;
9
+ overrideFrontendPort?: number;
8
10
  }
9
11
 
10
12
  interface EnvVars {
@@ -45,7 +47,7 @@ export class Context {
45
47
  }
46
48
 
47
49
  get ngrokEnabled() {
48
- return !!this.args.ngrok;
50
+ return this.args.ngrok;
49
51
  }
50
52
 
51
53
  get hmrEnabled() {
@@ -53,7 +55,7 @@ export class Context {
53
55
  }
54
56
 
55
57
  get httpsEnabled() {
56
- return !!this.args.useHttps;
58
+ return this.args.useHttps;
57
59
  }
58
60
 
59
61
  get frontendEntryPath() {
@@ -69,11 +71,11 @@ export class Context {
69
71
  }
70
72
 
71
73
  get frontendUrl() {
72
- return `${this.protocol}://localhost:${this.envVars.frontendPort}`;
74
+ return `${this.protocol}://localhost:${this.frontendPort}`;
73
75
  }
74
76
 
75
77
  get frontendPort() {
76
- return this.envVars.frontendPort;
78
+ return this.args.overrideFrontendPort || this.envVars.frontendPort;
77
79
  }
78
80
 
79
81
  get developerBackendEntryPath(): string | undefined {
@@ -117,6 +119,10 @@ export class Context {
117
119
  return this.envVars.appId;
118
120
  }
119
121
 
122
+ get openPreview(): boolean {
123
+ return this.args.preview;
124
+ }
125
+
120
126
  private get protocol(): "https" | "http" {
121
127
  return this.httpsEnabled ? "https" : "http";
122
128
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as yargs from "yargs";
3
- import { AppRunner } from "./app_runner";
4
3
  import { hideBin } from "yargs/helpers";
4
+ import { AppRunner } from "./app_runner";
5
5
  import { Context } from "./context";
6
6
 
7
7
  const appRunner = new AppRunner();
@@ -23,6 +23,17 @@ yargs(hideBin(process.argv))
23
23
  default:
24
24
  process.env.npm_config_use_https?.toLocaleLowerCase().trim() === "true",
25
25
  })
26
+ .option("override-frontend-port", {
27
+ description:
28
+ "Port to run the local development server on. Overrides the frontend port set in the .env file.",
29
+ type: "number",
30
+ alias: "p",
31
+ })
32
+ .option("preview", {
33
+ description: "Open the app in Canva.",
34
+ type: "boolean",
35
+ default: false,
36
+ })
26
37
  .command(
27
38
  "$0",
28
39
  "Starts local development",