diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c5e8e6..89adbaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.16 + +- Add `Add Swift Support` command + ## 0.15 - `Name` is now checked for non allowed characters for Command, Preference and Argument diff --git a/README.md b/README.md index 98cbf56..e924306 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ and more - ✅ Raycast Tree-view for easy navigation - ✅ Auto Completion for script directives - ✅ Open the Extension Issues Dashboard +- ✅ Add Swift support with one command ## Requirements diff --git a/package-lock.json b/package-lock.json index 6ae8838..8f6b8b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "raycast", - "version": "0.15.0", + "version": "0.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "raycast", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "dependencies": { "edit-json-file": "^1.7.0", "json-to-ast": "^2.1.0", - "node-fetch": "^3.3.2" + "node-fetch": "^3.3.2", + "which": "^4.0.0" }, "devDependencies": { "@types/edit-json-file": "^1.7.0", @@ -21,6 +22,7 @@ "@types/node": "14.x", "@types/semver": "^7.3.13", "@types/vscode": "^1.67.0", + "@types/which": "^3.0.3", "@typescript-eslint/eslint-plugin": "^5.21.0", "@typescript-eslint/parser": "^5.21.0", "@vscode/test-cli": "^0.0.4", @@ -398,6 +400,12 @@ "integrity": "sha512-GH8BDf8cw9AC9080uneJfulhSa7KHSMI2s/CyKePXoGNos9J486w2V4YKoeNUqIEkW4hKoEAWp6/cXTwyGj47g==", "dev": true }, + "node_modules/@types/which": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz", + "integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.25.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz", @@ -1467,6 +1475,27 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", @@ -2487,10 +2516,12 @@ "dev": true }, "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } }, "node_modules/isobject": { "version": "3.0.1", @@ -2872,6 +2903,12 @@ "node": "*" } }, + "node_modules/mocha/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/mocha/node_modules/minimatch": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", @@ -2905,6 +2942,21 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mocha/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4098,18 +4150,17 @@ } }, "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { - "node-which": "bin/node-which" + "node-which": "bin/which.js" }, "engines": { - "node": ">= 8" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/wildcard": { @@ -4529,6 +4580,12 @@ "integrity": "sha512-GH8BDf8cw9AC9080uneJfulhSa7KHSMI2s/CyKePXoGNos9J486w2V4YKoeNUqIEkW4hKoEAWp6/cXTwyGj47g==", "dev": true }, + "@types/which": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz", + "integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.25.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.25.0.tgz", @@ -5317,6 +5374,23 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "data-uri-to-buffer": { @@ -6070,10 +6144,9 @@ "dev": true }, "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" }, "isobject": { "version": "3.0.1", @@ -6362,6 +6435,12 @@ } } }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "minimatch": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", @@ -6385,6 +6464,15 @@ "requires": { "has-flag": "^4.0.0" } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } }, @@ -7207,12 +7295,11 @@ "dev": true }, "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "requires": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" } }, "wildcard": { diff --git a/package.json b/package.json index b245c77..b531c45 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "raycast", "displayName": "Raycast", "description": "", - "version": "0.15.0", + "version": "0.16.0", "engines": { "vscode": "^1.67.0" }, @@ -33,6 +33,7 @@ "onCommand:raycast.goto.command.interval", "onCommand:raycast.updateinternalstate", "onCommand:raycast.opencommand", + "onCommand:raycast.addswiftsupport", "onView:raycast", "workspaceContains:package.json" ], @@ -128,6 +129,11 @@ "title": "Publish", "category": "Raycast" }, + { + "command": "raycast.addswiftsupport", + "title": "Add Swift Support", + "category": "Raycast" + }, { "command": "raycast.attachdebugger", "title": "Attach Debugger", @@ -233,6 +239,10 @@ "command": "raycast.publish", "when": "raycast.workspaceEnabled" }, + { + "command": "raycast.addswiftsupport", + "when": "raycast.workspaceEnabled" + }, { "command": "raycast.attachdebugger", "when": "raycast.workspaceEnabled" @@ -406,6 +416,7 @@ "@typescript-eslint/parser": "^5.21.0", "@vscode/test-cli": "^0.0.4", "@vscode/test-electron": "^2.3.9", + "@types/which": "^3.0.3", "eslint": "^8.14.0", "glob": "^8.0.1", "mocha": "^9.2.2", @@ -428,6 +439,7 @@ "dependencies": { "edit-json-file": "^1.7.0", "json-to-ast": "^2.1.0", - "node-fetch": "^3.3.2" + "node-fetch": "^3.3.2", + "which": "^4.0.0" } } diff --git a/src/cmds.ts b/src/cmds.ts index e599695..c7cbdc1 100644 --- a/src/cmds.ts +++ b/src/cmds.ts @@ -24,6 +24,7 @@ import { addCommandArgumentCmd } from "./commands/addCommandArgument"; import { updateInternalState } from "./commands/updateInternalState"; import { gotoCommandDisabledByDefaultManifestLocationCmd } from "./commands/goto/disabledByDefault"; import { extensionIssuesCmd } from "./commands/extensionIssues"; +import { addSwiftSupportCmd } from "./commands/swiftSupport"; export function registerAllCommands(manager: ExtensionManager) { manager.registerCommand("lint", async () => lintCmd(manager)); @@ -61,4 +62,5 @@ export function registerAllCommands(manager: ExtensionManager) { gotoCommandArgumentManifestLocationCmd(manager, args), ); manager.registerCommand("command.arguments.add", async (...args: any[]) => addCommandArgumentCmd(manager, args)); + manager.registerCommand("addswiftsupport", async () => addSwiftSupportCmd(manager)); } diff --git a/src/commands/swiftSupport.ts b/src/commands/swiftSupport.ts new file mode 100644 index 0000000..84f580f --- /dev/null +++ b/src/commands/swiftSupport.ts @@ -0,0 +1,154 @@ +import * as afs from "fs/promises"; +import { ExtensionManager } from "../manager"; +import * as vscode from "vscode"; +import { fileExists, showTextDocumentAtPosition } from "../utils"; +import path = require("path"); +import { readManifestFile } from "../manifest"; +import which = require("which"); + +const raycastSwiftUrl = "https://github.com/raycast/extensions-swift-tools"; + +async function isXcodeInstalled() { + const resolved = await which("xcode-select", { nothrow: true }); + return resolved ? true : false; +} + +async function openXcodeInAppStore() { + await vscode.env.openExternal(vscode.Uri.parse("https://apps.apple.com/app/xcode/id497799835")); +} + +function commentify(lines: string[]) { + return lines.map((c) => `//${c.length > 0 ? " " : ""}${c}`).join("\n"); +} + +async function addSwiftSupport(manager: ExtensionManager, rootFolder: string) { + const manifest = await readManifestFile(manager.getActiveWorkspacePackageFilename()); + const packageName = manifest?.name; + if (!packageName) { + throw new Error("Could not get name from manifest"); + } + const gitignoreFilename = path.join(rootFolder, ".gitignore"); + const gitignore = [".DS_Store", ".build/", ".swiftpm/", ".vscode/", "Package.resolved"]; + const sourcesFolder = path.join(rootFolder, "Sources"); + await afs.mkdir(sourcesFolder, { recursive: true }); + manager.logger.debug(`Write ${gitignoreFilename}`); + await afs.writeFile(gitignoreFilename, gitignore.join("\n")); + const swiftPackageFilename = path.join(rootFolder, "Package.swift"); + const swiftPackage = `// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "${packageName}", + platforms: [.macOS(.v12)], + dependencies: [ + .package(url: "https://github.com/raycast/extensions-swift-tools.git", from: "1.0.1"), + ], + targets: [ + .executableTarget( + name: "${packageName}", + dependencies: [ + .product(name: "RaycastSwiftMacros", package: "extensions-swift-tools"), + .product(name: "RaycastSwiftPlugin", package: "extensions-swift-tools"), + .product(name: "RaycastTypeScriptPlugin", package: "extensions-swift-tools"), + ] + ), + ] +)`; + manager.logger.debug(`Write ${swiftPackageFilename}`); + await afs.writeFile(swiftPackageFilename, swiftPackage); + + const example = [ + "", + "# How to Import from TypeScript file", + "", + "Example TypeScript file src/mycommand.tsx :", + "", + 'import { hello, helloName } from "swift:../swift" // relative path to the swift directory from the workspace root', + "", + "async function exampleFunction() {", + " await hello();", + ' await helloName("Michael");', + "}", + ]; + + const warning = [ + "Warning: You shouldn't have a main.swift file in your project nor a structure marked with @main.", + " These are reserved for the Swift-to-TypeScript plugins.", + ]; + + const generalInstructions = [ + "", + "# How to make a Swift function available in TypeScript", + "", + "Write global Swift functions and mark them with the @raycast attribute.", + "Global functions marked with @raycast are exported to TypeScript.", + "These functions can have any number of parameters, and one or no return type.", + "Exported functions can also be asynchronous (async) or throw errors (throws).", + "", + "The only restrictions are:", + "", + "- Parameters must conform to Decodable", + "- The return type (if any) must conform to Encodable (or be Void or ()).", + "- Variadic parameters and parameter packs are not supported.", + "- Only global functions will be exported. Methods or functions within structs, classes, or enums won't be exported.", + "", + `For more details check out the official repository ${raycastSwiftUrl}`, + ]; + + const source = `import Foundation +import RaycastSwiftMacros + +@raycast func hello() -> String { + "Hello from Swift" +} + +@raycast func helloName(name:String) -> String{ + "Hello \(name)" +} + +${commentify(example)} + +${commentify(warning)} + +${commentify(generalInstructions)} +`; + const swiftCodeFilename = path.join(sourcesFolder, `${packageName}.swift`); + manager.logger.debug(`Write ${swiftCodeFilename}`); + await afs.writeFile(swiftCodeFilename, source); + return swiftCodeFilename; +} + +export async function addSwiftSupportCmd(manager: ExtensionManager) { + manager.logger.debug("Add Swift support"); + const ws = manager.getActiveWorkspace(); + if (!ws) { + throw new Error("No active workspace"); + } + const swiftRootFolder = path.join(ws.uri.fsPath, "swift"); + if (await fileExists(path.join(swiftRootFolder, "Package.swift"))) { + throw new Error("Swift Support already exists"); + } + const swiftFilename = await addSwiftSupport(manager, swiftRootFolder); + showTextDocumentAtPosition(vscode.Uri.file(swiftFilename)); + vscode.window.showInformationMessage("Swift Support added successfully", ...["More Info"]).then((selected) => { + if (selected === "More Info") { + vscode.env.openExternal(vscode.Uri.parse(raycastSwiftUrl)); + } + }); + const xcodeInstalled = await isXcodeInstalled(); + if (!xcodeInstalled) { + vscode.window + .showWarningMessage( + "You need to install Xcode because it is required for Swift for Raycast", + ...["More Info", "AppStore"], + ) + .then((selected) => { + if (selected === "More Info") { + vscode.env.openExternal(vscode.Uri.parse(raycastSwiftUrl + "#requirements")); + } else if (selected === "AppStore") { + openXcodeInAppStore(); + } + }); + } +}