17 |
Extension Tester
18 |
19 |
20 |
21 | {`Passed: `}
22 | {passedTests}
23 | {`, Failed: `}
24 | {failedTests}
25 | {`, Total: `}
26 | {totalTests}
27 |
28 |
29 |
30 |
31 |
50 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/modules/extensions/src/api/messages.ts:
--------------------------------------------------------------------------------
1 | import { extensionPort } from "../util/comlink";
2 |
3 | /**
4 | * Shows a confirmation toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID
5 | */
6 | export const showConfirm = async (str: string, length: number = 4000) => {
7 | if (typeof str !== "string") {
8 | throw new Error("Messages must be strings");
9 | }
10 |
11 | return extensionPort.showConfirm(str, length);
12 | };
13 |
14 | /**
15 | * Shows an error toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID
16 | */
17 | export const showError = async (str: string, length: number = 4000) => {
18 | if (typeof str !== "string") {
19 | throw new Error("Messages must be strings");
20 | }
21 |
22 | return extensionPort.showError(str, length);
23 | };
24 |
25 | /**
26 | * Shows a notice toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID
27 | */
28 | export const showNotice = async (str: string, length: number = 4000) => {
29 | if (typeof str !== "string") {
30 | throw new Error("Messages must be strings");
31 | }
32 |
33 | return extensionPort.showNotice(str, length);
34 | };
35 |
36 | /**
37 | * Shows a warning toast message within the Replit workspace for `length` milliseconds. Returns the ID of the message as a UUID
38 | */
39 | export const showWarning = async (str: string, length: number = 4000) => {
40 | if (typeof str !== "string") {
41 | throw new Error("Messages must be strings");
42 | }
43 |
44 | return extensionPort.showWarning(str, length);
45 | };
46 |
47 | /**
48 | * Hides a message by its IDs
49 | */
50 | export const hideMessage = async (id: string) => {
51 | return extensionPort.hideMessage(id);
52 | };
53 |
54 | /**
55 | * Hides all toast messages visible on the screens
56 | */
57 | export const hideAllMessages = async () => {
58 | return extensionPort.hideAllMessages();
59 | };
60 |
--------------------------------------------------------------------------------
/modules/extensions/buildTests/build.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import fs from "fs";
5 | import { version } from "../package.json";
6 |
7 | declare global {
8 | interface Window {
9 | replit: any;
10 | }
11 | }
12 |
13 | describe("dist/index.global.js (IIFE)", () => {
14 | test("exists", () => {
15 | expect(fs.existsSync("./dist/index.global.js")).toBe(true);
16 | });
17 | test("sourcemap file exists", () => {
18 | expect(fs.existsSync("./dist/index.global.js.map")).toBe(true);
19 | });
20 | test("evaluates to produce `replit` object on window", async () => {
21 | let replitPromise = new Promise((resolve, reject) => {
22 | const code = fs.readFileSync("./dist/index.global.js", "utf8");
23 |
24 | const scriptTag = document.createElement("script");
25 | scriptTag.type = "text/javascript";
26 | scriptTag.text = code;
27 | scriptTag.onload = () => {
28 | resolve(window.replit);
29 | };
30 | scriptTag.onerror = () => {
31 | reject(new Error("Failed to load script"));
32 | };
33 | document.body.appendChild(scriptTag);
34 | });
35 |
36 | expect(replitPromise).resolves.toBeDefined();
37 |
38 | const replit = await replitPromise;
39 | expect(replit).toBeDefined();
40 | expect((replit as any).version).toEqual(version);
41 | });
42 | });
43 |
44 | describe("dist/index.cjs (CommonJS)", () => {
45 | test("exists", () => {
46 | expect(fs.existsSync("./dist/index.cjs")).toBe(true);
47 | });
48 | test("sourcemap file exists", () => {
49 | expect(fs.existsSync("./dist/index.cjs.map")).toBe(true);
50 | });
51 | });
52 |
53 | describe("dist/index.js (ES Module)", () => {
54 | test("exists", () => {
55 | expect(fs.existsSync("./dist/index.js")).toBe(true);
56 | });
57 | test("sourcemap file exists", () => {
58 | expect(fs.existsSync("./dist/index.js.map")).toBe(true);
59 | });
60 | });
61 |
62 | describe("dist/index.d.ts (TypeScript defs)", () => {
63 | test("exists", () => {
64 | expect(fs.existsSync("./dist/index.d.ts")).toBe(true);
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "replit-extensions-client",
3 | "description": "Replit Extensions Monorepo (All Clients)",
4 | "scripts": {
5 | "tester": "cd modules/tester && pnpm dev",
6 | "dev": "cd modules/dev && pnpm dev",
7 | "lint": "turbo run lint",
8 | "lint:check": "turbo run lint:check",
9 | "type:check": "turbo run type:check",
10 | "test:build": "turbo run test:build",
11 | "build": "turbo run build",
12 | "clean": "turbo run clean",
13 | "publish:extensions": "turbo run lint:check type:check build && cd modules/extensions && pnpm publish",
14 | "publish:react": "turbo run lint:check type:check build && cd modules/extensions-react && pnpm publish",
15 | "publish:extensions:beta": "turbo run lint:check type:check build && cd modules/extensions && pnpm publish --tag beta",
16 | "publish:react:beta": "turbo run lint:check type:check build && cd modules/extensions-react && pnpm publish --tag beta",
17 | "test:extensions": "cd modules/extensions && turbo run build test:build --log-prefix=none",
18 | "test:react": "cd modules/extensions-react && turbo run build test:build --log-prefix=none",
19 | "test": "pnpm test:extensions && pnpm test:react"
20 | },
21 | "keywords": [
22 | "extensions",
23 | "api",
24 | "client",
25 | "replit"
26 | ],
27 | "author": "Replit",
28 | "license": "MIT",
29 | "engines": {
30 | "node": ">=18",
31 | "pnpm": ">=6"
32 | },
33 | "devDependencies": {
34 | "@changesets/cli": "^2.26.2",
35 | "@replit/extensions": "workspace:*",
36 | "@testing-library/jest-dom": "^5.17.0",
37 | "@types/jest": "^29.5.5",
38 | "@types/node": "^20.6.1",
39 | "@types/react": "^18.2.0",
40 | "@vitejs/plugin-react": "^4.0.4",
41 | "esbuild": "^0.15.18",
42 | "esbuild-jest": "^0.5.0",
43 | "jest": "^29.7.0",
44 | "jest-environment-jsdom": "^29.7.0",
45 | "prettier": "^2.8.8",
46 | "react": "^18.2.0",
47 | "react-dom": "^18.2.0",
48 | "tsup": "^6.7.0",
49 | "turbo": "^1.10.14",
50 | "typedoc": "^0.24.8",
51 | "typescript": "^4.9.5",
52 | "vite": "^4.4.9"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/modules/tester/src/tests/data.ts:
--------------------------------------------------------------------------------
1 | import { TestNamespace, TestObject } from "../types";
2 | import { data } from "@replit/extensions";
3 | import { assert, expect } from "chai";
4 |
5 | const tests: TestObject = {
6 | "currentUser gets the current user": async (log) => {
7 | const res = await data.currentUser();
8 |
9 | assert.isObject(res.user);
10 | assert.isNumber(res.user.id);
11 |
12 | log("Current User: " + res.user.username);
13 | },
14 | "userById fetches a Replit user by their ID": async () => {
15 | const res = await data.userById({
16 | id: 1,
17 | });
18 |
19 | assert.isObject(res.user);
20 | assert.isString(res.user.username);
21 | },
22 | "userByUsername fetches a Replit user by their username": async () => {
23 | const res = await data.userByUsername({
24 | username: "friend", // friend is the autogenerated user on local, and exists in prod as well
25 | });
26 |
27 | assert.isObject(res.userByUsername);
28 | assert.isString(res.userByUsername.username);
29 | },
30 | "currentRepl gets fetches the current Repl": async (log) => {
31 | const res = await data.currentRepl({
32 | includeOwner: true,
33 | });
34 |
35 | assert.isObject(res.repl);
36 | assert.isString(res.repl.id);
37 |
38 | log("Repl: " + res.repl.title + " by @" + res.repl.owner?.username);
39 | },
40 | "replById fetches a Repl by its ID": async () => {
41 | const currentRepl = await data.currentRepl();
42 | const id = currentRepl.repl.id;
43 |
44 | const res = await data.replById({ id });
45 |
46 | assert.isObject(res.repl);
47 | assert.isString(res.repl.id);
48 | assert.isString(res.repl.title);
49 | },
50 | "replByUrl fetches a Repl by its URL": async () => {
51 | const currentRepl = await data.currentRepl();
52 | const res = await data.replByUrl({ url: currentRepl.repl.url });
53 |
54 | assert.isObject(res.repl);
55 | assert.isString(res.repl.id);
56 | assert.isString(res.repl.title);
57 | },
58 | };
59 |
60 | const DataTests: TestNamespace = {
61 | module: "data",
62 | tests,
63 | };
64 |
65 | export default DataTests;
66 |
--------------------------------------------------------------------------------
/modules/extensions/src/api/data.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ReplDataInclusion,
3 | UserDataInclusion,
4 | CurrentUserDataInclusion,
5 | } from "../types";
6 | import { extensionPort } from "../util/comlink";
7 |
8 | /**
9 | * Fetches the current user via graphql
10 | */
11 | export async function currentUser(args: CurrentUserDataInclusion = {}) {
12 | return await extensionPort.currentUser(args);
13 | }
14 |
15 | /**
16 | * Fetches a user by their id via graphql
17 | */
18 | export async function userById(args: { id: number } & UserDataInclusion) {
19 | if (typeof args.id !== "number") {
20 | throw new Error(
21 | `Query parameter "id" must be a number. Found type ${typeof args.id} instead.`
22 | );
23 | }
24 |
25 | return await extensionPort.userById(args);
26 | }
27 |
28 | /**
29 | * Fetches a user by their username via graphql
30 | */
31 | export async function userByUsername(
32 | args: { username: string } & UserDataInclusion
33 | ) {
34 | if (typeof args.username !== "string") {
35 | throw new Error(
36 | `Query parameter "username" must be a string. Found type ${typeof args.username} instead.`
37 | );
38 | }
39 |
40 | return await extensionPort.userByUsername(args);
41 | }
42 |
43 | /**
44 | * Fetches the current Repl via graphql
45 | */
46 | export async function currentRepl(args: ReplDataInclusion = {}) {
47 | return await extensionPort.currentRepl(args);
48 | }
49 |
50 | /**
51 | * Fetches a Repl by its ID via graphql
52 | */
53 | export async function replById(args: { id: string } & ReplDataInclusion) {
54 | if (typeof args.id !== "string") {
55 | throw new Error(
56 | `Query parameter "id" must be a string. Found type ${typeof args.id} instead.`
57 | );
58 | }
59 |
60 | return await extensionPort.replById(args);
61 | }
62 |
63 | /**
64 | * Fetches a Repl by its URL via graphql
65 | */
66 | export async function replByUrl(args: { url: string } & ReplDataInclusion) {
67 | if (typeof args.url !== "string") {
68 | throw new Error(
69 | `Query parameter "url" must be a string. Found type ${typeof args.url} instead.`
70 | );
71 | }
72 |
73 | return await extensionPort.replByUrl(args);
74 | }
75 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to `@replit/extensions`
2 |
3 | We appreciate your interest in contributing to our project! As a team, we believe in the power of community-driven development and the potential of collaborative open-source projects to create and foster innovation.
4 |
5 | ## Getting Started
6 |
7 | 1. Fork the [repository](https://github.com/replit/extensions) by clicking the "Fork" button in the top-right corner of the page.
8 | 2. Clone your forked repository to your local machine using `git clone https://github.com/replit/extensions.git`.
9 | 3. Navigate into the repository with `cd extensions`.
10 | 4. Create a new branch for your feature, bug fix, or enhancement: `git checkout -b your-branch-name`.
11 | 5. Make your changes to the codebase.
12 | 6. Add and commit your changes using clear and concise commit messages: `git add .`, `git commit -m "Description of your changes"`.
13 | 7. Push your changes to your forked repository: `git push -u origin your-branch-name`.
14 | 8. Create a pull request from your forked repository to the original repository. Provide a descriptive title and comments explaining your proposed changes.
15 |
16 | ## Developer Guide
17 |
18 | 1. Import [this repository](https://replit.com/github/replit/extensions) onto Replit. Make sure you select Typescript as the language. If you make your own fork to create a Pull Request, import that onto Replit instead.
19 | 2. Install packages by running `pnpm install` in the shell
20 | 3. Run `git reset --hard origin/main` in the shell
21 | 4. Run the Repl
22 | 5. Open the Extension Devtools and press "Preview" on the Extensions API Tester
23 |
24 | ## Checklist
25 |
26 | 1. Ensure that your code does not have any syntax or compiler errors. You can check this by running `pnpm type:check` in the shell.
27 | 2. Make sure everything is formatted properly with `pnpm lint`.
28 | 3. Write a test plan showing how we can test that your new implementation works
29 | 4. Make sure tests pass as expected with the Extensions API Tester. If you introduce a new API method, make sure you write a test for it.
30 |
31 | ## Requesting Features or Enhancements
32 |
33 | We encourage suggestions to improve our project. If you have an idea for a new feature or enhancement, please create a post on the [Ask Forum](https://ask.replit.com) in the [relevant category](https://ask.replit.com/c/extensions).
34 |
--------------------------------------------------------------------------------
/modules/extensions/src/index.ts:
--------------------------------------------------------------------------------
1 | import { HandshakeStatus, ReplitInitArgs, ReplitInitOutput } from "./types";
2 | import { extensionPort, proxy } from "./util/comlink";
3 | import { getHandshakeStatus, setHandshakeStatus } from "./util/handshake";
4 | export * from "./api";
5 | export { extensionPort, proxy };
6 | export * from "./types";
7 | export * from "./commands";
8 | import * as replit from ".";
9 |
10 | import { version } from "../package.json";
11 | import { patchConsole } from "./util/patchConsole";
12 |
13 | export { version };
14 |
15 | function promiseWithTimeout