@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
@@ -4,7 +4,7 @@
4
4
  "description": "An empty Canva 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,72 +12,77 @@
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
+ "start:preview": "npm run start -- --preview",
16
+ "test": "jest --no-cache",
16
17
  "test:watch": "jest --watchAll",
17
- "postinstall": "ts-node ./scripts/copy-env.ts"
18
+ "test:update": "npm run test -- -u",
19
+ "postinstall": "ts-node ./scripts/copy_env.ts"
18
20
  },
19
21
  "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",
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",
25
29
  "react": "18.3.1",
26
30
  "react-dom": "18.3.1",
27
31
  "react-intl": "6.8.7"
28
32
  },
29
33
  "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",
34
- "@ngrok/ngrok": "1.4.1",
34
+ "@canva/app-eslint-plugin": "^1.0.0-beta.3",
35
+ "@canva/cli": ">= 0.0.1-beta.13",
36
+ "@formatjs/cli": "6.7.2",
37
+ "@formatjs/ts-transformer": "3.14.0",
38
+ "@ngrok/ngrok": "1.5.1",
39
+ "@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
35
40
  "@svgr/webpack": "8.1.0",
41
+ "@testing-library/react": "16.3.0",
36
42
  "@types/express": "4.17.21",
37
- "@types/express-serve-static-core": "4.19.6",
43
+ "@types/express-serve-static-core": "5.0.6",
38
44
  "@types/jest": "29.5.14",
39
- "@types/jsonwebtoken": "9.0.7",
40
- "@types/node": "20.10.0",
41
- "@types/node-fetch": "2.6.11",
45
+ "@types/jsonwebtoken": "9.0.10",
46
+ "@types/node": "20.19.2",
47
+ "@types/node-fetch": "2.6.12",
42
48
  "@types/node-forge": "1.3.11",
43
49
  "@types/nodemon": "1.19.6",
44
50
  "@types/react": "18.3.12",
45
51
  "@types/react-dom": "18.3.1",
46
- "@types/webpack-env": "1.18.5",
47
- "@typescript-eslint/eslint-plugin": "8.13.0",
48
- "@typescript-eslint/parser": "8.13.0",
52
+ "@types/webpack-env": "1.18.8",
49
53
  "chalk": "4.1.2",
50
54
  "cli-table3": "0.6.5",
51
55
  "css-loader": "7.1.2",
52
56
  "css-modules-typescript-loader": "4.0.1",
53
- "cssnano": "7.0.6",
54
- "debug": "4.3.7",
55
- "dotenv": "16.4.5",
56
- "eslint": "9.14.0",
57
- "eslint-plugin-formatjs": "5.2.2",
58
- "eslint-plugin-jest": "28.9.0",
59
- "eslint-plugin-react": "7.37.2",
60
- "express": "4.21.1",
57
+ "cssnano": "7.0.7",
58
+ "debug": "4.4.1",
59
+ "dotenv": "16.6.0",
60
+ "express": "4.21.2",
61
61
  "express-basic-auth": "1.2.1",
62
62
  "jest": "29.7.0",
63
+ "jest-css-modules-transform": "4.4.2",
64
+ "jest-environment-jsdom": "29.7.0",
63
65
  "jsonwebtoken": "9.0.2",
64
- "jwks-rsa": "3.1.0",
66
+ "jwks-rsa": "3.2.0",
65
67
  "mini-css-extract-plugin": "2.9.2",
66
68
  "node-fetch": "3.3.2",
67
69
  "node-forge": "1.3.1",
68
70
  "nodemon": "3.0.1",
71
+ "open": "8.4.2",
69
72
  "postcss-loader": "8.1.1",
70
- "prettier": "3.3.3",
73
+ "prettier": "3.6.2",
74
+ "react-refresh": "0.17.0",
71
75
  "style-loader": "4.0.0",
72
- "terser-webpack-plugin": "5.3.10",
73
- "ts-jest": "29.2.5",
74
- "ts-loader": "9.5.1",
76
+ "terser-webpack-plugin": "5.3.14",
77
+ "tree-kill": "1.2.2",
78
+ "ts-jest": "29.4.0",
79
+ "ts-loader": "9.5.2",
75
80
  "ts-node": "10.9.2",
76
- "typescript": "5.5.4",
81
+ "typescript": "5.8.2",
77
82
  "url-loader": "4.1.1",
78
- "webpack": "5.96.1",
79
- "webpack-cli": "5.1.4",
80
- "webpack-dev-server": "5.1.0",
83
+ "webpack": "5.99.9",
84
+ "webpack-cli": "6.0.1",
85
+ "webpack-dev-server": "5.2.2",
81
86
  "yargs": "17.7.2"
