@canva/cli 0.0.1-beta.2 → 0.0.1-beta.21

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 (97) hide show
  1. package/README.md +203 -106
  2. package/cli.js +467 -394
  3. package/lib/cjs/index.cjs +388 -0
  4. package/lib/esm/index.mjs +388 -0
  5. package/package.json +12 -3
  6. package/templates/base/backend/routers/oauth.ts +393 -0
  7. package/templates/base/eslint.config.mjs +2 -4
  8. package/templates/base/package.json +31 -25
  9. package/templates/base/scripts/copy_env.ts +10 -0
  10. package/templates/base/scripts/start/app_runner.ts +39 -2
  11. package/templates/base/scripts/start/context.ts +12 -6
  12. package/templates/base/scripts/start/start.ts +11 -0
  13. package/templates/base/scripts/start/tests/start.tests.ts +61 -0
  14. package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
  15. package/templates/base/utils/backend/bearer_middleware/index.ts +1 -0
  16. package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
  17. package/templates/base/utils/use_add_element.ts +48 -0
  18. package/templates/base/utils/use_feature_support.ts +28 -0
  19. package/templates/base/{webpack.config.cjs → webpack.config.ts} +34 -42
  20. package/templates/common/.gitignore.template +5 -6
  21. package/templates/common/.nvmrc +1 -0
  22. package/templates/common/.prettierrc +21 -0
  23. package/templates/common/.vscode/extensions.json +6 -0
  24. package/templates/common/README.md +4 -74
  25. package/templates/common/conf/eslint-local-i18n-rules/index.mjs +181 -0
  26. package/templates/common/conf/{eslint-general.mjs → eslint_general.mjs} +40 -0
  27. package/templates/common/conf/eslint_i18n.mjs +41 -0
  28. package/templates/common/jest.config.mjs +29 -2
  29. package/templates/common/jest.setup.ts +20 -0
  30. package/templates/common/utils/backend/base_backend/create.ts +104 -0
  31. package/templates/common/utils/table_wrapper.ts +477 -0
  32. package/templates/common/utils/tests/table_wrapper.tests.ts +252 -0
  33. package/templates/common/utils/use_add_element.ts +48 -0
  34. package/templates/common/utils/use_feature_support.ts +28 -0
  35. package/templates/common/utils/use_overlay_hook.ts +74 -0
  36. package/templates/common/utils/use_selection_hook.ts +37 -0
  37. package/templates/dam/backend/routers/dam.ts +17 -12
  38. package/templates/dam/backend/server.ts +0 -7
  39. package/templates/dam/eslint.config.mjs +2 -2
  40. package/templates/dam/package.json +51 -39
  41. package/templates/dam/scripts/copy_env.ts +10 -0
  42. package/templates/dam/scripts/start/app_runner.ts +39 -2
  43. package/templates/dam/scripts/start/context.ts +12 -6
  44. package/templates/dam/scripts/start/start.ts +11 -0
  45. package/templates/dam/scripts/start/tests/start.tests.ts +61 -0
  46. package/templates/dam/src/app.tsx +26 -138
  47. package/templates/dam/src/config.ts +209 -87
  48. package/templates/{hello_world/webpack.config.cjs → dam/webpack.config.ts} +34 -42
  49. package/templates/gen_ai/README.md +1 -1
  50. package/templates/gen_ai/backend/routers/image.ts +3 -3
  51. package/templates/gen_ai/backend/server.ts +0 -7
  52. package/templates/gen_ai/eslint.config.mjs +2 -4
  53. package/templates/gen_ai/package.json +56 -42
  54. package/templates/gen_ai/scripts/copy_env.ts +10 -0
  55. package/templates/gen_ai/scripts/start/app_runner.ts +39 -2
  56. package/templates/gen_ai/scripts/start/context.ts +12 -6
  57. package/templates/gen_ai/scripts/start/start.ts +11 -0
  58. package/templates/gen_ai/scripts/start/tests/start.tests.ts +61 -0
  59. package/templates/gen_ai/src/api/api.ts +1 -39
  60. package/templates/gen_ai/src/app.tsx +16 -10
  61. package/templates/gen_ai/src/components/footer.messages.ts +0 -5
  62. package/templates/gen_ai/src/components/footer.tsx +2 -16
  63. package/templates/gen_ai/src/components/image_grid.tsx +8 -6
  64. package/templates/gen_ai/src/components/index.ts +0 -1
  65. package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
  66. package/templates/gen_ai/src/context/app_context.tsx +4 -26
  67. package/templates/gen_ai/src/context/context.messages.ts +1 -12
  68. package/templates/gen_ai/src/home.tsx +13 -0
  69. package/templates/gen_ai/src/index.tsx +2 -18
  70. package/templates/gen_ai/src/routes/routes.tsx +2 -2
  71. package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
  72. package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +1 -0
  73. package/templates/gen_ai/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
  74. package/templates/gen_ai/{webpack.config.cjs → webpack.config.ts} +34 -42
  75. package/templates/hello_world/eslint.config.mjs +2 -4
  76. package/templates/hello_world/package.json +55 -33
  77. package/templates/hello_world/scripts/copy_env.ts +10 -0
  78. package/templates/hello_world/scripts/start/app_runner.ts +39 -2
  79. package/templates/hello_world/scripts/start/context.ts +12 -6
  80. package/templates/hello_world/scripts/start/start.ts +11 -0
  81. package/templates/hello_world/scripts/start/tests/start.tests.ts +61 -0
  82. package/templates/hello_world/src/app.tsx +24 -2
  83. package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
  84. package/templates/hello_world/src/tests/app.tests.tsx +86 -0
  85. package/templates/hello_world/utils/use_add_element.ts +48 -0
  86. package/templates/hello_world/utils/use_feature_support.ts +28 -0
  87. package/templates/{dam/webpack.config.cjs → hello_world/webpack.config.ts} +34 -42
  88. package/templates/common/conf/eslint-i18n.mjs +0 -23
  89. package/templates/dam/backend/database/database.ts +0 -42
  90. package/templates/dam/backend/routers/auth.ts +0 -285
  91. package/templates/gen_ai/backend/routers/auth.ts +0 -285
  92. package/templates/gen_ai/src/components/logged_in_status.tsx +0 -44
  93. package/templates/gen_ai/src/services/auth.tsx +0 -31
  94. package/templates/gen_ai/src/services/index.ts +0 -1
  95. /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/index.ts +0 -0
  96. /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
  97. /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +0 -0
