@public-tauri/raycast-convert 1.0.1
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/LICENSE.md +7 -0
- package/README.md +104 -0
- package/dist/cli.mjs +50 -0
- package/dist/index.mjs +2 -0
- package/dist/src-DPoXoCnp.mjs +414 -0
- package/package.json +38 -0
- package/src/build.ts +46 -0
- package/src/cli.ts +63 -0
- package/src/commands.ts +44 -0
- package/src/files.ts +24 -0
- package/src/generate/public-main.ts +22 -0
- package/src/generate/server-module.ts +56 -0
- package/src/generate/tsdown-config.ts +44 -0
- package/src/icons.ts +12 -0
- package/src/index.ts +81 -0
- package/src/options.ts +23 -0
- package/src/package-json.ts +50 -0
- package/src/preferences.ts +62 -0
- package/src/types.ts +73 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2022 qwertyyb
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# @public-tauri/raycast-convert
|
|
2
|
+
|
|
3
|
+
将 Raycast 插件转换为 Public Tauri 插件。
|
|
4
|
+
|
|
5
|
+
当前阶段暂时只支持 Raycast `no-view` 命令;Raycast view 插件的适配正在进行中。
|
|
6
|
+
|
|
7
|
+
仓库根目录提供了以下脚本:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm raycast:convert <raycast-plugin-dir> --out <public-plugin-dir> [--build]
|
|
11
|
+
pnpm raycast:convert:production <raycast-plugin-dir> --out <public-plugin-dir> [--build]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
该包也提供 CLI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
raycast-convert <raycast-plugin-dir> --out <public-plugin-dir> [--build] [--mode development|production]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 功能
|
|
21
|
+
|
|
22
|
+
转换器读取一个 Raycast 插件目录,并生成一个 Public Tauri 插件目录。
|
|
23
|
+
|
|
24
|
+
生成的插件包含:
|
|
25
|
+
|
|
26
|
+
- `package.json`,包含 `publicPlugin` manifest。
|
|
27
|
+
- `.raycast-build/public-main.ts`,浏览器侧桥接入口。
|
|
28
|
+
- `.raycast-build/server.ts`,Node 侧命令运行入口。
|
|
29
|
+
- `tsdown.config.ts`,用于打包转换后的插件。
|
|
30
|
+
- `raycast-conversion-report.json`,记录已转换命令、跳过命令和 warnings。
|
|
31
|
+
- `assets/`,当 Raycast 插件存在资源目录时会复制过来。
|
|
32
|
+
|
|
33
|
+
如果传入 `--build`,转换器会在生成的 Public Tauri 插件目录中安装依赖,并执行 `tsdown` 构建。
|
|
34
|
+
|
|
35
|
+
## 转换流程
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
Raycast 插件源码
|
|
39
|
+
-> 生成 Public Tauri 插件文件
|
|
40
|
+
-> 写入 package.json 和 tsdown.config.ts
|
|
41
|
+
-> 在生成的插件目录执行 pnpm install
|
|
42
|
+
-> pnpm exec tsdown --config tsdown.config.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
原始 Raycast 插件源码目录不会被安装依赖,也不会被修改。
|
|
46
|
+
|
|
47
|
+
## 模式
|
|
48
|
+
|
|
49
|
+
### development
|
|
50
|
+
|
|
51
|
+
默认模式。
|
|
52
|
+
|
|
53
|
+
用于在当前 monorepo 内开发和调试。生成的 `package.json` 会让 `@public-tauri/api` 指向本地开发代码:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@public-tauri/api": "file:/path/to/public-tauri/packages/api"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### production
|
|
64
|
+
|
|
65
|
+
用于生产环境或打包应用内转换。生成的插件依赖已发布的 API 包:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@public-tauri/api": "latest"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Raycast API 映射
|
|
76
|
+
|
|
77
|
+
生成的 `tsdown.config.ts` 会将 Raycast API 映射到 Public Tauri 的兼容层:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
alias: {
|
|
81
|
+
'@raycast/api': '@public-tauri/api/raycast',
|
|
82
|
+
'@raycast/utils': '@public-tauri/api/raycast/utils',
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
原 Raycast 插件中的其它依赖会保留。
|
|
87
|
+
|
|
88
|
+
## 当前支持范围
|
|
89
|
+
|
|
90
|
+
目前暂时只转换以下类型的 Raycast 命令:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mode": "no-view"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
其它 command mode 会被跳过,并写入 `raycast-conversion-report.json`。这不是该包的长期边界,Raycast view 插件的适配正在进行中。
|
|
99
|
+
|
|
100
|
+
## 限制
|
|
101
|
+
|
|
102
|
+
- 暂时不转换 Raycast React UI 命令。
|
|
103
|
+
- 命令入口文件需要符合常见 Raycast 布局,例如 `src/<command>.ts`、`src/<command>.tsx` 或 `src/<command>/index.ts`。
|
|
104
|
+
- 运行时能力取决于 `@public-tauri/api/raycast` 提供的兼容层。
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as convertRaycastPlugin } from "./src-DPoXoCnp.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
//#region src/cli.ts
|
|
6
|
+
const usage = () => {
|
|
7
|
+
console.error("Usage: raycast-convert <raycast-plugin-dir> --out <public-plugin-dir> [--build] [--mode development|production]");
|
|
8
|
+
};
|
|
9
|
+
const parseCliArgs = () => parseArgs({
|
|
10
|
+
args: process.argv.slice(2),
|
|
11
|
+
allowPositionals: true,
|
|
12
|
+
options: {
|
|
13
|
+
out: { type: "string" },
|
|
14
|
+
build: {
|
|
15
|
+
type: "boolean",
|
|
16
|
+
default: false
|
|
17
|
+
},
|
|
18
|
+
mode: {
|
|
19
|
+
type: "string",
|
|
20
|
+
default: "development"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const getConvertMode = (value) => {
|
|
25
|
+
if (value !== "development" && value !== "production") throw new Error(`Unsupported --mode "${value}". Expected "development" or "production".`);
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
const runCli = async () => {
|
|
29
|
+
const { values, positionals } = parseCliArgs();
|
|
30
|
+
const inputArg = positionals[0];
|
|
31
|
+
if (!inputArg) {
|
|
32
|
+
usage();
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const report = await convertRaycastPlugin({
|
|
36
|
+
inputDir: path.resolve(inputArg),
|
|
37
|
+
outputDir: values.out,
|
|
38
|
+
build: values.build,
|
|
39
|
+
mode: getConvertMode(values.mode),
|
|
40
|
+
invocationDir: process.cwd()
|
|
41
|
+
});
|
|
42
|
+
console.log(`Converted ${report.convertedCommands.length} command(s) to ${report.output}`);
|
|
43
|
+
if (report.skippedCommands.length) console.log(`Skipped ${report.skippedCommands.length} command(s). See raycast-conversion-report.json`);
|
|
44
|
+
};
|
|
45
|
+
if (import.meta.url === `file://${process.argv[1]}`) runCli().catch((error) => {
|
|
46
|
+
console.error(error instanceof Error ? error.message : error);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
//#endregion
|
|
50
|
+
export { runCli };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
//#region src/build.ts
|
|
5
|
+
const installCommand = "pnpm";
|
|
6
|
+
const installArgs = [
|
|
7
|
+
"install",
|
|
8
|
+
"--ignore-scripts",
|
|
9
|
+
"--frozen-lockfile=false"
|
|
10
|
+
];
|
|
11
|
+
const runInstall = (cwd, label) => {
|
|
12
|
+
console.log(`Installing ${label} dependencies...`);
|
|
13
|
+
const result = spawnSync(installCommand, installArgs, {
|
|
14
|
+
cwd,
|
|
15
|
+
stdio: "inherit"
|
|
16
|
+
});
|
|
17
|
+
if (result.status !== 0) throw new Error(`${label} dependency install failed with exit code ${result.status}`);
|
|
18
|
+
};
|
|
19
|
+
const installOutputDependencies = (options) => {
|
|
20
|
+
runInstall(options.outputDir, "converted plugin");
|
|
21
|
+
};
|
|
22
|
+
const getBuildCommand = (options) => {
|
|
23
|
+
return {
|
|
24
|
+
command: "pnpm",
|
|
25
|
+
args: [
|
|
26
|
+
"exec",
|
|
27
|
+
"tsdown",
|
|
28
|
+
"--config",
|
|
29
|
+
path.join(options.outputDir, "tsdown.config.ts")
|
|
30
|
+
],
|
|
31
|
+
cwd: options.outputDir
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const buildConvertedPlugin = (options) => {
|
|
35
|
+
const { command, args: buildArgs, cwd } = getBuildCommand(options);
|
|
36
|
+
const result = spawnSync(command, buildArgs, {
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: "inherit"
|
|
39
|
+
});
|
|
40
|
+
if (result.status !== 0) throw new Error(`Build failed with exit code ${result.status}`);
|
|
41
|
+
};
|
|
42
|
+
const installAndBuild = (options) => {
|
|
43
|
+
installOutputDependencies(options);
|
|
44
|
+
buildConvertedPlugin(options);
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/files.ts
|
|
48
|
+
const readJson = async (filePath) => JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
49
|
+
const writeJson = async (filePath, value) => {
|
|
50
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
51
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
52
|
+
};
|
|
53
|
+
const exists = async (filePath) => {
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(filePath);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const copyAssetsDir = async (inputDir, assetsDir) => {
|
|
62
|
+
const source = path.join(inputDir, "assets");
|
|
63
|
+
if (!await exists(source)) return;
|
|
64
|
+
await fs.cp(source, assetsDir, { recursive: true });
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/commands.ts
|
|
68
|
+
const findCommandEntry = async (inputDir, command) => {
|
|
69
|
+
const candidates = [
|
|
70
|
+
path.join(inputDir, "src", `${command.name}.tsx`),
|
|
71
|
+
path.join(inputDir, "src", `${command.name}.ts`),
|
|
72
|
+
path.join(inputDir, "src", `${command.name}.jsx`),
|
|
73
|
+
path.join(inputDir, "src", `${command.name}.js`),
|
|
74
|
+
path.join(inputDir, "src", command.name, "index.tsx"),
|
|
75
|
+
path.join(inputDir, "src", command.name, "index.ts"),
|
|
76
|
+
path.join(inputDir, "src", command.name, "index.jsx"),
|
|
77
|
+
path.join(inputDir, "src", command.name, "index.js")
|
|
78
|
+
];
|
|
79
|
+
for (const candidate of candidates) if (await exists(candidate)) return candidate;
|
|
80
|
+
return null;
|
|
81
|
+
};
|
|
82
|
+
const resolveNoViewCommands = async (inputDir, sourceCommands) => {
|
|
83
|
+
const convertedCommands = [];
|
|
84
|
+
const skippedCommands = [];
|
|
85
|
+
for (const command of sourceCommands) {
|
|
86
|
+
if (command.mode !== "no-view") {
|
|
87
|
+
skippedCommands.push({
|
|
88
|
+
name: command.name,
|
|
89
|
+
reason: `Unsupported mode: ${command.mode || "<empty>"}`
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const entry = await findCommandEntry(inputDir, command);
|
|
94
|
+
if (!entry) {
|
|
95
|
+
skippedCommands.push({
|
|
96
|
+
name: command.name,
|
|
97
|
+
reason: "Command entry not found under src/"
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
convertedCommands.push({
|
|
102
|
+
...command,
|
|
103
|
+
entry
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (!convertedCommands.length) throw new Error("No Raycast no-view commands were converted");
|
|
107
|
+
return {
|
|
108
|
+
convertedCommands,
|
|
109
|
+
skippedCommands
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/generate/public-main.ts
|
|
114
|
+
const generatePublicMain = () => `export default function createPlugin() {
|
|
115
|
+
const getApi = () => window.$wujie?.props;
|
|
116
|
+
return {
|
|
117
|
+
async onAction(command, action, query, options) {
|
|
118
|
+
const api = getApi();
|
|
119
|
+
try {
|
|
120
|
+
await api.channel.invoke('raycast:run', {
|
|
121
|
+
commandName: command.name,
|
|
122
|
+
query,
|
|
123
|
+
action,
|
|
124
|
+
options,
|
|
125
|
+
preferences: api.getPreferences?.() || {},
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
await api.dialog.showToast(message);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/generate/server-module.ts
|
|
138
|
+
const toImportName = (index) => `command${index}`;
|
|
139
|
+
const generateServerModule = (commands, packageName, publicCommands) => {
|
|
140
|
+
return `import path from 'node:path';
|
|
141
|
+
import { fileURLToPath } from 'node:url';
|
|
142
|
+
import { channel } from '@public-tauri/api/node';
|
|
143
|
+
import { __setRaycastContext } from '@public-tauri/api/raycast';
|
|
144
|
+
|
|
145
|
+
${commands.map((command, index) => `import * as ${toImportName(index)} from ${JSON.stringify(command.entry)};`).join("\n")}
|
|
146
|
+
|
|
147
|
+
const commandModules: Record<string, any> = {
|
|
148
|
+
${commands.map((command, index) => ` ${JSON.stringify(command.name)}: ${toImportName(index)},`).join("\n")}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const distDir = path.dirname(fileURLToPath(import.meta.url));
|
|
152
|
+
const pluginRoot = path.dirname(distDir);
|
|
153
|
+
const commandManifests = ${JSON.stringify(publicCommands, null, 2)};
|
|
154
|
+
|
|
155
|
+
channel.handle('raycast:run', async (payload = {}) => {
|
|
156
|
+
const commandName = String(payload.commandName || '');
|
|
157
|
+
const commandModule = commandModules[commandName];
|
|
158
|
+
if (!commandModule) {
|
|
159
|
+
throw new Error(\`Unknown Raycast command: \${commandName}\`);
|
|
160
|
+
}
|
|
161
|
+
const run = commandModule.default;
|
|
162
|
+
if (typeof run !== 'function') {
|
|
163
|
+
throw new Error(\`Raycast command \${commandName} has no default function export\`);
|
|
164
|
+
}
|
|
165
|
+
const launchPayload = payload.options?.payload || {};
|
|
166
|
+
__setRaycastContext({
|
|
167
|
+
pluginName: ${JSON.stringify(packageName)},
|
|
168
|
+
commandName,
|
|
169
|
+
commands: commandManifests,
|
|
170
|
+
launchType: launchPayload.launchType,
|
|
171
|
+
preferences: payload.preferences || {},
|
|
172
|
+
supportPath: path.join(pluginRoot, '.raycast-compat'),
|
|
173
|
+
assetsPath: path.join(pluginRoot, 'assets'),
|
|
174
|
+
});
|
|
175
|
+
return await run({
|
|
176
|
+
arguments: launchPayload.arguments || {},
|
|
177
|
+
fallbackText: launchPayload.fallbackText ?? payload.query ?? '',
|
|
178
|
+
launchContext: launchPayload.context ?? payload,
|
|
179
|
+
launchType: launchPayload.launchType || 'userInitiated',
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
`;
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/generate/tsdown-config.ts
|
|
186
|
+
const formatAlias = (aliases) => Object.entries(aliases).map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`).join("\n");
|
|
187
|
+
const formatAliasProperty = (aliases) => {
|
|
188
|
+
const entries = formatAlias(aliases);
|
|
189
|
+
return entries ? ` alias: {\n${entries}\n },` : " alias: {},";
|
|
190
|
+
};
|
|
191
|
+
const getServerAliases = () => ({
|
|
192
|
+
"@raycast/api": "@public-tauri/api/raycast",
|
|
193
|
+
"@raycast/utils": "@public-tauri/api/raycast/utils"
|
|
194
|
+
});
|
|
195
|
+
const generateTsdownConfig = (options) => `export default [
|
|
196
|
+
{
|
|
197
|
+
entry: ${JSON.stringify(path.join(options.buildDir, "public-main.ts"))},
|
|
198
|
+
format: 'esm',
|
|
199
|
+
platform: 'browser',
|
|
200
|
+
target: 'es2022',
|
|
201
|
+
outDir: ${JSON.stringify(options.distDir)},
|
|
202
|
+
outExtensions: () => ({ js: '.js' }),
|
|
203
|
+
deps: {
|
|
204
|
+
alwaysBundle: () => true,
|
|
205
|
+
},
|
|
206
|
+
${formatAliasProperty({})}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
entry: ${JSON.stringify(path.join(options.buildDir, "server.ts"))},
|
|
210
|
+
format: 'esm',
|
|
211
|
+
platform: 'node',
|
|
212
|
+
target: 'es2022',
|
|
213
|
+
outDir: ${JSON.stringify(options.distDir)},
|
|
214
|
+
outExtensions: () => ({ js: '.js' }),
|
|
215
|
+
deps: {
|
|
216
|
+
alwaysBundle: () => true,
|
|
217
|
+
},
|
|
218
|
+
${formatAliasProperty(getServerAliases())}
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
`;
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/icons.ts
|
|
224
|
+
const isUrlLike = (value) => /^(?:https?:|data:|asset:|public-icon:)/.test(value);
|
|
225
|
+
const hasPathSegment = (value) => value.includes("/") || value.includes("\\");
|
|
226
|
+
const normalizeRaycastIcon = (icon) => {
|
|
227
|
+
if (!icon) return void 0;
|
|
228
|
+
if (isUrlLike(icon)) return icon;
|
|
229
|
+
if (icon.startsWith("./") || icon.startsWith("../") || icon.startsWith("/")) return icon;
|
|
230
|
+
if (hasPathSegment(icon)) return `./${icon}`;
|
|
231
|
+
return `./assets/${icon}`;
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/options.ts
|
|
235
|
+
const resolveMode = (mode) => mode || "development";
|
|
236
|
+
const resolveConvertOptions = (options) => {
|
|
237
|
+
const inputDir = path.resolve(options.inputDir);
|
|
238
|
+
const outputDir = path.resolve(options.outputDir || `${inputDir}-public`);
|
|
239
|
+
const invocationDir = path.resolve(options.invocationDir || process.cwd());
|
|
240
|
+
const mode = resolveMode(options.mode);
|
|
241
|
+
return {
|
|
242
|
+
inputDir,
|
|
243
|
+
outputDir,
|
|
244
|
+
build: Boolean(options.build),
|
|
245
|
+
mode,
|
|
246
|
+
invocationDir,
|
|
247
|
+
publicApiDependency: mode === "development" ? `file:${path.join(invocationDir, "packages", "api")}` : "latest",
|
|
248
|
+
buildDir: path.join(outputDir, ".raycast-build"),
|
|
249
|
+
distDir: path.join(outputDir, "dist"),
|
|
250
|
+
assetsDir: path.join(outputDir, "assets")
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/package-json.ts
|
|
255
|
+
const raycastApiPackages = new Set(["@raycast/api", "@raycast/utils"]);
|
|
256
|
+
const rewriteDependencyMap = (dependencies) => {
|
|
257
|
+
const rewritten = { ...dependencies || {} };
|
|
258
|
+
let replacedRaycastApi = false;
|
|
259
|
+
for (const packageName of raycastApiPackages) if (packageName in rewritten) {
|
|
260
|
+
delete rewritten[packageName];
|
|
261
|
+
replacedRaycastApi = true;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
dependencies: rewritten,
|
|
265
|
+
replacedRaycastApi
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
const createConvertedPackage = (sourcePackage, publicPlugin, options) => {
|
|
269
|
+
const dependenciesResult = rewriteDependencyMap(sourcePackage.dependencies);
|
|
270
|
+
const devDependenciesResult = rewriteDependencyMap(sourcePackage.devDependencies);
|
|
271
|
+
if (dependenciesResult.replacedRaycastApi || devDependenciesResult.replacedRaycastApi) options.warnings.push({
|
|
272
|
+
type: "dependency",
|
|
273
|
+
message: "Replaced @raycast/api and/or @raycast/utils with @public-tauri/api"
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
...sourcePackage,
|
|
277
|
+
version: sourcePackage.version || "1.0.0",
|
|
278
|
+
type: "module",
|
|
279
|
+
private: true,
|
|
280
|
+
publicPlugin,
|
|
281
|
+
scripts: {
|
|
282
|
+
...sourcePackage.scripts || {},
|
|
283
|
+
build: "tsdown --config tsdown.config.ts"
|
|
284
|
+
},
|
|
285
|
+
dependencies: {
|
|
286
|
+
...dependenciesResult.dependencies,
|
|
287
|
+
"@public-tauri/api": dependenciesResult.dependencies["@public-tauri/api"] || options.publicApiDependency
|
|
288
|
+
},
|
|
289
|
+
devDependencies: {
|
|
290
|
+
...devDependenciesResult.dependencies,
|
|
291
|
+
tsdown: devDependenciesResult.dependencies.tsdown || "^0.21.7"
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/preferences.ts
|
|
297
|
+
const mapPreferenceType = (type, warnings) => {
|
|
298
|
+
switch (type) {
|
|
299
|
+
case "password": return "password";
|
|
300
|
+
case "textarea": return "textarea";
|
|
301
|
+
case "dropdown": return "select";
|
|
302
|
+
case "checkbox": return "select";
|
|
303
|
+
case "textfield":
|
|
304
|
+
case "appPicker":
|
|
305
|
+
case void 0: return "text";
|
|
306
|
+
default:
|
|
307
|
+
warnings.push({
|
|
308
|
+
type: "preference",
|
|
309
|
+
message: `Unsupported preference type "${type}", converted to text`
|
|
310
|
+
});
|
|
311
|
+
return "text";
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const convertPreference = (preference, warnings) => {
|
|
315
|
+
const type = mapPreferenceType(preference.type, warnings);
|
|
316
|
+
const options = preference.type === "checkbox" ? [{
|
|
317
|
+
label: "Yes",
|
|
318
|
+
value: true
|
|
319
|
+
}, {
|
|
320
|
+
label: "No",
|
|
321
|
+
value: false
|
|
322
|
+
}] : preference.data?.map((item) => ({
|
|
323
|
+
label: item.title || item.label || String(item.value),
|
|
324
|
+
value: item.value
|
|
325
|
+
}));
|
|
326
|
+
return {
|
|
327
|
+
name: preference.name,
|
|
328
|
+
title: preference.title || preference.label || preference.name,
|
|
329
|
+
description: preference.description,
|
|
330
|
+
type,
|
|
331
|
+
required: Boolean(preference.required),
|
|
332
|
+
placeholder: preference.placeholder,
|
|
333
|
+
defaultValue: preference.defaultValue ?? preference.default,
|
|
334
|
+
...options?.length ? { options } : {}
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
const mergePreferences = (pluginPreferences, commandPreferences, warnings) => {
|
|
338
|
+
const preferenceNames = /* @__PURE__ */ new Set();
|
|
339
|
+
return [...pluginPreferences, ...commandPreferences].map((preference) => convertPreference(preference, warnings)).filter((preference) => {
|
|
340
|
+
if (preferenceNames.has(preference.name)) {
|
|
341
|
+
warnings.push({
|
|
342
|
+
type: "preference",
|
|
343
|
+
message: `Duplicate preference "${preference.name}" was skipped`
|
|
344
|
+
});
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
preferenceNames.add(preference.name);
|
|
348
|
+
return true;
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/index.ts
|
|
353
|
+
const createPublicCommands = (commands, icon) => commands.map((command) => ({
|
|
354
|
+
name: command.name,
|
|
355
|
+
title: command.title || command.name,
|
|
356
|
+
subtitle: command.subtitle || command.description,
|
|
357
|
+
description: command.description,
|
|
358
|
+
icon: normalizeRaycastIcon(command.icon) || icon,
|
|
359
|
+
mode: "none",
|
|
360
|
+
matches: [{
|
|
361
|
+
type: "text",
|
|
362
|
+
keywords: command.keywords?.length ? command.keywords : [command.title || command.name]
|
|
363
|
+
}]
|
|
364
|
+
}));
|
|
365
|
+
const convertRaycastPlugin = async (rawOptions) => {
|
|
366
|
+
const options = resolveConvertOptions(rawOptions);
|
|
367
|
+
const warnings = [];
|
|
368
|
+
const sourcePackage = await readJson(path.join(options.inputDir, "package.json"));
|
|
369
|
+
const sourceCommands = sourcePackage.commands || [];
|
|
370
|
+
const { convertedCommands, skippedCommands } = await resolveNoViewCommands(options.inputDir, sourceCommands);
|
|
371
|
+
await fs.rm(options.outputDir, {
|
|
372
|
+
recursive: true,
|
|
373
|
+
force: true
|
|
374
|
+
});
|
|
375
|
+
await fs.mkdir(options.buildDir, { recursive: true });
|
|
376
|
+
await fs.mkdir(options.distDir, { recursive: true });
|
|
377
|
+
await copyAssetsDir(options.inputDir, options.assetsDir);
|
|
378
|
+
const icon = normalizeRaycastIcon(sourcePackage.icon || convertedCommands[0]?.icon) || "extension";
|
|
379
|
+
const commandPreferences = convertedCommands.flatMap((command) => command.preferences || []);
|
|
380
|
+
const preferences = mergePreferences(sourcePackage.preferences || [], commandPreferences, warnings);
|
|
381
|
+
const publicCommands = createPublicCommands(convertedCommands, icon);
|
|
382
|
+
const publicPlugin = {
|
|
383
|
+
title: sourcePackage.title || sourcePackage.name,
|
|
384
|
+
subtitle: sourcePackage.description || sourcePackage.name,
|
|
385
|
+
description: sourcePackage.description,
|
|
386
|
+
icon,
|
|
387
|
+
main: "./dist/public-main.js",
|
|
388
|
+
server: "./dist/server.js",
|
|
389
|
+
...preferences.length ? { preferences } : {},
|
|
390
|
+
commands: publicCommands
|
|
391
|
+
};
|
|
392
|
+
await writeJson(path.join(options.outputDir, "package.json"), createConvertedPackage(sourcePackage, publicPlugin, {
|
|
393
|
+
publicApiDependency: options.publicApiDependency,
|
|
394
|
+
warnings
|
|
395
|
+
}));
|
|
396
|
+
await fs.writeFile(path.join(options.buildDir, "public-main.ts"), generatePublicMain(), "utf8");
|
|
397
|
+
await fs.writeFile(path.join(options.buildDir, "server.ts"), generateServerModule(convertedCommands, sourcePackage.name, publicCommands), "utf8");
|
|
398
|
+
await fs.writeFile(path.join(options.outputDir, "tsdown.config.ts"), generateTsdownConfig(options), "utf8");
|
|
399
|
+
const report = {
|
|
400
|
+
source: options.inputDir,
|
|
401
|
+
output: options.outputDir,
|
|
402
|
+
convertedCommands: convertedCommands.map((command) => ({
|
|
403
|
+
name: command.name,
|
|
404
|
+
entry: command.entry
|
|
405
|
+
})),
|
|
406
|
+
skippedCommands,
|
|
407
|
+
warnings
|
|
408
|
+
};
|
|
409
|
+
await writeJson(path.join(options.outputDir, "raycast-conversion-report.json"), report);
|
|
410
|
+
if (options.build) installAndBuild(options);
|
|
411
|
+
return report;
|
|
412
|
+
};
|
|
413
|
+
//#endregion
|
|
414
|
+
export { convertRaycastPlugin as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@public-tauri/raycast-convert",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Convert Raycast extensions into Public Tauri plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"package.json"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.mjs",
|
|
14
|
+
"./cli": "./dist/cli.mjs",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"raycast-convert": "./dist/cli.mjs"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"tsx": "^4.20.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.1.0",
|
|
25
|
+
"eslint": "^9.32.0",
|
|
26
|
+
"tsdown": "^0.21.7",
|
|
27
|
+
"typescript": "~5.9.2"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsdown",
|
|
35
|
+
"convert": "tsx src/cli.ts",
|
|
36
|
+
"lint": "eslint src"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ResolvedConvertOptions } from './types';
|
|
4
|
+
|
|
5
|
+
const installCommand = 'pnpm';
|
|
6
|
+
const installArgs = ['install', '--ignore-scripts', '--frozen-lockfile=false'];
|
|
7
|
+
|
|
8
|
+
const runInstall = (cwd: string, label: string) => {
|
|
9
|
+
console.log(`Installing ${label} dependencies...`);
|
|
10
|
+
const result = spawnSync(installCommand, installArgs, {
|
|
11
|
+
cwd,
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
});
|
|
14
|
+
if (result.status !== 0) {
|
|
15
|
+
throw new Error(`${label} dependency install failed with exit code ${result.status}`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const installOutputDependencies = (options: ResolvedConvertOptions) => {
|
|
20
|
+
runInstall(options.outputDir, 'converted plugin');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getBuildCommand = (options: ResolvedConvertOptions) => {
|
|
24
|
+
const configPath = path.join(options.outputDir, 'tsdown.config.ts');
|
|
25
|
+
return {
|
|
26
|
+
command: 'pnpm',
|
|
27
|
+
args: ['exec', 'tsdown', '--config', configPath],
|
|
28
|
+
cwd: options.outputDir,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const buildConvertedPlugin = (options: ResolvedConvertOptions) => {
|
|
33
|
+
const { command, args: buildArgs, cwd } = getBuildCommand(options);
|
|
34
|
+
const result = spawnSync(command, buildArgs, {
|
|
35
|
+
cwd,
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
throw new Error(`Build failed with exit code ${result.status}`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const installAndBuild = (options: ResolvedConvertOptions) => {
|
|
44
|
+
installOutputDependencies(options);
|
|
45
|
+
buildConvertedPlugin(options);
|
|
46
|
+
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { convertRaycastPlugin } from './index';
|
|
5
|
+
import type { ConvertMode } from './types';
|
|
6
|
+
|
|
7
|
+
const usage = () => {
|
|
8
|
+
console.error('Usage: raycast-convert <raycast-plugin-dir> --out <public-plugin-dir> [--build] [--mode development|production]');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const parseCliArgs = () => parseArgs({
|
|
12
|
+
args: process.argv.slice(2),
|
|
13
|
+
allowPositionals: true,
|
|
14
|
+
options: {
|
|
15
|
+
out: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
},
|
|
18
|
+
build: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
22
|
+
mode: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
default: 'development',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const getConvertMode = (value: string): ConvertMode => {
|
|
30
|
+
if (value !== 'development' && value !== 'production') {
|
|
31
|
+
throw new Error(`Unsupported --mode "${value}". Expected "development" or "production".`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const runCli = async () => {
|
|
37
|
+
const { values, positionals } = parseCliArgs();
|
|
38
|
+
const inputArg = positionals[0];
|
|
39
|
+
if (!inputArg) {
|
|
40
|
+
usage();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const report = await convertRaycastPlugin({
|
|
45
|
+
inputDir: path.resolve(inputArg),
|
|
46
|
+
outputDir: values.out,
|
|
47
|
+
build: values.build,
|
|
48
|
+
mode: getConvertMode(values.mode),
|
|
49
|
+
invocationDir: process.cwd(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
console.log(`Converted ${report.convertedCommands.length} command(s) to ${report.output}`);
|
|
53
|
+
if (report.skippedCommands.length) {
|
|
54
|
+
console.log(`Skipped ${report.skippedCommands.length} command(s). See raycast-conversion-report.json`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
59
|
+
runCli().catch((error) => {
|
|
60
|
+
console.error(error instanceof Error ? error.message : error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { exists } from './files';
|
|
3
|
+
import type { ConvertedCommand, RaycastCommand } from './types';
|
|
4
|
+
|
|
5
|
+
const findCommandEntry = async (inputDir: string, command: RaycastCommand) => {
|
|
6
|
+
const candidates = [
|
|
7
|
+
path.join(inputDir, 'src', `${command.name}.tsx`),
|
|
8
|
+
path.join(inputDir, 'src', `${command.name}.ts`),
|
|
9
|
+
path.join(inputDir, 'src', `${command.name}.jsx`),
|
|
10
|
+
path.join(inputDir, 'src', `${command.name}.js`),
|
|
11
|
+
path.join(inputDir, 'src', command.name, 'index.tsx'),
|
|
12
|
+
path.join(inputDir, 'src', command.name, 'index.ts'),
|
|
13
|
+
path.join(inputDir, 'src', command.name, 'index.jsx'),
|
|
14
|
+
path.join(inputDir, 'src', command.name, 'index.js'),
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (await exists(candidate)) return candidate;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const resolveNoViewCommands = async (inputDir: string, sourceCommands: RaycastCommand[]) => {
|
|
23
|
+
const convertedCommands: ConvertedCommand[] = [];
|
|
24
|
+
const skippedCommands: { name: string, reason: string }[] = [];
|
|
25
|
+
|
|
26
|
+
for (const command of sourceCommands) {
|
|
27
|
+
if (command.mode !== 'no-view') {
|
|
28
|
+
skippedCommands.push({ name: command.name, reason: `Unsupported mode: ${command.mode || '<empty>'}` });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const entry = await findCommandEntry(inputDir, command);
|
|
32
|
+
if (!entry) {
|
|
33
|
+
skippedCommands.push({ name: command.name, reason: 'Command entry not found under src/' });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
convertedCommands.push({ ...command, entry });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!convertedCommands.length) {
|
|
40
|
+
throw new Error('No Raycast no-view commands were converted');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { convertedCommands, skippedCommands };
|
|
44
|
+
};
|
package/src/files.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const readJson = async <T>(filePath: string): Promise<T> => JSON.parse(await fs.readFile(filePath, 'utf8')) as T;
|
|
5
|
+
|
|
6
|
+
export const writeJson = async (filePath: string, value: unknown) => {
|
|
7
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
8
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const exists = async (filePath: string) => {
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(filePath);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const copyAssetsDir = async (inputDir: string, assetsDir: string) => {
|
|
21
|
+
const source = path.join(inputDir, 'assets');
|
|
22
|
+
if (!await exists(source)) return;
|
|
23
|
+
await fs.cp(source, assetsDir, { recursive: true });
|
|
24
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const generatePublicMain = () => `export default function createPlugin() {
|
|
2
|
+
const getApi = () => window.$wujie?.props;
|
|
3
|
+
return {
|
|
4
|
+
async onAction(command, action, query, options) {
|
|
5
|
+
const api = getApi();
|
|
6
|
+
try {
|
|
7
|
+
await api.channel.invoke('raycast:run', {
|
|
8
|
+
commandName: command.name,
|
|
9
|
+
query,
|
|
10
|
+
action,
|
|
11
|
+
options,
|
|
12
|
+
preferences: api.getPreferences?.() || {},
|
|
13
|
+
});
|
|
14
|
+
} catch (error) {
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
await api.dialog.showToast(message);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ConvertedCommand } from '../types';
|
|
2
|
+
|
|
3
|
+
const toImportName = (index: number) => `command${index}`;
|
|
4
|
+
|
|
5
|
+
export const generateServerModule = (
|
|
6
|
+
commands: ConvertedCommand[],
|
|
7
|
+
packageName: string,
|
|
8
|
+
publicCommands: Record<string, unknown>[],
|
|
9
|
+
) => {
|
|
10
|
+
const imports = commands.map((command, index) => `import * as ${toImportName(index)} from ${JSON.stringify(command.entry)};`).join('\n');
|
|
11
|
+
const commandMapEntries = commands.map((command, index) => ` ${JSON.stringify(command.name)}: ${toImportName(index)},`).join('\n');
|
|
12
|
+
const commandManifests = JSON.stringify(publicCommands, null, 2);
|
|
13
|
+
return `import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { channel } from '@public-tauri/api/node';
|
|
16
|
+
import { __setRaycastContext } from '@public-tauri/api/raycast';
|
|
17
|
+
|
|
18
|
+
${imports}
|
|
19
|
+
|
|
20
|
+
const commandModules: Record<string, any> = {
|
|
21
|
+
${commandMapEntries}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const distDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const pluginRoot = path.dirname(distDir);
|
|
26
|
+
const commandManifests = ${commandManifests};
|
|
27
|
+
|
|
28
|
+
channel.handle('raycast:run', async (payload = {}) => {
|
|
29
|
+
const commandName = String(payload.commandName || '');
|
|
30
|
+
const commandModule = commandModules[commandName];
|
|
31
|
+
if (!commandModule) {
|
|
32
|
+
throw new Error(\`Unknown Raycast command: \${commandName}\`);
|
|
33
|
+
}
|
|
34
|
+
const run = commandModule.default;
|
|
35
|
+
if (typeof run !== 'function') {
|
|
36
|
+
throw new Error(\`Raycast command \${commandName} has no default function export\`);
|
|
37
|
+
}
|
|
38
|
+
const launchPayload = payload.options?.payload || {};
|
|
39
|
+
__setRaycastContext({
|
|
40
|
+
pluginName: ${JSON.stringify(packageName)},
|
|
41
|
+
commandName,
|
|
42
|
+
commands: commandManifests,
|
|
43
|
+
launchType: launchPayload.launchType,
|
|
44
|
+
preferences: payload.preferences || {},
|
|
45
|
+
supportPath: path.join(pluginRoot, '.raycast-compat'),
|
|
46
|
+
assetsPath: path.join(pluginRoot, 'assets'),
|
|
47
|
+
});
|
|
48
|
+
return await run({
|
|
49
|
+
arguments: launchPayload.arguments || {},
|
|
50
|
+
fallbackText: launchPayload.fallbackText ?? payload.query ?? '',
|
|
51
|
+
launchContext: launchPayload.context ?? payload,
|
|
52
|
+
launchType: launchPayload.launchType || 'userInitiated',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
`;
|
|
56
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { ResolvedConvertOptions } from '../types';
|
|
3
|
+
|
|
4
|
+
const formatAlias = (aliases: Record<string, string>) => Object.entries(aliases)
|
|
5
|
+
.map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`)
|
|
6
|
+
.join('\n');
|
|
7
|
+
|
|
8
|
+
const formatAliasProperty = (aliases: Record<string, string>) => {
|
|
9
|
+
const entries = formatAlias(aliases);
|
|
10
|
+
return entries ? ` alias: {\n${entries}\n },` : ' alias: {},';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getServerAliases = (): Record<string, string> => ({
|
|
14
|
+
'@raycast/api': '@public-tauri/api/raycast',
|
|
15
|
+
'@raycast/utils': '@public-tauri/api/raycast/utils',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const generateTsdownConfig = (options: ResolvedConvertOptions) => `export default [
|
|
19
|
+
{
|
|
20
|
+
entry: ${JSON.stringify(path.join(options.buildDir, 'public-main.ts'))},
|
|
21
|
+
format: 'esm',
|
|
22
|
+
platform: 'browser',
|
|
23
|
+
target: 'es2022',
|
|
24
|
+
outDir: ${JSON.stringify(options.distDir)},
|
|
25
|
+
outExtensions: () => ({ js: '.js' }),
|
|
26
|
+
deps: {
|
|
27
|
+
alwaysBundle: () => true,
|
|
28
|
+
},
|
|
29
|
+
${formatAliasProperty({})}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
entry: ${JSON.stringify(path.join(options.buildDir, 'server.ts'))},
|
|
33
|
+
format: 'esm',
|
|
34
|
+
platform: 'node',
|
|
35
|
+
target: 'es2022',
|
|
36
|
+
outDir: ${JSON.stringify(options.distDir)},
|
|
37
|
+
outExtensions: () => ({ js: '.js' }),
|
|
38
|
+
deps: {
|
|
39
|
+
alwaysBundle: () => true,
|
|
40
|
+
},
|
|
41
|
+
${formatAliasProperty(getServerAliases())}
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
`;
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const isUrlLike = (value: string) => /^(?:https?:|data:|asset:|public-icon:)/.test(value);
|
|
2
|
+
const hasPathSegment = (value: string) => value.includes('/') || value.includes('\\');
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PLUGIN_ICON = 'extension';
|
|
5
|
+
|
|
6
|
+
export const normalizeRaycastIcon = (icon: string | undefined) => {
|
|
7
|
+
if (!icon) return undefined;
|
|
8
|
+
if (isUrlLike(icon)) return icon;
|
|
9
|
+
if (icon.startsWith('./') || icon.startsWith('../') || icon.startsWith('/')) return icon;
|
|
10
|
+
if (hasPathSegment(icon)) return `./${icon}`;
|
|
11
|
+
return `./assets/${icon}`;
|
|
12
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { installAndBuild } from './build';
|
|
4
|
+
import { resolveNoViewCommands } from './commands';
|
|
5
|
+
import { copyAssetsDir, readJson, writeJson } from './files';
|
|
6
|
+
import { generatePublicMain } from './generate/public-main';
|
|
7
|
+
import { generateServerModule } from './generate/server-module';
|
|
8
|
+
import { generateTsdownConfig } from './generate/tsdown-config';
|
|
9
|
+
import { DEFAULT_PLUGIN_ICON, normalizeRaycastIcon } from './icons';
|
|
10
|
+
import { resolveConvertOptions } from './options';
|
|
11
|
+
import { createConvertedPackage } from './package-json';
|
|
12
|
+
import { mergePreferences } from './preferences';
|
|
13
|
+
import type { ConversionReport, ConvertOptions, ConvertWarning, RaycastPackage } from './types';
|
|
14
|
+
|
|
15
|
+
export type * from './types';
|
|
16
|
+
|
|
17
|
+
const createPublicCommands = (commands: { name: string, title?: string, subtitle?: string, description?: string, icon?: string, keywords?: string[] }[], icon: string) => commands.map(command => ({
|
|
18
|
+
name: command.name,
|
|
19
|
+
title: command.title || command.name,
|
|
20
|
+
subtitle: command.subtitle || command.description,
|
|
21
|
+
description: command.description,
|
|
22
|
+
icon: normalizeRaycastIcon(command.icon) || icon,
|
|
23
|
+
mode: 'none',
|
|
24
|
+
matches: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text',
|
|
27
|
+
keywords: command.keywords?.length ? command.keywords : [command.title || command.name],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
export const convertRaycastPlugin = async (rawOptions: ConvertOptions): Promise<ConversionReport> => {
|
|
33
|
+
const options = resolveConvertOptions(rawOptions);
|
|
34
|
+
const warnings: ConvertWarning[] = [];
|
|
35
|
+
const sourcePackage = await readJson<RaycastPackage>(path.join(options.inputDir, 'package.json'));
|
|
36
|
+
const sourceCommands = sourcePackage.commands || [];
|
|
37
|
+
const { convertedCommands, skippedCommands } = await resolveNoViewCommands(options.inputDir, sourceCommands);
|
|
38
|
+
|
|
39
|
+
await fs.rm(options.outputDir, { recursive: true, force: true });
|
|
40
|
+
await fs.mkdir(options.buildDir, { recursive: true });
|
|
41
|
+
await fs.mkdir(options.distDir, { recursive: true });
|
|
42
|
+
await copyAssetsDir(options.inputDir, options.assetsDir);
|
|
43
|
+
|
|
44
|
+
const icon = normalizeRaycastIcon(sourcePackage.icon || convertedCommands[0]?.icon) || DEFAULT_PLUGIN_ICON;
|
|
45
|
+
const commandPreferences = convertedCommands.flatMap(command => command.preferences || []);
|
|
46
|
+
const preferences = mergePreferences(sourcePackage.preferences || [], commandPreferences, warnings);
|
|
47
|
+
const publicCommands = createPublicCommands(convertedCommands, icon);
|
|
48
|
+
const publicPlugin = {
|
|
49
|
+
title: sourcePackage.title || sourcePackage.name,
|
|
50
|
+
subtitle: sourcePackage.description || sourcePackage.name,
|
|
51
|
+
description: sourcePackage.description,
|
|
52
|
+
icon,
|
|
53
|
+
main: './dist/public-main.js',
|
|
54
|
+
server: './dist/server.js',
|
|
55
|
+
...(preferences.length ? { preferences } : {}),
|
|
56
|
+
commands: publicCommands,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await writeJson(path.join(options.outputDir, 'package.json'), createConvertedPackage(sourcePackage, publicPlugin, {
|
|
60
|
+
publicApiDependency: options.publicApiDependency,
|
|
61
|
+
warnings,
|
|
62
|
+
}));
|
|
63
|
+
await fs.writeFile(path.join(options.buildDir, 'public-main.ts'), generatePublicMain(), 'utf8');
|
|
64
|
+
await fs.writeFile(path.join(options.buildDir, 'server.ts'), generateServerModule(convertedCommands, sourcePackage.name, publicCommands), 'utf8');
|
|
65
|
+
await fs.writeFile(path.join(options.outputDir, 'tsdown.config.ts'), generateTsdownConfig(options), 'utf8');
|
|
66
|
+
|
|
67
|
+
const report: ConversionReport = {
|
|
68
|
+
source: options.inputDir,
|
|
69
|
+
output: options.outputDir,
|
|
70
|
+
convertedCommands: convertedCommands.map(command => ({ name: command.name, entry: command.entry })),
|
|
71
|
+
skippedCommands,
|
|
72
|
+
warnings,
|
|
73
|
+
};
|
|
74
|
+
await writeJson(path.join(options.outputDir, 'raycast-conversion-report.json'), report);
|
|
75
|
+
|
|
76
|
+
if (options.build) {
|
|
77
|
+
installAndBuild(options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return report;
|
|
81
|
+
};
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import type { ConvertMode, ConvertOptions, ResolvedConvertOptions } from './types';
|
|
3
|
+
|
|
4
|
+
const resolveMode = (mode: ConvertOptions['mode']): ConvertMode => mode || 'development';
|
|
5
|
+
|
|
6
|
+
export const resolveConvertOptions = (options: ConvertOptions): ResolvedConvertOptions => {
|
|
7
|
+
const inputDir = path.resolve(options.inputDir);
|
|
8
|
+
const outputDir = path.resolve(options.outputDir || `${inputDir}-public`);
|
|
9
|
+
const invocationDir = path.resolve(options.invocationDir || process.cwd());
|
|
10
|
+
const mode = resolveMode(options.mode);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
inputDir,
|
|
14
|
+
outputDir,
|
|
15
|
+
build: Boolean(options.build),
|
|
16
|
+
mode,
|
|
17
|
+
invocationDir,
|
|
18
|
+
publicApiDependency: mode === 'development' ? `file:${path.join(invocationDir, 'packages', 'api')}` : 'latest',
|
|
19
|
+
buildDir: path.join(outputDir, '.raycast-build'),
|
|
20
|
+
distDir: path.join(outputDir, 'dist'),
|
|
21
|
+
assetsDir: path.join(outputDir, 'assets'),
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ConvertWarning, RaycastPackage } from './types';
|
|
2
|
+
|
|
3
|
+
const raycastApiPackages = new Set(['@raycast/api', '@raycast/utils']);
|
|
4
|
+
|
|
5
|
+
const rewriteDependencyMap = (dependencies: Record<string, string> | undefined) => {
|
|
6
|
+
const rewritten = { ...(dependencies || {}) };
|
|
7
|
+
let replacedRaycastApi = false;
|
|
8
|
+
for (const packageName of raycastApiPackages) {
|
|
9
|
+
if (packageName in rewritten) {
|
|
10
|
+
delete rewritten[packageName];
|
|
11
|
+
replacedRaycastApi = true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { dependencies: rewritten, replacedRaycastApi };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const createConvertedPackage = (
|
|
18
|
+
sourcePackage: RaycastPackage,
|
|
19
|
+
publicPlugin: Record<string, unknown>,
|
|
20
|
+
options: { publicApiDependency: string, warnings: ConvertWarning[] },
|
|
21
|
+
) => {
|
|
22
|
+
const dependenciesResult = rewriteDependencyMap(sourcePackage.dependencies);
|
|
23
|
+
const devDependenciesResult = rewriteDependencyMap(sourcePackage.devDependencies);
|
|
24
|
+
if (dependenciesResult.replacedRaycastApi || devDependenciesResult.replacedRaycastApi) {
|
|
25
|
+
options.warnings.push({
|
|
26
|
+
type: 'dependency',
|
|
27
|
+
message: 'Replaced @raycast/api and/or @raycast/utils with @public-tauri/api',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...sourcePackage,
|
|
33
|
+
version: sourcePackage.version || '1.0.0',
|
|
34
|
+
type: 'module',
|
|
35
|
+
private: true,
|
|
36
|
+
publicPlugin,
|
|
37
|
+
scripts: {
|
|
38
|
+
...(sourcePackage.scripts || {}),
|
|
39
|
+
build: 'tsdown --config tsdown.config.ts',
|
|
40
|
+
},
|
|
41
|
+
dependencies: {
|
|
42
|
+
...dependenciesResult.dependencies,
|
|
43
|
+
'@public-tauri/api': dependenciesResult.dependencies['@public-tauri/api'] || options.publicApiDependency,
|
|
44
|
+
},
|
|
45
|
+
devDependencies: {
|
|
46
|
+
...devDependenciesResult.dependencies,
|
|
47
|
+
tsdown: devDependenciesResult.dependencies.tsdown || '^0.21.7',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ConvertWarning, RaycastPreference } from './types';
|
|
2
|
+
|
|
3
|
+
const mapPreferenceType = (type: string | undefined, warnings: ConvertWarning[]) => {
|
|
4
|
+
switch (type) {
|
|
5
|
+
case 'password':
|
|
6
|
+
return 'password';
|
|
7
|
+
case 'textarea':
|
|
8
|
+
return 'textarea';
|
|
9
|
+
case 'dropdown':
|
|
10
|
+
return 'select';
|
|
11
|
+
case 'checkbox':
|
|
12
|
+
return 'select';
|
|
13
|
+
case 'textfield':
|
|
14
|
+
case 'appPicker':
|
|
15
|
+
case undefined:
|
|
16
|
+
return 'text';
|
|
17
|
+
default:
|
|
18
|
+
warnings.push({ type: 'preference', message: `Unsupported preference type "${type}", converted to text` });
|
|
19
|
+
return 'text';
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const convertPreference = (preference: RaycastPreference, warnings: ConvertWarning[]) => {
|
|
24
|
+
const type = mapPreferenceType(preference.type, warnings);
|
|
25
|
+
const options = preference.type === 'checkbox'
|
|
26
|
+
? [
|
|
27
|
+
{ label: 'Yes', value: true },
|
|
28
|
+
{ label: 'No', value: false },
|
|
29
|
+
]
|
|
30
|
+
: preference.data?.map(item => ({
|
|
31
|
+
label: item.title || item.label || String(item.value),
|
|
32
|
+
value: item.value,
|
|
33
|
+
}));
|
|
34
|
+
return {
|
|
35
|
+
name: preference.name,
|
|
36
|
+
title: preference.title || preference.label || preference.name,
|
|
37
|
+
description: preference.description,
|
|
38
|
+
type,
|
|
39
|
+
required: Boolean(preference.required),
|
|
40
|
+
placeholder: preference.placeholder,
|
|
41
|
+
defaultValue: preference.defaultValue ?? preference.default,
|
|
42
|
+
...(options?.length ? { options } : {}),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const mergePreferences = (
|
|
47
|
+
pluginPreferences: RaycastPreference[],
|
|
48
|
+
commandPreferences: RaycastPreference[],
|
|
49
|
+
warnings: ConvertWarning[],
|
|
50
|
+
) => {
|
|
51
|
+
const preferenceNames = new Set<string>();
|
|
52
|
+
return [...pluginPreferences, ...commandPreferences]
|
|
53
|
+
.map(preference => convertPreference(preference, warnings))
|
|
54
|
+
.filter((preference) => {
|
|
55
|
+
if (preferenceNames.has(preference.name)) {
|
|
56
|
+
warnings.push({ type: 'preference', message: `Duplicate preference "${preference.name}" was skipped` });
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
preferenceNames.add(preference.name);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type RaycastPreference = {
|
|
2
|
+
name: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
default?: unknown;
|
|
10
|
+
defaultValue?: unknown;
|
|
11
|
+
data?: { title?: string, label?: string, value: string | number | boolean }[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type RaycastCommand = {
|
|
15
|
+
name: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
mode?: string;
|
|
20
|
+
keywords?: string[];
|
|
21
|
+
icon?: string;
|
|
22
|
+
preferences?: RaycastPreference[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RaycastPackage = {
|
|
26
|
+
name: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
type?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
icon?: string;
|
|
32
|
+
commands?: RaycastCommand[];
|
|
33
|
+
preferences?: RaycastPreference[];
|
|
34
|
+
scripts?: Record<string, string>;
|
|
35
|
+
dependencies?: Record<string, string>;
|
|
36
|
+
devDependencies?: Record<string, string>;
|
|
37
|
+
} & Record<string, unknown>;
|
|
38
|
+
|
|
39
|
+
export type ConvertWarning = {
|
|
40
|
+
type: string;
|
|
41
|
+
message: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ConvertedCommand = RaycastCommand & {
|
|
45
|
+
entry: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type ConvertMode = 'development' | 'production';
|
|
49
|
+
|
|
50
|
+
export type ConvertOptions = {
|
|
51
|
+
inputDir: string;
|
|
52
|
+
outputDir?: string;
|
|
53
|
+
build?: boolean;
|
|
54
|
+
mode?: ConvertMode;
|
|
55
|
+
invocationDir?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ResolvedConvertOptions = Required<Omit<ConvertOptions, 'outputDir' | 'mode'>> & {
|
|
59
|
+
outputDir: string;
|
|
60
|
+
mode: ConvertMode;
|
|
61
|
+
publicApiDependency: string;
|
|
62
|
+
buildDir: string;
|
|
63
|
+
distDir: string;
|
|
64
|
+
assetsDir: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ConversionReport = {
|
|
68
|
+
source: string;
|
|
69
|
+
output: string;
|
|
70
|
+
convertedCommands: { name: string, entry: string }[];
|
|
71
|
+
skippedCommands: { name: string, reason: string }[];
|
|
72
|
+
warnings: ConvertWarning[];
|
|
73
|
+
};
|