82
87
  }
83
88
  }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ const envPath = path.resolve(__dirname, "..", ".env");
6
+ const templatePath = path.resolve(__dirname, "..", ".env.template");
7
+
8
+ if (!fs.existsSync(envPath)) {
9
+ fs.copyFileSync(templatePath, envPath);
10
+ }
@@ -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",
@@ -0,0 +1,61 @@
1
+ /* eslint-disable no-console */
2
+
3
+ import type { ChildProcess } from "child_process";
4
+ import { spawn } from "child_process";
5
+ import * as treeKill from "tree-kill";
6
+
7
+ describe("start script", () => {
8
+ let serverProcess: ChildProcess;
9
+
10
+ afterEach(() => {
11
+ if (serverProcess?.pid && !serverProcess.exitCode) {
12
+ treeKill(serverProcess.pid);
13
+ }
14
+ });
15
+
16
+ it("should execute 'npm run start' and start a dev server", async () => {
17
+ const testServerPort = 8089;
18
+ serverProcess = await spawn(
19
+ `npm run start -- -p ${testServerPort} --no-preview`,
20
+ {
21
+ shell: true,
22
+ },
23
+ );
24
+
25
+ if (!serverProcess.pid) {
26
+ throw new Error("Unable to start server");
27
+ }
28
+
29
+ // wait for the server to start and collect output until it reports the running URL
30
+ let output = "";
31
+ await new Promise<void>((resolve, reject) => {
32
+ serverProcess.stdout?.on("data", (data) => {
33
+ output += data.toString();
34
+ if (output.includes("Development URL")) {
35
+ resolve();
36
+ }
37
+ });
38
+
39
+ serverProcess.stderr?.on("data", (data) => {
40
+ console.error("Server process error: ", data.toString());
41
+ reject();
42
+ });
43
+
44
+ // timeout and fail test if the server hasn't correctly started in 10 seconds
45
+ setTimeout(() => {
46
+ if (serverProcess?.pid && !serverProcess.exitCode) {
47
+ treeKill(serverProcess.pid);
48
+ }
49
+ reject(new Error("Test timed out"));
50
+ }, 10000);
51
+ });
52
+
53
+ // check that the server is running and accessible
54
+ const expectedServerURL = `http://localhost:${testServerPort}`;
55
+ expect(output).toContain(expectedServerURL);
56
+ expect(serverProcess.exitCode).toBeNull();
57
+
58
+ // clean up
59
+ treeKill(serverProcess.pid);
60
+ }, 15000); // 15 seconds timeout to allow for the server to start
61
+ });
@@ -1,7 +1,10 @@
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
- import * as styles from "styles/components.css";
4
4
  import { useAddElement } from "utils/use_add_element";
5
+ import * as styles from "styles/components.css";
6
+
7
+ export const DOCS_URL = "https://www.canva.dev/docs/apps/";
5
8
 
6
9
  export const App = () => {
7
10
  const addElement = useAddElement();
@@ -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
  );
@@ -1,8 +1,8 @@
1
+ import { AppI18nProvider } from "@canva/app-i18n-kit";
1
2
  import { AppUiProvider } from "@canva/app-ui-kit";
2
3
  import { createRoot } from "react-dom/client";
3
4
  import { App } from "./app";
4
5
  import "@canva/app-ui-kit/styles.css";
5
- import { AppI18nProvider } from "@canva/app-i18n-kit";
6
6
 
7
7
  const root = createRoot(document.getElementById("root") as Element);
8
8
  function render() {
@@ -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="aWBg0w aZskFA u16U_g"
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="_5KtATA LQzFZw VgvqkQ _8ERLTg rV61VQ LQzFZw VgvqkQ _4_iekA _3oHQrw j34Zww aqbYPg _3FvZZg"
23
+ type="button"
24
+ >
25
+ <span
26
+ class="khPe7Q"
27
+ >
28
+ Do something cool
29
+ </span>
30
+ </button>
31
+ <button
32
+ class="_5KtATA LQzFZw VgvqkQ _8ERLTg Z3nT2A LQzFZw VgvqkQ _4_iekA j34Zww aqbYPg _3FvZZg"
33
+ type="button"
34
+ >
35
+ <span
36
+ class="khPe7Q"
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 { TestAppI18nProvider } from "@canva/app-i18n-kit";
3
+ import { TestAppUiProvider } from "@canva/app-ui-kit";
4
+ import { requestOpenExternalUrl } from "@canva/platform";
5
+ import { fireEvent, render } from "@testing-library/react";
6
+ import type { RenderResult } from "@testing-library/react";
7
+ import React from "react";
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
+ });
@@ -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