@@ -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,12 +1,14 @@
1
1
  /* eslint-disable no-console */
2
2
  import type { Context } from "./context";
3
3
  import * as chalk from "chalk";
4
- import { buildConfig } from "../../webpack.config.cjs";
4
+ import { buildConfig } from "../../webpack.config";
5
5
  import * as ngrok from "@ngrok/ngrok";
6
6
  import * as nodemon from "nodemon";
7
7
  import * as Table from "cli-table3";
8
8
  import * as webpack from "webpack";
9
9
  import * as WebpackDevServer from "webpack-dev-server";
10
+ import * as open from "open";
11
+ import { generatePreviewUrl } from "@canva/cli";
10
12
  import type { Certificate } from "../ssl/ssl";
11
13
  import { createOrRetrieveCertificate } from "../ssl/ssl";
12
14
 
@@ -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
  }
@@ -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,147 +1,35 @@
1
- import { useEffect, useState } from "react";
2
1
  import { SearchableListView } from "@canva/app-components";
3
- import { Alert, Box, Button, LoadingIndicator, Rows } from "@canva/app-ui-kit";
4
- import type { Authentication } from "@canva/user";
5
- import { auth } from "@canva/user";
2
+ import { Box } from "@canva/app-ui-kit";
3
+ import "@canva/app-ui-kit/styles.css";
4
+ import { useConfig } from "./config";
6
5
  import { findResources } from "./adapter";
7
- import { config } from "./config";
8
6
  import * as styles from "./index.css";
9
- import "@canva/app-ui-kit/styles.css";
10
-
11
- type AuthenticationState =
12
- | "authenticated"
13
- | "not_authenticated"
14
- | "checking"
15
- | "cancelled"
16
- | "error";
17
-
18
- /**
19
- * This endpoint is defined in the ./backend/server.ts file. You need to
20
- * register the endpoint in the Developer Portal before sending requests.
21
- *
22
- * BACKEND_HOST is configured in the root .env file, for more information,
23
- * refer to the README.md.
24
- */
25
- const AUTHENTICATION_CHECK_URL = `${BACKEND_HOST}/api/authentication/status`;
26
-
27
- const checkAuthenticationStatus = async (
28
- auth: Authentication,
29
- ): Promise<AuthenticationState> => {
30
- /**
31
- * Send a request to an endpoint that checks if the user is authenticated.
32
- * This is example code, intended to convey the basic idea. When implementing this in your app, you might want more advanced checks.
33
- *
34
- * Note: You must register the provided endpoint via the Developer Portal.
35
- */
36
- try {
37
- const token = await auth.getCanvaUserToken();
38
- const res = await fetch(AUTHENTICATION_CHECK_URL, {
39
- headers: {
40
- Authorization: `Bearer ${token}`,
41
- },
42
- method: "POST",
43
- });
44
- const body = await res.json();
45
-
46
- if (body?.isAuthenticated) {
47
- return "authenticated";
48
- } else {
49
- return "not_authenticated";
50
- }
51
- } catch (error) {
52
- // eslint-disable-next-line no-console
53
- console.error(error);
54
- return "error";
55
- }
56
- };
57
7
 
