diff --git a/docs/utils-reference/functions/createDeeplink.md b/docs/utils-reference/functions/createDeeplink.md new file mode 100644 index 0000000..747f668 --- /dev/null +++ b/docs/utils-reference/functions/createDeeplink.md @@ -0,0 +1,155 @@ +# `createDeeplink` + +Function that creates a deeplink for an extension or script command. + +## Signature + +There are three ways to use the function. + +The first one is for creating a deeplink to a command inside the current extension: + +```ts +function createDeeplink(options: { + type?: DeeplinkType.Extension, + command: string, + launchType?: LaunchType, + arguments?: LaunchProps["arguments"], + fallbackText?: string, +}): string; +``` + +The second one is for creating a deeplink to an extension that is not the current extension: + +```ts +function createDeeplink(options: { + type?: DeeplinkType.Extension, + ownerOrAuthorName: string, + extensionName: string, + command: string, + launchType?: LaunchType, + arguments?: LaunchProps["arguments"], + fallbackText?: string, +}): string; +``` + +The third one is for creating a deeplink to a script command: + +```ts +function createDeeplink(options: { + type: DeeplinkType.ScriptCommand, + command: string, + arguments?: string[], +}): string; +``` + +### Arguments + +#### Extension + +- `type` is the type of the deeplink. It must be `DeeplinkType.Extension`. +- `command` is the name of the command to deeplink to. +- `launchType` is the type of the launch. +- `arguments` is an object that contains the arguments to pass to the command. +- `fallbackText` is the text to show if the command is not available. +- For intra-extension deeplinks: + - `ownerOrAuthorName` is the name of the owner or author of the extension. + - `extensionName` is the name of the extension. + +#### Script command + +- `type` is the type of the deeplink. It must be `DeeplinkType.ScriptCommand`. +- `command` is the name of the script command to deeplink to. +- `arguments` is an array of strings to be passed as arguments to the script command. + +### Return + +Returns a string. + +## Example + +```tsx +import { Action, ActionPanel, LaunchProps, List } from "@raycast/api"; +import { createDeeplink, DeeplinkType } from "@raycast/utils"; + +export default function Command(props: LaunchProps<{ launchContext: { message: string } }>) { + console.log(props.launchContext?.message); + + return ( + + + + + } + /> + + + + } + /> + + + + } + /> + + ); +} +``` + +## Types + +### DeeplinkType + +A type to denote whether the deeplink is for a script command or an extension. + +```ts +export enum DeeplinkType { + /** A script command */ + ScriptCommand = "script-command", + /** An extension command */ + Extension = "extension", +} +``` diff --git a/docs/utils-reference/getting-started.md b/docs/utils-reference/getting-started.md index a404c3a..920c23e 100644 --- a/docs/utils-reference/getting-started.md +++ b/docs/utils-reference/getting-started.md @@ -16,6 +16,10 @@ npm install --save @raycast/utils ## Changelog +### v1.17.0 + +- Add a new [`createDeeplink`](./functions/createDeeplink.md) function. + ### v1.16.5 - Fixed the bug where `failureToastOptions` did not apply for `useExec` and `useStreamJSON` hooks. @@ -42,11 +46,11 @@ npm install --save @raycast/utils ### v1.15.0 -- Add `useLocalStorage` hook. +- Add [`useLocalStorage`](./react-hooks/useLocalStorage.md) hook. ### v1.14.0 -- Add `useStreamJSON` hook. +- Add [`useStreamJSON`](./react-hooks/useStreamJSON.md) hook. ### v1.13.6 diff --git a/package-lock.json b/package-lock.json index d441ae2..ef4765d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@raycast/utils", - "version": "1.16.4", + "version": "1.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@raycast/utils", - "version": "1.16.4", + "version": "1.17.0", "license": "MIT", "dependencies": { "cross-fetch": "^3.1.6", @@ -202,6 +202,7 @@ "integrity": "sha512-Zra/NnUnvHBshjUWlkaYFHMuB7PF1d3WiOen7xx8XSg0sUoLo80WiPsGC0yF/ZWefdKNwKZFWkXxLQfuYlIedg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@types/node": "18.8.3", "@types/react": "18.0.9", @@ -224,6 +225,13 @@ } } }, + "node_modules/@raycast/api/node_modules/@types/node": { + "version": "18.8.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz", + "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/content-type": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.6.tgz", @@ -243,10 +251,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.8.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz", - "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==", - "dev": true + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/object-hash": { "version": "3.0.4", @@ -255,16 +266,18 @@ "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true, + "license": "MIT" }, "node_modules/@types/react": { "version": "18.0.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -272,10 +285,11 @@ } }, "node_modules/@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/semver": { "version": "7.5.3", @@ -1113,10 +1127,11 @@ } }, "node_modules/csstype": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", - "dev": true + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" }, "node_modules/debug": { "version": "4.3.4", @@ -2559,7 +2574,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2636,6 +2652,7 @@ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -3096,6 +3113,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -3108,6 +3126,7 @@ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.28.0.tgz", "integrity": "sha512-sGIHDOpgVjRYgsi8NgosDnbkDvvkYFFSF900ZUhUw0+lSBEA5n76TcKFaVkfYMIuYm+7W6mT8Q673DLBfuTxcQ==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.22.0" @@ -3357,6 +3376,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", "dev": true, + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -3791,6 +3811,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", @@ -4024,6 +4050,14 @@ "@types/react": "18.0.9", "react": "18.1.0", "react-reconciler": "0.28.0" + }, + "dependencies": { + "@types/node": { + "version": "18.8.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz", + "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==", + "dev": true + } } }, "@types/content-type": { @@ -4045,10 +4079,13 @@ "dev": true }, "@types/node": { - "version": "18.8.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz", - "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==", - "dev": true + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } }, "@types/object-hash": { "version": "3.0.4", @@ -4057,9 +4094,9 @@ "dev": true }, "@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "dev": true }, "@types/react": { @@ -4074,9 +4111,9 @@ } }, "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", "dev": true }, "@types/semver": { @@ -4617,9 +4654,9 @@ } }, "csstype": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, "debug": { @@ -6491,6 +6528,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "untildify": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", diff --git a/package.json b/package.json index 3c20444..84c1f43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@raycast/utils", - "version": "1.16.5", + "version": "1.17.0", "description": "Set of utilities to streamline building Raycast extensions", "author": "Raycast Technologies Ltd.", "homepage": "https://developers.raycast.com/utils-reference", diff --git a/src/createDeeplink.ts b/src/createDeeplink.ts new file mode 100644 index 0000000..bf82815 --- /dev/null +++ b/src/createDeeplink.ts @@ -0,0 +1,152 @@ +import { environment, LaunchProps, LaunchType } from "@raycast/api"; +import fs from "node:fs"; +import path from "node:path"; + +export enum DeeplinkType { + /** A script command */ + ScriptCommand = "script-command", + /** An extension command */ + Extension = "extension", +} + +/** + * Options for creating a deeplink to a script command. + */ +export type CreateScriptCommandDeeplinkOptions = { + /** + * The type of deeplink, which should be "script-command". + */ + type: DeeplinkType.ScriptCommand; + /** + * The name of the command. + */ + command: string; + /** + * If the command accepts arguments, they can be passed using this query parameter. + */ + arguments?: string[]; +}; + +/** + * Base options for creating a deeplink to an extension. + */ +export type CreateExtensionDeeplinkBaseOptions = { + /** + * The type of deeplink, which should be "extension". + */ + type?: DeeplinkType.Extension; + /** + * The command associated with the extension. + */ + command: string; + /** + * Either "userInitiated", which runs the command in the foreground, or "background", which skips bringing Raycast to the front. + */ + launchType?: LaunchType; + /** + * If the command accepts arguments, they can be passed using this query parameter. + */ + arguments?: LaunchProps["arguments"]; + /** + * If the command make use of LaunchContext, it can be passed using this query parameter. + */ + context?: LaunchProps["launchContext"]; + /** + * Some text to prefill the search bar or first text input of the command + */ + fallbackText?: string; +}; + +/** + * Options for creating a deeplink to an extension from another extension. + * Requires both the ownerOrAuthorName and extensionName. + */ +export type CreateInterExtensionDeeplinkOptions = CreateExtensionDeeplinkBaseOptions & { + /** + * The name of the owner or author of the extension. + */ + ownerOrAuthorName: string; + /** + * The name of the extension. + */ + extensionName: string; +}; + +/** + * Options for creating a deeplink to an extension. + */ +export type CreateExtensionDeeplinkOptions = CreateInterExtensionDeeplinkOptions | CreateExtensionDeeplinkBaseOptions; + +/** + * Options for creating a deeplink. + */ +export type CreateDeeplinkOptions = CreateScriptCommandDeeplinkOptions | CreateExtensionDeeplinkOptions; + +function getProtocol() { + return environment.raycastVersion.includes("alpha") ? "raycastinternal://" : "raycast://"; +} + +function getOwnerOrAuthorName() { + const packageJSON = JSON.parse(fs.readFileSync(path.join(environment.assetsPath, "..", "package.json"), "utf8")); + return packageJSON.owner || packageJSON.author; +} + +export function createScriptCommandDeeplink(options: CreateScriptCommandDeeplinkOptions): string { + let url = `${getProtocol()}script-commands/${options.command}`; + + if (options.arguments) { + let params = ""; + for (const arg of options.arguments) { + params += "&arguments=" + encodeURIComponent(arg); + } + url += "?" + params.substring(1); + } + + return url; +} + +export function createExtensionDeeplink(options: CreateExtensionDeeplinkOptions): string { + let ownerOrAuthorName = getOwnerOrAuthorName(); + let extensionName = environment.extensionName; + + if ("ownerOrAuthorName" in options && "extensionName" in options) { + ownerOrAuthorName = options.ownerOrAuthorName; + extensionName = options.extensionName; + } + + let url = `${getProtocol()}extensions/${ownerOrAuthorName}/${extensionName}/${options.command}`; + + let params = ""; + if (options.launchType) { + params += "&launchType=" + encodeURIComponent(options.launchType); + } + + if (options.arguments) { + params += "&arguments=" + encodeURIComponent(JSON.stringify(options.arguments)); + } + + if (options.context) { + params += "&context=" + encodeURIComponent(JSON.stringify(options.context)); + } + + if (options.fallbackText) { + params += "&fallbackText=" + encodeURIComponent(options.fallbackText); + } + + if (params) { + url += "?" + params.substring(1); + } + + return url; +} + +/** + * Creates a deeplink to a script command or extension. + */ +export function createDeeplink(options: CreateDeeplinkOptions): string { + if (options.type === DeeplinkType.ScriptCommand) { + return createScriptCommandDeeplink(options); + } else { + return createExtensionDeeplink(options); + } +} diff --git a/src/index.ts b/src/index.ts index 22b004c..647a0b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,5 +17,7 @@ export * from "./oauth"; export * from "./run-applescript"; export * from "./showFailureToast"; +export * from "./createDeeplink"; + export type { AsyncState, MutatePromise } from "./types"; export type { Response } from "cross-fetch"; diff --git a/tests/package-lock.json b/tests/package-lock.json index e581206..204a8b1 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -32,7 +32,7 @@ "stream-json": "^1.8.0" }, "devDependencies": { - "@raycast/api": "1.52.0", + "@raycast/api": "1.79.1", "@types/content-type": "^1.1.6", "@types/object-hash": "^3.0.4", "@types/signal-exit": "^3.0.2", @@ -4010,7 +4010,7 @@ "@raycast/utils": { "version": "file:..", "requires": { - "@raycast/api": "1.52.0", + "@raycast/api": "1.79.1", "@types/content-type": "^1.1.6", "@types/object-hash": "^3.0.4", "@types/signal-exit": "^3.0.2", diff --git a/tests/package.json b/tests/package.json index a294f07..77c654d 100644 --- a/tests/package.json +++ b/tests/package.json @@ -199,6 +199,20 @@ "subtitle": "Utils Smoke Tests", "description": "Utils Smoke Tests", "mode": "view" + }, + { + "name": "create-deeplink", + "title": "createDeeplink", + "subtitle": "Utils Smoke Tests", + "description": "Utils Smoke Tests", + "mode": "view", + "arguments": [ + { + "name": "text", + "placeholder": "Text", + "type": "text" + } + ] } ], "dependencies": { diff --git a/tests/src/ai.tsx b/tests/src/ai.tsx index 521c7ed..003d332 100644 --- a/tests/src/ai.tsx +++ b/tests/src/ai.tsx @@ -28,7 +28,7 @@ export default function Command(props: LaunchProps<{ arguments: Arguments.Ai }>) handleSetCreativity(0)} /> handleSetCreativity(2)} /> handleSetCreativity(3)} /> - handleSetCreativity(undefined)} /> + handleSetCreativity(undefined)} /> } diff --git a/tests/src/create-deeplink.tsx b/tests/src/create-deeplink.tsx new file mode 100644 index 0000000..0d127f8 --- /dev/null +++ b/tests/src/create-deeplink.tsx @@ -0,0 +1,113 @@ +import { Action, ActionPanel, LaunchProps, List } from "@raycast/api"; +import { createDeeplink, DeeplinkType } from "@raycast/utils"; + +export default function Command( + props: LaunchProps<{ launchContext: { message: string }; arguments: Arguments.CreateDeeplink }>, +) { + return ( + + {props.launchContext?.message ? ( + + + + ) : null} + + {props.arguments.text ? ( + + + + ) : null} + + + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + + + } + /> + + + ); +}