@canva/cli 0.0.1-beta.2 → 0.0.1-beta.20
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.
- package/README.md +184 -108
- package/cli.js +467 -394
- package/lib/cjs/index.cjs +388 -0
- package/lib/esm/index.mjs +388 -0
- package/package.json +12 -3
- package/templates/base/backend/routers/oauth.ts +393 -0
- package/templates/base/eslint.config.mjs +2 -4
- package/templates/base/package.json +31 -25
- package/templates/base/scripts/copy_env.ts +10 -0
- package/templates/base/scripts/start/app_runner.ts +39 -2
- package/templates/base/scripts/start/context.ts +12 -6
- package/templates/base/scripts/start/start.ts +11 -0
- package/templates/base/scripts/start/tests/start.tests.ts +61 -0
- package/templates/base/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
- package/templates/base/utils/backend/bearer_middleware/index.ts +1 -0
- package/templates/base/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
- package/templates/base/utils/use_add_element.ts +48 -0
- package/templates/base/utils/use_feature_support.ts +28 -0
- package/templates/base/{webpack.config.cjs → webpack.config.ts} +34 -42
- package/templates/common/.gitignore.template +5 -6
- package/templates/common/.nvmrc +1 -0
- package/templates/common/.prettierrc +21 -0
- package/templates/common/.vscode/extensions.json +6 -0
- package/templates/common/README.md +4 -74
- package/templates/common/conf/eslint-local-i18n-rules/index.mjs +181 -0
- package/templates/common/conf/{eslint-general.mjs → eslint_general.mjs} +40 -0
- package/templates/common/conf/eslint_i18n.mjs +41 -0
- package/templates/common/jest.config.mjs +29 -2
- package/templates/common/jest.setup.ts +20 -0
- package/templates/common/utils/backend/base_backend/create.ts +104 -0
- package/templates/common/utils/table_wrapper.ts +477 -0
- package/templates/common/utils/tests/table_wrapper.tests.ts +252 -0
- package/templates/common/utils/use_add_element.ts +48 -0
- package/templates/common/utils/use_feature_support.ts +28 -0
- package/templates/common/utils/use_overlay_hook.ts +74 -0
- package/templates/common/utils/use_selection_hook.ts +37 -0
- package/templates/dam/backend/routers/dam.ts +17 -12
- package/templates/dam/backend/server.ts +0 -7
- package/templates/dam/eslint.config.mjs +2 -2
- package/templates/dam/package.json +51 -39
- package/templates/dam/scripts/copy_env.ts +10 -0
- package/templates/dam/scripts/start/app_runner.ts +39 -2
- package/templates/dam/scripts/start/context.ts +12 -6
- package/templates/dam/scripts/start/start.ts +11 -0
- package/templates/dam/scripts/start/tests/start.tests.ts +61 -0
- package/templates/dam/src/app.tsx +26 -138
- package/templates/dam/src/config.ts +209 -87
- package/templates/{hello_world/webpack.config.cjs → dam/webpack.config.ts} +34 -42
- package/templates/gen_ai/README.md +1 -1
- package/templates/gen_ai/backend/routers/image.ts +3 -3
- package/templates/gen_ai/backend/server.ts +0 -7
- package/templates/gen_ai/eslint.config.mjs +2 -4
- package/templates/gen_ai/package.json +56 -42
- package/templates/gen_ai/scripts/copy_env.ts +10 -0
- package/templates/gen_ai/scripts/start/app_runner.ts +39 -2
- package/templates/gen_ai/scripts/start/context.ts +12 -6
- package/templates/gen_ai/scripts/start/start.ts +11 -0
- package/templates/gen_ai/scripts/start/tests/start.tests.ts +61 -0
- package/templates/gen_ai/src/api/api.ts +1 -39
- package/templates/gen_ai/src/app.tsx +16 -10
- package/templates/gen_ai/src/components/footer.messages.ts +0 -5
- package/templates/gen_ai/src/components/footer.tsx +2 -16
- package/templates/gen_ai/src/components/image_grid.tsx +8 -6
- package/templates/gen_ai/src/components/index.ts +0 -1
- package/templates/gen_ai/src/components/tests/remaining_credit.tests.tsx +43 -0
- package/templates/gen_ai/src/context/app_context.tsx +4 -26
- package/templates/gen_ai/src/context/context.messages.ts +1 -12
- package/templates/gen_ai/src/home.tsx +13 -0
- package/templates/gen_ai/src/index.tsx +2 -18
- package/templates/gen_ai/src/routes/routes.tsx +2 -2
- package/templates/gen_ai/utils/backend/bearer_middleware/bearer_middleware.ts +101 -0
- package/templates/gen_ai/utils/backend/bearer_middleware/index.ts +1 -0
- package/templates/gen_ai/utils/backend/bearer_middleware/tests/bearer_middleware.tests.ts +192 -0
- package/templates/gen_ai/{webpack.config.cjs → webpack.config.ts} +34 -42
- package/templates/hello_world/eslint.config.mjs +2 -4
- package/templates/hello_world/package.json +55 -33
- package/templates/hello_world/scripts/copy_env.ts +10 -0
- package/templates/hello_world/scripts/start/app_runner.ts +39 -2
- package/templates/hello_world/scripts/start/context.ts +12 -6
- package/templates/hello_world/scripts/start/start.ts +11 -0
- package/templates/hello_world/scripts/start/tests/start.tests.ts +61 -0
- package/templates/hello_world/src/app.tsx +24 -2
- package/templates/hello_world/src/tests/__snapshots__/app.tests.tsx.snap +45 -0
- package/templates/hello_world/src/tests/app.tests.tsx +86 -0
- package/templates/hello_world/utils/use_add_element.ts +48 -0
- package/templates/hello_world/utils/use_feature_support.ts +28 -0
- package/templates/{dam/webpack.config.cjs → hello_world/webpack.config.ts} +34 -42
- package/templates/common/conf/eslint-i18n.mjs +0 -23
- package/templates/dam/backend/database/database.ts +0 -42
- package/templates/dam/backend/routers/auth.ts +0 -285
- package/templates/gen_ai/backend/routers/auth.ts +0 -285
- package/templates/gen_ai/src/components/logged_in_status.tsx +0 -44
- package/templates/gen_ai/src/services/auth.tsx +0 -31
- package/templates/gen_ai/src/services/index.ts +0 -1
- /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/index.ts +0 -0
- /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/jwt_middleware.ts +0 -0
- /package/templates/{gen_ai → common}/utils/backend/jwt_middleware/tests/jwt_middleware.tests.ts +0 -0
|
@@ -3,8 +3,10 @@ import * as path from "path";
|
|
|
3
3
|
|
|
4
4
|
interface CliArgs {
|
|
5
5
|
example?: string;
|
|
6
|
-
useHttps
|
|
7
|
-
ngrok
|
|
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
|
|
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
|
|
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.
|
|
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,16 +1,31 @@
|
|
|
1
1
|
import { Button, Rows, Text } from "@canva/app-ui-kit";
|
|
2
|
-
import {
|
|
2
|
+
import { requestOpenExternalUrl } from "@canva/platform";
|
|
3
3
|
import { FormattedMessage, useIntl } from "react-intl";
|
|
4
4
|
import * as styles from "styles/components.css";
|
|
5
|
+
import { useAddElement } from "utils/use_add_element";
|
|
6
|
+
|
|
7
|
+
export const DOCS_URL = "https://www.canva.dev/docs/apps/";
|
|
5
8
|
|
|
6
9
|
export const App = () => {
|
|
10
|
+
const addElement = useAddElement();
|
|
11
|
+
|
|
7
12
|
const onClick = () => {
|
|
8
|
-
|
|
13
|
+
addElement({
|
|
9
14
|
type: "text",
|
|
10
15
|
children: ["Hello world!"],
|
|
11
16
|
});
|
|
12
17
|
};
|
|
13
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
|
+
|
|
14
29
|
const intl = useIntl();
|
|
15
30
|
|
|
16
31
|
return (
|
|
@@ -35,6 +50,13 @@ export const App = () => {
|
|
|
35
50
|
"Button text to do something cool. Creates a new text element when pressed.",
|
|
36
51
|
})}
|
|
37
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>
|
|
38
60
|
</Rows>
|
|
39
61
|
</div>
|
|
40
62
|
);
|
|
@@ -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 React from "react";
|
|
3
|
+
import type { RenderResult } from "@testing-library/react";
|
|
4
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
5
|
+
import { TestAppI18nProvider } from "@canva/app-i18n-kit";
|
|
6
|
+
import { TestAppUiProvider } from "@canva/app-ui-kit";
|
|
7
|
+
import { requestOpenExternalUrl } from "@canva/platform";
|
|
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
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EmbedElement,
|
|
3
|
+
ImageElement,
|
|
4
|
+
RichtextElement,
|
|
5
|
+
TableElement,
|
|
6
|
+
TextElement,
|
|
7
|
+
VideoElement,
|
|
8
|
+
} from "@canva/design";
|
|
9
|
+
import { addElementAtCursor, addElementAtPoint } from "@canva/design";
|
|
10
|
+
import { useFeatureSupport } from "./use_feature_support";
|
|
11
|
+
import { features } from "@canva/platform";
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
|
|
14
|
+
type AddElementParams =
|
|
15
|
+
| ImageElement
|
|
16
|
+
| VideoElement
|
|
17
|
+
| EmbedElement
|
|
18
|
+
| TextElement
|
|
19
|
+
| RichtextElement
|
|
20
|
+
| TableElement;
|
|
21
|
+
|
|
22
|
+
export const useAddElement = () => {
|
|
23
|
+
const isSupported = useFeatureSupport();
|
|
24
|
+
|
|
25
|
+
// Store a wrapped addElement function that checks feature support
|
|
26
|
+
const [addElement, setAddElement] = useState(() => {
|
|
27
|
+
return (element: AddElementParams) => {
|
|
28
|
+
if (features.isSupported(addElementAtPoint)) {
|
|
29
|
+
return addElementAtPoint(element);
|
|
30
|
+
} else if (features.isSupported(addElementAtCursor)) {
|
|
31
|
+
return addElementAtCursor(element);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const addElement = (element: AddElementParams) => {
|
|
38
|
+
if (isSupported(addElementAtPoint)) {
|
|
39
|
+
return addElementAtPoint(element);
|
|
40
|
+
} else if (isSupported(addElementAtCursor)) {
|
|
41
|
+
return addElementAtCursor(element);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
setAddElement(() => addElement);
|
|
45
|
+
}, [isSupported]);
|
|
46
|
+
|
|
47
|
+
return addElement;
|
|
48
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { features } from "@canva/platform";
|
|
2
|
+
import type { Feature } from "@canva/platform";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This hook allows re-rendering of a React component whenever
|
|
7
|
+
* the state of feature support changes in Canva.
|
|
8
|
+
*
|
|
9
|
+
* @returns isSupported - callback to inspect a Canva SDK method.
|
|
10
|
+
**/
|
|
11
|
+
export function useFeatureSupport() {
|
|
12
|
+
// Store a wrapped function that checks feature support
|
|
13
|
+
const [isSupported, setIsSupported] = useState(() => {
|
|
14
|
+
return (...args: Feature[]) => features.isSupported(...args);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
// create new function ref when feature support changes to trigger
|
|
19
|
+
// re-render
|
|
20
|
+
return features.registerOnSupportChange(() => {
|
|
21
|
+
setIsSupported(() => {
|
|
22
|
+
return (...args: Feature[]) => features.isSupported(...args);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return isSupported;
|
|
28
|
+
}
|
|
@@ -1,30 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
}: {
|
|
27
|
+
devConfig?: DevConfig;
|
|
28
|
+
appEntry?: string;
|
|
29
|
+
backendHost?: string;
|
|
30
|
+
} = {}): Configuration & DevServerConfiguration {
|
|
28
31
|
const mode = devConfig ? "development" : "production";
|
|
29
32
|
|
|
30
33
|
if (!backendHost) {
|
|
@@ -173,32 +176,23 @@ function buildConfig({
|
|
|
173
176
|
}),
|
|
174
177
|
// Apps can only submit a single JS file via the developer portal
|
|
175
178
|
new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
|
176
|
-
],
|
|
179
|
+
].filter(Boolean),
|
|
177
180
|
...buildDevConfig(devConfig),
|
|
178
181
|
};
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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) {
|
|
184
|
+
function buildDevConfig(options?: DevConfig): {
|
|
185
|
+
devtool?: string;
|
|
186
|
+
devServer?: DevServerConfiguration;
|
|
187
|
+
} {
|
|
194
188
|
if (!options) {
|
|
195
|
-
return
|
|
189
|
+
return {};
|
|
196
190
|
}
|
|
197
191
|
|
|
198
192
|
const { port, enableHmr, appOrigin, appId, enableHttps, certFile, keyFile } =
|
|
199
193
|
options;
|
|
200
194
|
|
|
201
|
-
let devServer = {
|
|
195
|
+
let devServer: DevServerConfiguration = {
|
|
202
196
|
server: enableHttps
|
|
203
197
|
? {
|
|
204
198
|
type: "https",
|
|
@@ -265,6 +259,4 @@ function buildDevConfig(options) {
|
|
|
265
259
|
};
|
|
266
260
|
}
|
|
267
261
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
module.exports.buildConfig = buildConfig;
|
|
262
|
+
export default buildConfig;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import formatjs from "eslint-plugin-formatjs";
|
|
2
|
-
|
|
3
|
-
export default [
|
|
4
|
-
{
|
|
5
|
-
plugins: {
|
|
6
|
-
formatjs,
|
|
7
|
-
},
|
|
8
|
-
rules: {
|
|
9
|
-
"formatjs/no-invalid-icu": "error",
|
|
10
|
-
"formatjs/no-literal-string-in-jsx": "error",
|
|
11
|
-
"formatjs/enforce-description": "error",
|
|
12
|
-
"formatjs/enforce-default-message": "error",
|
|
13
|
-
"formatjs/enforce-placeholders": "error",
|
|
14
|
-
"formatjs/no-id": "error",
|
|
15
|
-
"formatjs/no-emoji": "error",
|
|
16
|
-
"formatjs/no-useless-message": "error",
|
|
17
|
-
"formatjs/no-multiple-plurals": "error",
|
|
18
|
-
"formatjs/no-offset": "error",
|
|
19
|
-
"formatjs/blocklist-elements": [2, ["selectordinal"]],
|
|
20
|
-
"formatjs/no-complex-selectors": "error",
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
];
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
}
|