58
8
  export function App() {
59
- // Keep track of the user's authentication status.
60
- const [authState, setAuthState] = useState<AuthenticationState>("checking");
61
-
62
- useEffect(() => {
63
- setAuthState("checking");
64
- checkAuthenticationStatus(auth).then((status) => {
65
- setAuthState(status);
66
- });
67
- }, []);
68
-
69
- useEffect(() => {
70
- if (authState === "not_authenticated") {
71
- startAuthenticationFlow();
72
- }
73
- }, [authState]);
74
-
75
- const startAuthenticationFlow = async () => {
76
- try {
77
- const response = await auth.requestAuthentication();
78
- switch (response.status) {
79
- case "COMPLETED":
80
- setAuthState("authenticated");
81
- break;
82
- case "ABORTED":
83
- // eslint-disable-next-line no-console
84
- console.warn("Authentication aborted by user.");
85
- setAuthState("cancelled");
86
- break;
87
- case "DENIED":
88
- // eslint-disable-next-line no-console
89
- console.warn("Authentication denied by user", response.details);
90
- setAuthState("cancelled");
91
- break;
92
- default:
93
- // eslint-disable-next-line no-console
94
- console.log("Unknown auth state");
95
- break;
96
- }
97
- } catch (e) {
98
- // eslint-disable-next-line no-console
99
- console.error(e);
100
- setAuthState("error");
101
- }
102
- };
103
-
104
- if (authState === "error") {
105
- // eslint-disable-next-line no-console
106
- console.warn(
107
- "Warning: authentication not enabled on this app. Please enable auth with the instructions in README",
108
- );
109
- // Comment this next line out for production apps
110
- setAuthState("authenticated");
111
- }
112
-
113
- // If user has denied or aborted auth flow
114
- if (authState === "cancelled") {
115
- return (
116
- <Box paddingEnd="2u" height="full" className={styles.centerInPage}>
117
- <Rows spacing="2u" align="center">
118
- <Alert tone="critical">
119
- Something went wrong while authenticating
120
- </Alert>
121
- <Button
122
- variant="primary"
123
- onClick={startAuthenticationFlow}
124
- stretch={true}
125
- >
126
- Start authentication flow
127
- </Button>
128
- </Rows>
129
- </Box>
130
- );
131
- }
132
-
133
- return authState === "authenticated" ? (
9
+ const config = useConfig();
10
+ return (
134
11
  <Box className={styles.rootWrapper}>
135
- <SearchableListView config={config} findResources={findResources} />
136
- </Box>
137
- ) : (
138
- <Box
139
- width="full"
140
- height="full"
141
- paddingTop="1u"
142
- className={styles.centerInPage}
143
- >
144
- <LoadingIndicator size="large" />
12
+ <SearchableListView
13
+ config={config}
14
+ findResources={findResources}
15
+ // TODO remove `saveExportedDesign` and `config.export` if your app does not support exporting the Canva design into an external platform
16
+ saveExportedDesign={(
17
+ exportedDesignUrl: string,
18
+ containerId: string | undefined,
19
+ designTitle: string | undefined,
20
+ ) => {
21
+ // TODO update the function to save the design to your platform
22
+ return new Promise((resolve) => {
23
+ setTimeout(() => {
24
+ // eslint-disable-next-line no-console
25
+ console.info(
26
+ `Saving file "${designTitle}" from ${exportedDesignUrl} to ${config.serviceName} container id: ${containerId}`,
27
+ );
28
+ resolve({ success: true });
29
+ }, 1000);
30
+ });
31
+ }}
32
+ />
145
33
  </Box>
146
34
  );
147
35
  }