@canva/cli 0.0.1-beta.1

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 (109) hide show
  1. package/LICENSE.md +48 -0
  2. package/README.md +206 -0
  3. package/cli.js +566 -0
  4. package/package.json +30 -0
  5. package/templates/base/backend/database/database.ts +42 -0
  6. package/templates/base/backend/routers/auth.ts +285 -0
  7. package/templates/base/declarations/declarations.d.ts +29 -0
  8. package/templates/base/eslint.config.mjs +309 -0
  9. package/templates/base/package.json +83 -0
  10. package/templates/base/scripts/ssl/ssl.ts +131 -0
  11. package/templates/base/scripts/start/app_runner.ts +164 -0
  12. package/templates/base/scripts/start/context.ts +165 -0
  13. package/templates/base/scripts/start/start.ts +35 -0
  14. package/templates/base/styles/components.css +38 -0
  15. package/templates/base/tsconfig.json +54 -0
  16. package/templates/base/utils/backend/base_backend/create.ts +104 -0
  17. package/templates/base/utils/backend/jwt_middleware/index.ts +1 -0
  18. package/templates/base/utils/backend/jwt_middleware/jwt_middleware.ts +229 -0
  19. package/templates/base/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +630 -0
  20. package/templates/base/webpack.config.cjs +270 -0
  21. package/templates/common/.env.template +6 -0
  22. package/templates/common/.gitignore.template +9 -0
  23. package/templates/common/LICENSE.md +48 -0
  24. package/templates/common/README.md +250 -0
  25. package/templates/common/jest.config.mjs +8 -0
  26. package/templates/dam/backend/database/database.ts +42 -0
  27. package/templates/dam/backend/routers/auth.ts +285 -0
  28. package/templates/dam/backend/routers/dam.ts +86 -0
  29. package/templates/dam/backend/server.ts +65 -0
  30. package/templates/dam/declarations/declarations.d.ts +29 -0
  31. package/templates/dam/eslint.config.mjs +309 -0
  32. package/templates/dam/package.json +90 -0
  33. package/templates/dam/scripts/ssl/ssl.ts +131 -0
  34. package/templates/dam/scripts/start/app_runner.ts +164 -0
  35. package/templates/dam/scripts/start/context.ts +165 -0
  36. package/templates/dam/scripts/start/start.ts +35 -0
  37. package/templates/dam/src/adapter.ts +44 -0
  38. package/templates/dam/src/app.tsx +147 -0
  39. package/templates/dam/src/config.ts +95 -0
  40. package/templates/dam/src/index.css +10 -0
  41. package/templates/dam/src/index.tsx +22 -0
  42. package/templates/dam/tsconfig.json +54 -0
  43. package/templates/dam/utils/backend/base_backend/create.ts +104 -0
  44. package/templates/dam/utils/backend/jwt_middleware/index.ts +1 -0
  45. package/templates/dam/utils/backend/jwt_middleware/jwt_middleware.ts +229 -0
  46. package/templates/dam/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +630 -0
  47. package/templates/dam/webpack.config.cjs +270 -0
  48. package/templates/gen_ai/README.md +27 -0
  49. package/templates/gen_ai/backend/database/database.ts +42 -0
  50. package/templates/gen_ai/backend/routers/auth.ts +285 -0
  51. package/templates/gen_ai/backend/routers/image.ts +234 -0
  52. package/templates/gen_ai/backend/server.ts +56 -0
  53. package/templates/gen_ai/declarations/declarations.d.ts +29 -0
  54. package/templates/gen_ai/eslint.config.mjs +309 -0
  55. package/templates/gen_ai/package.json +92 -0
  56. package/templates/gen_ai/scripts/ssl/ssl.ts +131 -0
  57. package/templates/gen_ai/scripts/start/app_runner.ts +164 -0
  58. package/templates/gen_ai/scripts/start/context.ts +165 -0
  59. package/templates/gen_ai/scripts/start/start.ts +35 -0
  60. package/templates/gen_ai/src/api/api.ts +228 -0
  61. package/templates/gen_ai/src/api/index.ts +1 -0
  62. package/templates/gen_ai/src/app.tsx +13 -0
  63. package/templates/gen_ai/src/components/app_error.tsx +18 -0
  64. package/templates/gen_ai/src/components/footer.messages.ts +53 -0
  65. package/templates/gen_ai/src/components/footer.tsx +157 -0
  66. package/templates/gen_ai/src/components/image_grid.tsx +96 -0
  67. package/templates/gen_ai/src/components/index.ts +8 -0
  68. package/templates/gen_ai/src/components/loading_results.tsx +169 -0
  69. package/templates/gen_ai/src/components/logged_in_status.tsx +44 -0
  70. package/templates/gen_ai/src/components/prompt_input.messages.ts +14 -0
  71. package/templates/gen_ai/src/components/prompt_input.tsx +149 -0
  72. package/templates/gen_ai/src/components/remaining_credits.tsx +75 -0
  73. package/templates/gen_ai/src/components/report_box.tsx +53 -0
  74. package/templates/gen_ai/src/config.ts +21 -0
  75. package/templates/gen_ai/src/context/app_context.tsx +174 -0
  76. package/templates/gen_ai/src/context/context.messages.ts +41 -0
  77. package/templates/gen_ai/src/context/index.ts +2 -0
  78. package/templates/gen_ai/src/context/use_app_context.ts +17 -0
  79. package/templates/gen_ai/src/index.tsx +31 -0
  80. package/templates/gen_ai/src/pages/error.tsx +41 -0
  81. package/templates/gen_ai/src/pages/generate.tsx +9 -0
  82. package/templates/gen_ai/src/pages/index.ts +3 -0
  83. package/templates/gen_ai/src/pages/results.tsx +31 -0
  84. package/templates/gen_ai/src/routes/index.ts +1 -0
  85. package/templates/gen_ai/src/routes/routes.tsx +26 -0
  86. package/templates/gen_ai/src/services/auth.tsx +31 -0
  87. package/templates/gen_ai/src/services/index.ts +1 -0
  88. package/templates/gen_ai/src/utils/index.ts +1 -0
  89. package/templates/gen_ai/src/utils/obscenity_filter.ts +33 -0
  90. package/templates/gen_ai/styles/components.css +38 -0
  91. package/templates/gen_ai/styles/utils.css +3 -0
  92. package/templates/gen_ai/tsconfig.json +54 -0
  93. package/templates/gen_ai/utils/backend/base_backend/create.ts +104 -0
  94. package/templates/gen_ai/utils/backend/jwt_middleware/index.ts +1 -0
  95. package/templates/gen_ai/utils/backend/jwt_middleware/jwt_middleware.ts +229 -0
  96. package/templates/gen_ai/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +630 -0
  97. package/templates/gen_ai/webpack.config.cjs +270 -0
  98. package/templates/hello_world/declarations/declarations.d.ts +29 -0
  99. package/templates/hello_world/eslint.config.mjs +309 -0
  100. package/templates/hello_world/package.json +73 -0
  101. package/templates/hello_world/scripts/ssl/ssl.ts +131 -0
  102. package/templates/hello_world/scripts/start/app_runner.ts +164 -0
  103. package/templates/hello_world/scripts/start/context.ts +165 -0
  104. package/templates/hello_world/scripts/start/start.ts +35 -0
  105. package/templates/hello_world/src/app.tsx +41 -0
  106. package/templates/hello_world/src/index.tsx +22 -0
  107. package/templates/hello_world/styles/components.css +38 -0
  108. package/templates/hello_world/tsconfig.json +54 -0
  109. package/templates/hello_world/webpack.config.cjs +270 -0
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@canva/cli",
3
+ "version": "0.0.1-beta.1",
4
+ "description": "The official Canva CLI.",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "author": "Canva Pty Ltd.",
7
+ "type": "module",
8
+ "bin": {
9
+ "canva": "./cli.js"
10
+ },
11
+ "files": [
12
+ "templates",
13
+ "cli.js",
14
+ "LICENSE.md",
15
+ "README.md"
16
+ ],
17
+ "dependencies": {
18
+ "ink": "5.0.1",
19
+ "react": "18.3.1"
20
+ },
21
+ "keywords": [
22
+ "apps sdk",
23
+ "canva",
24
+ "cli",
25
+ "starter kit"
26
+ ],
27
+ "engines": {
28
+ "node": ">=16"
29
+ }
30
+ }
@@ -0,0 +1,42 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+
4
+ /**
5
+ * This file creates a "database" out of a JSON file. It's only for
6
+ * demonstration purposes. A real app should use a real database.
7
+ */
8
+ const DATABASE_FILE_PATH = path.join(__dirname, "db.json");
9
+
10
+ interface Database<T> {
11
+ read(): Promise<T>;
12
+ write(data: T): Promise<void>;
13
+ }
14
+
15
+ export class JSONFileDatabase<T> implements Database<T> {
16
+ constructor(private readonly seedData: T) {}
17
+
18
+ // Creates a database file if one doesn't already exist
19
+ private async init(): Promise<void> {
20
+ try {
21
+ // Do nothing, since the database is initialized
22
+ await fs.stat(DATABASE_FILE_PATH);
23
+ } catch {
24
+ const file = JSON.stringify(this.seedData);
25
+ await fs.writeFile(DATABASE_FILE_PATH, file);
26
+ }
27
+ }
28
+
29
+ // Loads and parses the database file
30
+ async read(): Promise<T> {
31
+ await this.init();
32
+ const file = await fs.readFile(DATABASE_FILE_PATH, "utf8");
33
+ return JSON.parse(file);
34
+ }
35
+
36
+ // Overwrites the database file with provided data
37
+ async write(data: T): Promise<void> {
38
+ await this.init();
39
+ const file = JSON.stringify(data);
40
+ await fs.writeFile(DATABASE_FILE_PATH, file);
41
+ }
42
+ }
@@ -0,0 +1,285 @@
1
+ import * as chalk from "chalk";
2
+ import * as crypto from "crypto";
3
+ import "dotenv/config";
4
+ import * as express from "express";
5
+ import * as cookieParser from "cookie-parser";
6
+ import * as basicAuth from "express-basic-auth";
7
+ import { JSONFileDatabase } from "../database/database";
8
+ import { getTokenFromQueryString } from "../../utils/backend/jwt_middleware/jwt_middleware";
9
+ import { createJwtMiddleware } from "../../utils/backend/jwt_middleware";
10
+
11
+ /**
12
+ * These are the hard-coded credentials for logging in to this template.
13
+ */
14
+ const USERNAME = "username";
15
+ const PASSWORD = "password";
16
+
17
+ const COOKIE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
18
+
19
+ const CANVA_BASE_URL = "https://canva.com";
20
+
21
+ interface Data {
22
+ users: string[];
23
+ }
24
+
25
+ /**
26
+ * For more information on Authentication, refer to our documentation: {@link https://www.canva.dev/docs/apps/authenticating-users/#types-of-authentication}.
27
+ */
28
+ export const createAuthRouter = () => {
29
+ const APP_ID = getAppId();
30
+
31
+ /**
32
+ * Set up a database with a "users" table. In this example code, the
33
+ * database is simply a JSON file.
34
+ */
35
+ const db = new JSONFileDatabase<Data>({ users: [] });
36
+
37
+ const router = express.Router();
38
+
39
+ /**
40
+ * The `cookieParser` middleware allows the routes to read and write cookies.
41
+ *
42
+ * By passing a value into the middleware, we enable the "signed cookies" feature of Express.js. The
43
+ * value should be static and cryptographically generated. If it's dynamic (as shown below), cookies
44
+ * won't persist between server restarts.
45
+ *
46
+ * TODO: Replace `crypto.randomUUID()` with a static value, loaded via an environment variable.
47
+ */
48
+ router.use(cookieParser(crypto.randomUUID()));
49
+
50
+ /**
51
+ * This endpoint is hit at the start of the authentication flow. It contains a state which must
52
+ * be passed back to canva so that Canva can verify the response. It must also set a nonce in the
53
+ * user's browser cookies and send the nonce back to Canva as a url parameter.
54
+ *
55
+ * If Canva can validate the state, it will then redirect back to the chosen redirect url.
56
+ */
57
+ router.get("/configuration/start", async (req, res) => {
58
+ /**
59
+ * Generate a unique nonce for each request. A nonce is a random, single-use value
60
+ * that's impossible to guess or enumerate. We recommended using a Version 4 UUID that
61
+ * is cryptographically secure, such as one generated by the `randomUUID` method.
62
+ */
63
+ const nonce = crypto.randomUUID();
64
+ // Set the expiry time for the nonce. We recommend 5 minutes.
65
+ const expiry = Date.now() + COOKIE_EXPIRY_MS;
66
+
67
+ // Create a JSON string that contains the nonce and an expiry time
68
+ const nonceWithExpiry = JSON.stringify([nonce, expiry]);
69
+
70
+ // Set a cookie that contains the nonce and the expiry time
71
+ res.cookie("nonce", nonceWithExpiry, {
72
+ secure: true,
73
+ httpOnly: true,
74
+ maxAge: COOKIE_EXPIRY_MS,
75
+ signed: true,
76
+ });
77
+
78
+ // Create the query parameters that Canva requires
79
+ const params = new URLSearchParams({
80
+ nonce,
81
+ state: req?.query?.state?.toString() || "",
82
+ });
83
+
84
+ // Redirect to Canva with required parameters
85
+ res.redirect(302, `${CANVA_BASE_URL}/apps/configure/link?${params}`);
86
+ });
87
+
88
+ /**
89
+ * This endpoint renders a login page. Once the user logs in, they're
90
+ * redirected back to Canva, which completes the authentication flow.
91
+ */
92
+ router.get(
93
+ "/redirect-url",
94
+ /**
95
+ * Use a JSON Web Token (JWT) to verify incoming requests. The JWT is
96
+ * extracted from the `canva_user_token` parameter.
97
+ */
98
+ createJwtMiddleware(APP_ID, getTokenFromQueryString),
99
+ /**
100
+ * Warning: For demonstration purposes, we're using basic authentication and
101
+ * hard- coding a username and password. This is not a production-ready
102
+ * solution!
103
+ */
104
+ basicAuth({
105
+ users: { [USERNAME]: PASSWORD },
106
+ challenge: true,
107
+ }),
108
+ async (req, res) => {
109
+ const failureResponse = () => {
110
+ const params = new URLSearchParams({
111
+ success: "false",
112
+ state: req.query.state?.toString() || "",
113
+ });
114
+ res.redirect(302, `${CANVA_BASE_URL}/apps/configured?${params}`);
115
+ };
116
+
117
+ // Get the nonce and expiry time stored in the cookie.
118
+ const cookieNonceAndExpiry = req.signedCookies.nonce;
119
+
120
+ // Get the nonce from the query parameter.
121
+ const queryNonce = req.query.nonce?.toString();
122
+
123
+ // After reading the cookie, clear it. This forces abandoned auth flows to be restarted.
124
+ res.clearCookie("nonce");
125
+
126
+ let cookieNonce = "";
127
+ let expiry = 0;
128
+
129
+ try {
130
+ [cookieNonce, expiry] = JSON.parse(cookieNonceAndExpiry);
131
+ } catch {
132
+ // If the nonce can't be parsed, assume something has been compromised and exit.
133
+ return failureResponse();
134
+ }
135
+
136
+ // If the nonces are empty, exit the authentication flow.
137
+ if (
138
+ isEmpty(cookieNonceAndExpiry) ||
139
+ isEmpty(queryNonce) ||
140
+ isEmpty(cookieNonce)
141
+ ) {
142
+ return failureResponse();
143
+ }
144
+
145
+ /**
146
+ * Check that:
147
+ *
148
+ * - The nonce in the cookie and query parameter contain the same value
149
+ * - The nonce has not expired
150
+ *
151
+ * **Note:** We could rely on the cookie expiry, but that is vulnerable to tampering
152
+ * with the browser's time. This allows us to double-check based on server time.
153
+ */
154
+ if (expiry < Date.now() || cookieNonce !== queryNonce) {
155
+ return failureResponse();
156
+ }
157
+
158
+ // Get the userId from JWT middleware
159
+ const { userId } = req.canva;
160
+
161
+ // Load the database
162
+ const data = await db.read();
163
+
164
+ // Add the user to the database
165
+ if (!data.users.includes(userId)) {
166
+ data.users.push(userId);
167
+ await db.write(data);
168
+ }
169
+
170
+ // Create query parameters for redirecting back to Canva
171
+ const params = new URLSearchParams({
172
+ success: "true",
173
+ state: req?.query?.state?.toString() || "",
174
+ });
175
+
176
+ // Redirect the user back to Canva
177
+ res.redirect(302, `${CANVA_BASE_URL}/apps/configured?${params}`);
178
+ },
179
+ );
180
+
181
+ /**
182
+ * TODO: Add this middleware to all routes that will receive requests from
183
+ * your app.
184
+ */
185
+ const jwtMiddleware = createJwtMiddleware(APP_ID);
186
+
187
+ /**
188
+ * This endpoint is called when a user disconnects an app from their account.
189
+ * The app is expected to de-authenticate the user on its backend, so if the
190
+ * user reconnects the app, they'll need to re-authenticate.
191
+ *
192
+ * Note: The name of the endpoint is *not* configurable.
193
+ *
194
+ * Note: This endpoint is called by Canva's backend directly and must be
195
+ * exposed via a public URL. To test this endpoint, add a proxy URL, such as
196
+ * one generated by nGrok, to the 'Add authentication' section in the
197
+ * Developer Portal. Localhost addresses will not work to test this endpoint.
198
+ */
199
+ router.post("/configuration/delete", jwtMiddleware, async (req, res) => {
200
+ // Get the userId from JWT middleware
201
+ const { userId } = req.canva;
202
+
203
+ // Load the database
204
+ const data = await db.read();
205
+
206
+ // Remove the user from the database
207
+ await db.write({
208
+ users: data.users.filter((user) => user !== userId),
209
+ });
210
+
211
+ // Confirm that the user was removed
212
+ res.send({
213
+ type: "SUCCESS",
214
+ });
215
+ });
216
+
217
+ /**
218
+ * All routes that start with /api will be protected by JWT authentication
219
+ */
220
+ router.use("/api", jwtMiddleware);
221
+
222
+ /**
223
+ * This endpoint checks if a user is authenticated.
224
+ */
225
+ router.post("/api/authentication/status", async (req, res) => {
226
+ // Load the database
227
+ const data = await db.read();
228
+
229
+ // Check if the user is authenticated
230
+ const isAuthenticated = data.users.includes(req.canva.userId);
231
+
232
+ // Return the authentication status
233
+ res.send({
234
+ isAuthenticated,
235
+ });
236
+ });
237
+
238
+ return router;
239
+ };
240
+
241
+ /**
242
+ * Checks if a given param is nullish or an empty string
243
+ *
244
+ * @param str The string to check
245
+ * @returns true if the string is nullish or empty, false otherwise
246
+ */
247
+ function isEmpty(str?: string): boolean {
248
+ return str == null || str.length === 0;
249
+ }
250
+
251
+ /**
252
+ * Retrieves the CANVA_APP_ID from the environment variables.
253
+ * Throws an error if the CANVA_APP_ID environment variable is undefined or set to a default value.
254
+ *
255
+ * @returns {string} The Canva App ID
256
+ * @throws {Error} If CANVA_APP_ID environment variable is undefined or set to a default value
257
+ */
258
+ function getAppId(): string {
259
+ // TODO: Set the CANVA_APP_ID environment variable in the project's .env file
260
+ const appId = process.env.CANVA_APP_ID;
261
+
262
+ if (!appId) {
263
+ throw new Error(
264
+ `The CANVA_APP_ID environment variable is undefined. Set the variable in the project's .env file.`,
265
+ );
266
+ }
267
+
268
+ if (appId === "YOUR_APP_ID_HERE") {
269
+ // eslint-disable-next-line no-console
270
+ console.log(
271
+ chalk.bgRedBright(
272
+ "Default 'CANVA_APP_ID' environment variable detected.",
273
+ ),
274
+ );
275
+ // eslint-disable-next-line no-console
276
+ console.log(
277
+ "Please update the 'CANVA_APP_ID' environment variable in your project's `.env` file " +
278
+ `with the App ID obtained from the Canva Developer Portal: ${chalk.greenBright(
279
+ "https://www.canva.com/developers/apps",
280
+ )}\n`,
281
+ );
282
+ }
283
+
284
+ return appId;
285
+ }
@@ -0,0 +1,29 @@
1
+ declare module "*.css" {
2
+ const styles: { [className: string]: string };
3
+ export = styles;
4
+ }
5
+
6
+ declare module "*.jpg" {
7
+ const content: string;
8
+ export default content;
9
+ }
10
+
11
+ declare module "*.jpeg" {
12
+ const content: string;
13
+ export default content;
14
+ }
15
+
16
+ declare module "*.png" {
17
+ const content: string;
18
+ export default content;
19
+ }
20
+
21
+ declare module "*.svg" {
22
+ const content: React.FunctionComponent<{
23
+ size?: "tiny" | "small" | "medium" | "large";
24
+ className?: string;
25
+ }>;
26
+ export default content;
27
+ }
28
+
29
+ declare const BACKEND_HOST: string;
@@ -0,0 +1,309 @@
1
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
+ import jest from "eslint-plugin-jest";
3
+ import react from "eslint-plugin-react";
4
+ import globals from "globals";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import js from "@eslint/js";
8
+ import { FlatCompat } from "@eslint/eslintrc";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const compat = new FlatCompat({
13
+ baseDirectory: __dirname,
14
+ recommendedConfig: js.configs.recommended,
15
+ allConfig: js.configs.all,
16
+ });
17
+
18
+ export default [
19
+ {
20
+ ignores: [
21
+ "**/node_modules/",
22
+ "**/dist",
23
+ "**/*.d.ts",
24
+ "**/*.d.tsx",
25
+ "**/sdk",
26
+ "**/internal",
27
+ "**/*.config.*",
28
+ ],
29
+ },
30
+ ...compat.extends(
31
+ "eslint:recommended",
32
+ "plugin:@typescript-eslint/recommended",
33
+ "plugin:@typescript-eslint/eslint-recommended",
34
+ "plugin:@typescript-eslint/strict",
35
+ "plugin:@typescript-eslint/stylistic",
36
+ "plugin:react/recommended",
37
+ "plugin:jest/recommended"
38
+ ),
39
+ {
40
+ plugins: {
41
+ "@typescript-eslint": typescriptEslint,
42
+ jest,
43
+ react,
44
+ },
45
+ languageOptions: {
46
+ globals: {
47
+ ...globals.serviceworker,
48
+ ...globals.browser,
49
+ },
50
+ },
51
+ settings: {
52
+ react: {
53
+ version: "detect",
54
+ },
55
+ },
56
+ rules: {
57
+ "@typescript-eslint/no-non-null-assertion": "warn",
58
+ "@typescript-eslint/no-empty-function": "off",
59
+ "@typescript-eslint/consistent-type-imports": "error",
60
+ "@typescript-eslint/no-explicit-any": "warn",
61
+ "@typescript-eslint/no-empty-interface": "warn",
62
+ "@typescript-eslint/consistent-type-definitions": "off",
63
+ "@typescript-eslint/explicit-member-accessibility": [
64
+ "error",
65
+ {
66
+ accessibility: "no-public",
67
+ overrides: {
68
+ parameterProperties: "off",
69
+ },
70
+ },
71
+ ],
72
+ "@typescript-eslint/naming-convention": [
73
+ "error",
74
+ {
75
+ selector: "typeLike",
76
+ format: ["PascalCase"],
77
+ leadingUnderscore: "allow",
78
+ },
79
+ ],
80
+ "no-invalid-this": "off",
81
+ "@typescript-eslint/no-invalid-this": "error",
82
+ "@typescript-eslint/no-unused-expressions": [
83
+ "error",
84
+ {
85
+ allowShortCircuit: true,
86
+ allowTernary: true,
87
+ },
88
+ ],
89
+ "no-unused-vars": "off",
90
+ "@typescript-eslint/no-unused-vars": [
91
+ "error",
92
+ {
93
+ vars: "all",
94
+ varsIgnorePattern: "^_",
95
+ args: "none",
96
+ ignoreRestSiblings: true,
97
+ },
98
+ ],
99
+ "@typescript-eslint/no-require-imports": "error",
100
+ "jest/no-restricted-matchers": [
101
+ "error",
102
+ {
103
+ toContainElement:
104
+ "toContainElement is not recommended as it encourages testing the internals of the components",
105
+ toContainHTML:
106
+ "toContainHTML is not recommended as it encourages testing the internals of the components",
107
+ toHaveAttribute:
108
+ "toHaveAttribute is not recommended as it encourages testing the internals of the components",
109
+ toHaveClass:
110
+ "toHaveClass is not recommended as it encourages testing the internals of the components",
111
+ toHaveStyle:
112
+ "toHaveStyle is not recommended as it encourages testing the internals of the components",
113
+ },
114
+ ],
115
+ "react/jsx-curly-brace-presence": [
116
+ "error",
117
+ {
118
+ props: "never",
119
+ children: "never",
120
+ },
121
+ ],
122
+ "react/jsx-tag-spacing": [
123
+ "error",
124
+ {
125
+ closingSlash: "never",
126
+ beforeSelfClosing: "allow",
127
+ afterOpening: "never",
128
+ beforeClosing: "allow",
129
+ },
130
+ ],
131
+ "react/self-closing-comp": "error",
132
+ "react/no-unescaped-entities": "off",
133
+ "react/jsx-uses-react": "off",
134
+ "react/react-in-jsx-scope": "off",
135
+ "default-case": "error",
136
+ eqeqeq: [
137
+ "error",
138
+ "always",
139
+ {
140
+ null: "never",
141
+ },
142
+ ],
143
+ "no-caller": "error",
144
+ "no-console": "error",
145
+ "no-eval": "error",
146
+ "no-inner-declarations": "error",
147
+ "no-new-wrappers": "error",
148
+ "no-restricted-globals": [
149
+ "error",
150
+ {
151
+ name: "fit",
152
+ message: "Don't focus tests",
153
+ },
154
+ {
155
+ name: "fdescribe",
156
+ message: "Don't focus tests",
157
+ },
158
+ {
159
+ name: "length",
160
+ message:
161
+ "Undefined length - Did you mean to use window.length instead?",
162
+ },
163
+ {
164
+ name: "name",
165
+ message: "Undefined name - Did you mean to use window.name instead?",
166
+ },
167
+ {
168
+ name: "status",
169
+ message:
170
+ "Undefined status - Did you mean to use window.status instead?",
171
+ },
172
+ {
173
+ name: "spyOn",
174
+ message: "Don't use spyOn directly, use jest.spyOn",
175
+ },
176
+ ],
177
+ "no-restricted-properties": [
178
+ "error",
179
+ {
180
+ property: "bind",
181
+ message: "Don't o.f.bind(o, ...), use () => o.f(...)",
182
+ },
183
+ {
184
+ object: "ReactDOM",
185
+ property: "findDOMNode",
186
+ message: "Don't use ReactDOM.findDOMNode() as it is deprecated",
187
+ },
188
+ ],
189
+ "no-restricted-syntax": [
190
+ "error",
191
+ {
192
+ selector: "AccessorProperty, TSAbstractAccessorProperty",
193
+ message:
194
+ "Accessor property syntax is not allowed, use getter and setters.",
195
+ },
196
+ {
197
+ selector: "PrivateIdentifier",
198
+ message:
199
+ "Private identifiers are not allowed, use TypeScript private fields.",
200
+ },
201
+ {
202
+ selector:
203
+ "JSXOpeningElement[name.name = /^[A-Z]/] > JSXAttribute[name.name = /-/]",
204
+ message:
205
+ "Passing hyphenated props to custom components is not type-safe. Prefer a camelCased equivalent if available. (See https://github.com/microsoft/TypeScript/issues/55182)",
206
+ },
207
+ {
208
+ selector:
209
+ "CallExpression[callee.object.name='window'][callee.property.name='open']",
210
+ message:
211
+ "Apps are currently not allowed to open popups, or new tabs via browser APIs. Please use `requestOpenExternalUrl` from `@canva/platform` to link to external URLs. To learn more, see https://www.canva.dev/docs/apps/api/platform-request-open-external-url/",
212
+ },
213
+ ],
214
+ "no-return-await": "error",
215
+ "no-throw-literal": "error",
216
+ "no-undef-init": "error",
217
+ "no-var": "error",
218
+ "object-shorthand": "error",
219
+ "prefer-const": [
220
+ "error",
221
+ {
222
+ destructuring: "all",
223
+ },
224
+ ],
225
+ "prefer-object-spread": "error",
226
+ "prefer-rest-params": "error",
227
+ "prefer-spread": "error",
228
+ radix: "error",
229
+ },
230
+ },
231
+ {
232
+ files: ["**/*.tsx"],
233
+ rules: {
234
+ "react/no-deprecated": "error",
235
+ "react/forbid-elements": [
236
+ "error",
237
+ {
238
+ forbid: [
239
+ {
240
+ element: "video",
241
+ message:
242
+ "Don't use HTML video directly. Instead, use the App UI Kit <VideoCard /> as this respects users' auto-playing preferences",
243
+ },
244
+ {
245
+ element: "em",
246
+ message:
247
+ "Don't use <em> to italicize text. Canva's UI fonts don't support italic font style.",
248
+ },
249
+ {
250
+ element: "i",
251
+ message:
252
+ "Don't use <i> to italicize text. Canva's UI fonts don't support italic font style.",
253
+ },
254
+ {
255
+ element: "iframe",
256
+ message:
257
+ "Canva Apps aren't allowed to contain iframes. You should either recreate the UI you want to show in the iframe in the app directly, or link to your page via a `<Link>` tag. For more info see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
258
+ },
259
+ {
260
+ element: "script",
261
+ message:
262
+ "Script tags are not allowed in Canva SDK Apps. You should import JavaScript modules instead. For more info see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
263
+ },
264
+ {
265
+ element: "a",
266
+ message:
267
+ "Don't use <a> tags. Instead, use the <Link> component from the App UI Kit, and remember to open the url via the requestOpenExternalUrl method from @canva/platform.",
268
+ },
269
+ {
270
+ element: "img",
271
+ message:
272
+ "Have you considered using <ImageCard /> from the App UI Kit instead?",
273
+ },
274
+ {
275
+ element: "embed",
276
+ message:
277
+ "Have you considered using <EmbedCard /> from the App UI Kit instead?",
278
+ },
279
+ {
280
+ element: "audio",
281
+ message:
282
+ "Have you considered using <AudioCard /> from the App UI Kit instead?",
283
+ },
284
+ {
285
+ element: "button",
286
+ message:
287
+ "Rather than using the native HTML <button> element, use the <Button> component from the App UI Kit for consistency and accessibility.",
288
+ },
289
+ {
290
+ element: "input",
291
+ message:
292
+ "Wherever possible, prefer using the form inputs from the App UI Kit for consistency and accessibility (TextInput, Checkbox, FileInput, etc).",
293
+ },
294
+ {
295
+ element: "base",
296
+ message:
297
+ "The <base> tag is not supported in Canva Apps. We recommend using hash-based routing. For more on what is and isn't allowed in Canva Apps see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
298
+ },
299
+ {
300
+ element: "link",
301
+ message:
302
+ "If you're trying to include a css stylesheet, we recommend importing css using React, or using embedded stylesheets. For more on what is and isn't allowed in Canva Apps see https://www.canva.dev/docs/apps/content-security-policy/#what-is-and-isnt-allowed",
303
+ },
304
+ ],
305
+ },
306
+ ],
307
+ },
308
+ },
309
+ ];