@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
@@ -1,30 +1,36 @@
1
- require("dotenv").config();
2
- const path = require("path");
3
- const TerserPlugin = require("terser-webpack-plugin");
4
- const { DefinePlugin, optimize } = require("webpack");
5
- const chalk = require("chalk");
6
- const { transform } = require("@formatjs/ts-transformer");
1
+ import type { Configuration } from "webpack";
2
+ import { DefinePlugin, optimize } from "webpack";
3
+ import * as path from "path";
4
+ import * as TerserPlugin from "terser-webpack-plugin";
5
+ import { transform } from "@formatjs/ts-transformer";
6
+ import * as chalk from "chalk";
7
+ import { config } from "dotenv";
8
+ import { Configuration as DevServerConfiguration } from "webpack-dev-server";
7
9
 
8
- /**
9
- *
10
- * @param {Object} [options]
11
- * @param {string} [options.appEntry=./src/index.tsx]
12
- * @param {string} [options.backendHost]
13
- * @param {Object} [options.devConfig]
14
- * @param {number} [options.devConfig.port]
15
- * @param {boolean} [options.devConfig.enableHmr]
16
- * @param {boolean} [options.devConfig.enableHttps]
17
- * @param {string} [options.devConfig.appOrigin]
18
- * @param {string} [options.devConfig.appId] - Deprecated in favour of appOrigin
19
- * @param {string} [options.devConfig.certFile]
20
- * @param {string} [options.devConfig.keyFile]
21
- * @returns {Object}
22
- */
23
- function buildConfig({
10
+ config();
11
+
12
+ type DevConfig = {
13
+ port: number;
14
+ enableHmr: boolean;
15
+ enableHttps: boolean;
16
+ appOrigin?: string;
17
+ appId?: string; // Deprecated in favour of appOrigin
18
+ certFile?: string;
19
+ keyFile?: string;
20
+ };
21
+
22
+ export function buildConfig({
24
23
  devConfig,
25
24
  appEntry = path.join(process.cwd(), "src", "index.tsx"),
26
25
  backendHost = process.env.CANVA_BACKEND_HOST,
27
- } = {}) {
26
+ // For IN_HARNESS, refer to the following docs for more information: https://www.canva.dev/docs/apps/test-harness/
27
+ inHarness = process.env.IN_HARNESS?.toLowerCase() === "true",
28
+ }: {
29
+ devConfig?: DevConfig;
30
+ appEntry?: string;
31
+ backendHost?: string;
32
+ inHarness?: boolean;
33
+ } = {}): Configuration & DevServerConfiguration {
28
34
  const mode = devConfig ? "development" : "production";
29
35
 
30
36
  if (!backendHost) {
@@ -45,9 +51,14 @@ function buildConfig({
45
51
  return {
46
52
  mode,
47
53
  context: path.resolve(process.cwd(), "./"),
48
- entry: {
49
- app: appEntry,
50
- },
54
+ entry: inHarness
55
+ ? {
56
+ harness: path.join(process.cwd(), "harness", "harness.tsx"),
57
+ init: path.join(process.cwd(), "harness", "init.ts"),
58
+ }
59
+ : {
60
+ app: appEntry,
61
+ },
51
62
  target: "web",
52
63
  resolve: {
53
64
  alias: {
@@ -59,7 +70,7 @@ function buildConfig({
59
70
  extensions: [".ts", ".tsx", ".js", ".css", ".svg", ".woff", ".woff2"],
60
71
  },
61
72
  infrastructureLogging: {
62
- level: "none",
73
+ level: inHarness ? "info" : "none",
63
74
  },
64
75
  module: {
65
76
  rules: [
@@ -171,34 +182,26 @@ function buildConfig({
171
182
  new DefinePlugin({
172
183
  BACKEND_HOST: JSON.stringify(backendHost),
173
184
  }),
174
- // Apps can only submit a single JS file via the developer portal
185
+ // Apps can only submit a single JS file via the Developer Portal
175
186
  new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
176
- ],
187
+ ].filter(Boolean),
177
188
  ...buildDevConfig(devConfig),
178
189
  };
179
190
  }
180
191
 
181
- /**
182
- *
183
- * @param {Object} [options]
184
- * @param {number} [options.port]
185
- * @param {boolean} [options.enableHmr]
186
- * @param {boolean} [options.enableHttps]
187
- * @param {string} [options.appOrigin]
188
- * @param {string} [options.appId] - Deprecated in favour of appOrigin
189
- * @param {string} [options.certFile]
190
- * @param {string} [options.keyFile]
191
- * @returns {Object|null}
192
- */
193
- function buildDevConfig(options) {
192
+ function buildDevConfig(options?: DevConfig): {
193
+ devtool?: string;
194
+ devServer?: DevServerConfiguration;
195
+ } {
194
196
  if (!options) {
195
- return null;
197
+ return {};
196
198
  }
197
199
 
198
200
  const { port, enableHmr, appOrigin, appId, enableHttps, certFile, keyFile } =
199
201
  options;
202
+ const host = "localhost";
200
203
 
201
- let devServer = {
204
+ let devServer: DevServerConfiguration = {
202
205
  server: enableHttps
203
206
  ? {
204
207
  type: "https",
@@ -208,7 +211,8 @@ function buildDevConfig(options) {
208
211
  },
209
212
  }
210
213
  : "http",
211
- host: "localhost",
214
+ host,
215
+ allowedHosts: [host],
212
216
  historyApiFallback: {
213
217
  rewrites: [{ from: /^\/$/, to: "/app.js" }],
214
218
  },
@@ -225,31 +229,13 @@ function buildDevConfig(options) {
225
229
  if (enableHmr && appOrigin) {
226
230
  devServer = {
227
231
  ...devServer,
228
- allowedHosts: new URL(appOrigin).hostname,
232
+ allowedHosts: [host, new URL(appOrigin).hostname],
229
233
  headers: {
230
234
  "Access-Control-Allow-Origin": appOrigin,
231
235
  "Access-Control-Allow-Credentials": "true",
232
236
  "Access-Control-Allow-Private-Network": "true",
233
237
  },
234
238
  };
235
- } else if (enableHmr && appId) {
236
- // Deprecated - App ID should not be used to configure HMR in the future and can be safely removed
237
- // after a few months.
238
-
239
- console.warn(
240
- "Enabling Hot Module Replacement (HMR) with an App ID is deprecated, please see the README.md on how to update.",
241
- );
242
-
243
- const appDomain = `app-${appId.toLowerCase().trim()}.canva-apps.com`;
244
- devServer = {
245
- ...devServer,
246
- allowedHosts: appDomain,
247
- headers: {
248
- "Access-Control-Allow-Origin": `https://${appDomain}`,
249
- "Access-Control-Allow-Credentials": "true",
250
- "Access-Control-Allow-Private-Network": "true",
251
- },
252
- };
253
239
  } else {
254
240
  if (enableHmr && !appOrigin) {
255
241
  console.warn(
@@ -265,6 +251,4 @@ function buildDevConfig(options) {
265
251
  };
266
252
  }
267
253
 
268
- module.exports = () => buildConfig();
269
-
270
- module.exports.buildConfig = buildConfig;
254
+ export default buildConfig;
@@ -22,48 +22,48 @@ const imageUrls: ImageResponse[] = [
22
22
  fullsize: {
23
23
  width: 1280,
24
24
  height: 853,
25
- url: "https://cdn.pixabay.com/photo/2023/02/03/05/11/youtube-background-7764170_1280.jpg",
25
+ url: "https://images.pexels.com/photos/1145720/pexels-photo-1145720.jpeg?auto=compress&cs=tinysrgb&w=1280&h=853&dpr=2",
26
26
  },
27
27
  thumbnail: {
28
28
  width: 640,
29
29
  height: 427,
30
- url: "https://cdn.pixabay.com/photo/2023/02/03/05/11/youtube-background-7764170_640.jpg",
30
+ url: "https://images.pexels.com/photos/1145720/pexels-photo-1145720.jpeg?auto=compress&cs=tinysrgb&w=640&h=427&dpr=2",
31
31
  },
32
32
  },
33
33
  {
34
34
  fullsize: {
35
35
  width: 1280,
36
36
  height: 853,
37
- url: "https://cdn.pixabay.com/photo/2023/02/03/05/12/youtube-background-7764172_1280.jpg",
37
+ url: "https://images.pexels.com/photos/4010108/pexels-photo-4010108.jpeg?auto=compress&cs=tinysrgb&w=1280&h=863&dpr=2",
38
38
  },
39
39
  thumbnail: {
40
40
  width: 640,
41
41
  height: 427,
42
- url: "https://cdn.pixabay.com/photo/2023/02/03/05/12/youtube-background-7764172_640.jpg",
42
+ url: "https://images.pexels.com/photos/4010108/pexels-photo-4010108.jpeg?auto=compress&cs=tinysrgb&w=640&h=427&dpr=2",
43
43
  },
44
44
  },
45
45
  {
46
46
  fullsize: {
47
47
  width: 1280,
48
48
  height: 853,
49
- url: "https://cdn.pixabay.com/photo/2023/02/03/05/07/colorful-7764162_1280.jpg",
49
+ url: "https://images.pexels.com/photos/1327496/pexels-photo-1327496.jpeg?auto=compress&cs=tinysrgb&w=1280&h=853&dpr=2",
50
50
  },
51
51
  thumbnail: {
52
52
  width: 640,
53
53
  height: 427,
54
- url: "https://cdn.pixabay.com/photo/2023/02/03/05/07/colorful-7764162_640.jpg",
54
+ url: "https://images.pexels.com/photos/1327496/pexels-photo-1327496.jpeg?auto=compress&cs=tinysrgb&w=640&h=427&dpr=2",
55
55
  },
56
56
  },
57
57
  {
58
58
  fullsize: {
59
59
  width: 1280,
60
60
  height: 853,
61
- url: "https://cdn.pixabay.com/photo/2023/02/03/04/57/swirls-7764142_1280.jpg",
61
+ url: "https://images.pexels.com/photos/4693135/pexels-photo-4693135.jpeg?auto=compress&cs=tinysrgb&w=1280&h=853&dpr=2",
62
62
  },
63
63
  thumbnail: {
64
64
  width: 640,
65
65
  height: 427,
66
- url: "https://cdn.pixabay.com/photo/2023/02/03/04/57/swirls-7764142_640.jpg",
66
+ url: "https://images.pexels.com/photos/4693135/pexels-photo-4693135.jpeg?auto=compress&cs=tinysrgb&w=640&h=427&dpr=2",
67
67
  },
68
68
  },
69
69
  ];
@@ -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 { createImageRouter } from "./routers/image";
5
5
 
@@ -0,0 +1,20 @@
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:asset:private:write",
8
+ "type": "mandatory"
9
+ },
10
+ {
11
+ "name": "canva:design:content:read",
12
+ "type": "mandatory"
13
+ },
14
+ {
15
+ "name": "canva:design:content:write",
16
+ "type": "mandatory"
17
+ }
18
+ ]
19
+ }
20
+ }
@@ -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
  {
@@ -23,15 +10,5 @@ export default [
23
10
  "**/*.config.*",
24
11
  ],
25
12
  },
26
- ...compat.extends(
27
- "eslint:recommended",
28
- "plugin:@typescript-eslint/recommended",
29
- "plugin:@typescript-eslint/eslint-recommended",
30
- "plugin:@typescript-eslint/strict",
31
- "plugin:@typescript-eslint/stylistic",
32
- "plugin:react/recommended",
33
- "plugin:jest/recommended",
34
- ),
35
- ...general,
36
- ...i18n,
13
+ ...canvaPlugin.configs.apps,
37
14
  ];
@@ -4,7 +4,7 @@
4
4
  "description": "An example app demonstrating common patterns that you might use in a generative AI 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,83 +12,88 @@
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-i18n-kit": "^1.0.1",
21
- "@canva/app-ui-kit": "^4.3.0",
22
- "@canva/asset": "^2.0.0",
23
- "@canva/design": "^2.2.1",
24
- "@canva/platform": "^2.0.0",
25
- "@canva/user": "^2.0.0",
21
+ "@canva/app-i18n-kit": "^1.0.3",
22
+ "@canva/app-ui-kit": "^4.10.0",
23
+ "@canva/asset": "^2.2.1",
24
+ "@canva/design": "^2.7.2",
25
+ "@canva/error": "^2.1.0",
26
+ "@canva/platform": "^2.2.0",
27
+ "@canva/user": "^2.1.1",
26
28
  "cookie-parser": "1.4.7",
27
29
  "cors": "2.8.5",
28
- "html-react-parser": "5.1.18",
29
- "obscenity": "0.4.0",
30
+ "html-react-parser": "5.2.5",
31
+ "obscenity": "0.4.4",
30
32
  "react": "18.3.1",
31
33
  "react-dom": "18.3.1",
32
- "react-error-boundary": "4.1.2",
34
+ "react-error-boundary": "6.0.0",
33
35
  "react-intl": "6.8.7",
34
- "react-router-dom": "6.28.0"
36
+ "react-router-dom": "7.6.3"
35
37
  },
36
38
  "devDependencies": {
37
- "@eslint/eslintrc": "3.1.0",
38
- "@eslint/js": "9.14.0",
39
- "@formatjs/cli": "6.3.8",
40
- "@formatjs/ts-transformer": "3.13.22",
41
- "@ngrok/ngrok": "1.4.1",
39
+ "@canva/app-eslint-plugin": "^1.0.0-beta.3",
40
+ "@canva/cli": ">= 0.0.1-beta.13",
41
+ "@formatjs/cli": "6.7.2",
42
+ "@formatjs/ts-transformer": "3.14.0",
43
+ "@ngrok/ngrok": "1.5.1",
44
+ "@pmmmwh/react-refresh-webpack-plugin": "0.6.1",
42
45
  "@svgr/webpack": "8.1.0",
46
+ "@testing-library/react": "16.3.0",
43
47
  "@types/debug": "4.1.12",
44
48
  "@types/express": "4.17.21",
45
- "@types/express-serve-static-core": "4.19.6",
49
+ "@types/express-serve-static-core": "5.0.6",
46
50
  "@types/jest": "29.5.14",
47
- "@types/jsonwebtoken": "9.0.7",
48
- "@types/node": "20.10.0",
49
- "@types/node-fetch": "2.6.11",
51
+ "@types/jsonwebtoken": "9.0.10",
52
+ "@types/node": "20.19.2",
53
+ "@types/node-fetch": "2.6.12",
50
54
  "@types/node-forge": "1.3.11",
51
55
  "@types/nodemon": "1.19.6",
52
56
  "@types/prompts": "2.4.9",
53
57
  "@types/react": "18.3.12",
54
58
  "@types/react-dom": "18.3.1",
55
- "@types/webpack-env": "1.18.5",
56
- "@typescript-eslint/eslint-plugin": "8.13.0",
57
- "@typescript-eslint/parser": "8.13.0",
59
+ "@types/webpack": "5.28.5",
60
+ "@types/webpack-dev-server": "4.7.2",
61
+ "@types/webpack-env": "1.18.8",
58
62
  "chalk": "4.1.2",
59
63
  "cli-table3": "0.6.5",
60
64
  "css-loader": "7.1.2",
61
65
  "css-modules-typescript-loader": "4.0.1",
62
- "cssnano": "7.0.6",
63
- "debug": "4.3.7",
64
- "dotenv": "16.4.5",
65
- "eslint": "9.14.0",
66
- "eslint-plugin-formatjs": "5.2.2",
67
- "eslint-plugin-jest": "28.9.0",
68
- "eslint-plugin-react": "7.37.2",
69
- "exponential-backoff": "3.1.1",
70
- "express": "4.21.1",
66
+ "cssnano": "7.0.7",
67
+ "debug": "4.4.1",
68
+ "dotenv": "16.6.0",
69
+ "exponential-backoff": "3.1.2",
70
+ "express": "4.21.2",
71
71
  "express-basic-auth": "1.2.1",
72
72
  "jest": "29.7.0",
73
+ "jest-css-modules-transform": "4.4.2",
74
+ "jest-environment-jsdom": "29.7.0",
73
75
  "jsonwebtoken": "9.0.2",
74
- "jwks-rsa": "3.1.0",
76
+ "jwks-rsa": "3.2.0",
75
77
  "mini-css-extract-plugin": "2.9.2",
76
78
  "node-fetch": "3.3.2",
77
79
  "node-forge": "1.3.1",
78
80
  "nodemon": "3.0.1",
81
+ "open": "8.4.2",
79
82
  "postcss-loader": "8.1.1",
80
- "prettier": "3.3.3",
83
+ "prettier": "3.6.2",
81
84
  "prompts": "2.4.2",
85
+ "react-refresh": "0.17.0",
82
86
  "style-loader": "4.0.0",
83
- "terser-webpack-plugin": "5.3.10",
84
- "ts-jest": "29.2.5",
85
- "ts-loader": "9.5.1",
87
+ "terser-webpack-plugin": "5.3.14",
88
+ "tree-kill": "1.2.2",
89
+ "ts-jest": "29.4.0",
90
+ "ts-loader": "9.5.2",
86
91
  "ts-node": "10.9.2",
87
- "typescript": "5.5.4",
92
+ "typescript": "5.8.2",
88
93
  "url-loader": "4.1.1",
89
- "webpack": "5.96.1",
90
- "webpack-cli": "5.1.4",
91
- "webpack-dev-server": "5.1.0",
94
+ "webpack": "5.99.9",
95
+ "webpack-cli": "6.0.1",
96
+ "webpack-dev-server": "5.2.2",
92
97
  "yargs": "17.7.2"
93
98
  }
94
99
  }
@@ -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",