@public-tauri/raycast-convert 1.0.1 → 1.1.0

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.
@@ -0,0 +1,729 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import fs$1 from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/build.ts
7
+ const installCommand = "pnpm";
8
+ const installArgs = [
9
+ "install",
10
+ "--ignore-scripts",
11
+ "--frozen-lockfile=false"
12
+ ];
13
+ const runInstall = (cwd, label) => {
14
+ console.log(`Installing ${label} dependencies...`);
15
+ const result = spawnSync(installCommand, installArgs, {
16
+ cwd,
17
+ stdio: "inherit"
18
+ });
19
+ if (result.status !== 0) throw new Error(`${label} dependency install failed with exit code ${result.status}`);
20
+ };
21
+ const installOutputDependencies = (options) => {
22
+ runInstall(options.outputDir, "converted plugin");
23
+ };
24
+ const getBuildCommand = (options) => {
25
+ return {
26
+ command: "pnpm",
27
+ args: [
28
+ "exec",
29
+ "tsdown",
30
+ "--config",
31
+ path.join(options.outputDir, "tsdown.config.ts")
32
+ ],
33
+ cwd: options.outputDir
34
+ };
35
+ };
36
+ const buildConvertedPlugin = (options) => {
37
+ const { command, args: buildArgs, cwd } = getBuildCommand(options);
38
+ const result = spawnSync(command, buildArgs, {
39
+ cwd,
40
+ stdio: "inherit"
41
+ });
42
+ if (result.status !== 0) throw new Error(`Build failed with exit code ${result.status}`);
43
+ };
44
+ const installAndBuild = (options) => {
45
+ installOutputDependencies(options);
46
+ buildConvertedPlugin(options);
47
+ };
48
+ //#endregion
49
+ //#region src/files.ts
50
+ const readJson = async (filePath) => JSON.parse(await fs.readFile(filePath, "utf8"));
51
+ const writeJson = async (filePath, value) => {
52
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
53
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
54
+ };
55
+ const exists = async (filePath) => {
56
+ try {
57
+ await fs.access(filePath);
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ };
63
+ /** Names skipped when mirroring the Raycast plugin into outDir (see README). */
64
+ const PLUGIN_COPY_SKIP_NAMES = new Set([
65
+ "package.json",
66
+ "node_modules",
67
+ "pnpm-lock.yaml",
68
+ "package-lock.json",
69
+ "yarn.lock"
70
+ ]);
71
+ /**
72
+ * Recursively copy the Raycast plugin directory into `outputDir`, except entries in `skipNames`.
73
+ * Source `package.json` is never copied; generated `package.json` is written separately.
74
+ */
75
+ const copyPluginSourceToOutput = async (inputDir, outputDir, skipNames = PLUGIN_COPY_SKIP_NAMES) => {
76
+ await fs.mkdir(outputDir, { recursive: true });
77
+ const entries = await fs.readdir(inputDir, { withFileTypes: true });
78
+ for (const entry of entries) {
79
+ if (skipNames.has(entry.name)) continue;
80
+ const from = path.join(inputDir, entry.name);
81
+ const to = path.join(outputDir, entry.name);
82
+ await fs.cp(from, to, { recursive: true });
83
+ }
84
+ };
85
+ //#endregion
86
+ //#region src/commands.ts
87
+ const findCommandEntry = async (inputDir, command) => {
88
+ const candidates = [
89
+ path.join(inputDir, "src", `${command.name}.tsx`),
90
+ path.join(inputDir, "src", `${command.name}.ts`),
91
+ path.join(inputDir, "src", `${command.name}.jsx`),
92
+ path.join(inputDir, "src", `${command.name}.js`),
93
+ path.join(inputDir, "src", command.name, "index.tsx"),
94
+ path.join(inputDir, "src", command.name, "index.ts"),
95
+ path.join(inputDir, "src", command.name, "index.jsx"),
96
+ path.join(inputDir, "src", command.name, "index.js")
97
+ ];
98
+ for (const candidate of candidates) if (await exists(candidate)) return candidate;
99
+ return null;
100
+ };
101
+ const SUPPORTED_COMMAND_MODES = new Set(["no-view", "view"]);
102
+ const SUPPORTED_COMMAND_ARGUMENT_TYPES = new Set([
103
+ "text",
104
+ "password",
105
+ "dropdown"
106
+ ]);
107
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
108
+ const validateCommandArguments = (command) => {
109
+ const args = command.arguments;
110
+ if (!args) return null;
111
+ if (!Array.isArray(args)) return "arguments must be an array";
112
+ if (args.length > 3) return "arguments length exceeds Raycast limit (3)";
113
+ for (const item of args) {
114
+ const arg = item;
115
+ if (!isObject(arg)) return "argument item must be an object";
116
+ if (!arg.name || typeof arg.name !== "string") return "argument.name is required";
117
+ if (!arg.type || typeof arg.type !== "string" || !SUPPORTED_COMMAND_ARGUMENT_TYPES.has(arg.type)) return `argument "${arg.name}" has unsupported type: ${String(arg.type)}`;
118
+ if (arg.required !== void 0 && typeof arg.required !== "boolean") return `argument "${arg.name}" has invalid required field`;
119
+ if (arg.placeholder !== void 0 && typeof arg.placeholder !== "string") return `argument "${arg.name}" has invalid placeholder field`;
120
+ if (arg.type === "dropdown") {
121
+ if (!Array.isArray(arg.data) || arg.data.length === 0) return `dropdown argument "${arg.name}" requires non-empty data`;
122
+ for (const option of arg.data) if (!isObject(option) || typeof option.value !== "string") return `dropdown argument "${arg.name}" has invalid option value`;
123
+ }
124
+ }
125
+ return null;
126
+ };
127
+ const resolveSupportedCommands = async (inputDir, sourceCommands) => {
128
+ const convertedCommands = [];
129
+ const skippedCommands = [];
130
+ for (const command of sourceCommands) {
131
+ const mode = command.mode || "view";
132
+ if (!SUPPORTED_COMMAND_MODES.has(mode)) {
133
+ skippedCommands.push({
134
+ name: command.name,
135
+ reason: `Unsupported mode: ${command.mode || "<empty>"}`
136
+ });
137
+ continue;
138
+ }
139
+ const argumentErr = validateCommandArguments(command);
140
+ if (argumentErr) {
141
+ skippedCommands.push({
142
+ name: command.name,
143
+ reason: `Invalid arguments: ${argumentErr}`
144
+ });
145
+ continue;
146
+ }
147
+ const entry = await findCommandEntry(inputDir, command);
148
+ if (!entry) {
149
+ skippedCommands.push({
150
+ name: command.name,
151
+ reason: "Command entry not found under src/"
152
+ });
153
+ continue;
154
+ }
155
+ convertedCommands.push({
156
+ ...command,
157
+ mode,
158
+ entry
159
+ });
160
+ }
161
+ if (!convertedCommands.length) throw new Error("No supported Raycast commands were converted");
162
+ return {
163
+ convertedCommands,
164
+ skippedCommands
165
+ };
166
+ };
167
+ //#endregion
168
+ //#region src/generate/copy-templates.ts
169
+ const templateDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../templates");
170
+ const workerViewRelPaths = [
171
+ "json-patch.ts",
172
+ "raycast-view-protocol.ts",
173
+ "raycast-worker-runtime.ts",
174
+ "host-instance.ts",
175
+ "virtual-serialize.ts"
176
+ ];
177
+ function copyTemplateFile(relativePath, buildDir) {
178
+ const dest = path.join(buildDir, relativePath);
179
+ fs$1.mkdirSync(path.dirname(dest), { recursive: true });
180
+ fs$1.copyFileSync(path.join(templateDir, relativePath), dest);
181
+ }
182
+ /** 自 startDir 向上查找 monorepo 根(与 options.ts 一致) */
183
+ function findPublicTauriRepoRoot$1(startDir) {
184
+ let dir = path.resolve(startDir);
185
+ const { root } = path.parse(dir);
186
+ while (dir !== root) {
187
+ const ws = path.join(dir, "pnpm-workspace.yaml");
188
+ const apiPkg = path.join(dir, "packages", "api", "package.json");
189
+ if (fs$1.existsSync(ws) && fs$1.existsSync(apiPkg)) return dir;
190
+ dir = path.dirname(dir);
191
+ }
192
+ return null;
193
+ }
194
+ function getTemplateRaycastViewDistDir(repoRoot) {
195
+ return path.join(repoRoot, "packages/template/dist");
196
+ }
197
+ /** 若尚未 build,则在 repo 根执行 `pnpm --filter @public-tauri/template build` */
198
+ function ensureRaycastViewTemplateBuilt(repoRoot) {
199
+ const marker = path.join(getTemplateRaycastViewDistDir(repoRoot), "raycast.html");
200
+ if (fs$1.existsSync(marker)) return;
201
+ if (spawnSync("pnpm", [
202
+ "--filter",
203
+ "@public-tauri/template",
204
+ "run",
205
+ "build"
206
+ ], {
207
+ cwd: repoRoot,
208
+ stdio: "inherit",
209
+ shell: process.platform === "win32"
210
+ }).status !== 0) throw new Error("Building @public-tauri/template failed. From the repo root run: pnpm --filter @public-tauri/template build");
211
+ }
212
+ /**
213
+ * 将 `packages/template` 的 Vite 应用产物复制到插件 `dist/view/`(与 `dist/server.js` 并列)。
214
+ * wujie `publicPlugin.html` 指向 `./dist/view/raycast.html`。
215
+ */
216
+ function copyRaycastViewTemplateAppDist(repoRoot, pluginDistDir) {
217
+ const srcDir = getTemplateRaycastViewDistDir(repoRoot);
218
+ const marker = path.join(srcDir, "raycast.html");
219
+ if (!fs$1.existsSync(marker)) throw new Error(`Missing ${marker}; build @public-tauri/template first.`);
220
+ const destDir = path.join(pluginDistDir, "view");
221
+ fs$1.rmSync(destDir, {
222
+ recursive: true,
223
+ force: true
224
+ });
225
+ fs$1.cpSync(srcDir, destDir, { recursive: true });
226
+ }
227
+ /** 将 Worker 侧 view 模板(reconciler + 宿主类型 + 序列化 + 组件)复制到 `.raycast-build` */
228
+ function copyRaycastWorkerViewBundle(buildDir) {
229
+ for (const rel of workerViewRelPaths) copyTemplateFile(rel, buildDir);
230
+ }
231
+ //#endregion
232
+ //#region src/generate/public-main.ts
233
+ const generatePublicMain = () => `export default function createPlugin() {
234
+ const getApi = () => window.$wujie?.props;
235
+ return {
236
+ async onAction(command, action, query, options) {
237
+ const api = getApi();
238
+ try {
239
+ await api.channel.invoke('raycast:run', {
240
+ commandName: command.name,
241
+ query,
242
+ action,
243
+ options,
244
+ preferences: api.getPreferences?.() || {},
245
+ });
246
+ } catch (error) {
247
+ const message = error instanceof Error ? error.message : String(error);
248
+ await api.dialog.showToast(message);
249
+ throw error;
250
+ }
251
+ },
252
+ };
253
+ }
254
+ `;
255
+ //#endregion
256
+ //#region src/generate/server-module.ts
257
+ const commandEntryImportSpecifier = (inputDir, outputDir, buildDir, entry) => {
258
+ const outputEntry = path.join(outputDir, path.relative(path.resolve(inputDir), path.resolve(entry)));
259
+ let rel = path.relative(path.resolve(buildDir), outputEntry);
260
+ rel = rel.split(path.sep).join("/");
261
+ if (rel && !rel.startsWith(".") && !rel.startsWith("/")) return `./${rel}`;
262
+ return rel;
263
+ };
264
+ const generateServerModule = (commands, packageName, publicCommands, layout) => {
265
+ const nvLoaders = commands.noView.map((command) => ` ${JSON.stringify(command.name)}: () => import(${JSON.stringify(commandEntryImportSpecifier(layout.inputDir, layout.outputDir, layout.buildDir, command.entry))}),`).join("\n");
266
+ const vvLoaders = commands.view.map((command) => ` ${JSON.stringify(command.name)}: () => import(${JSON.stringify(commandEntryImportSpecifier(layout.inputDir, layout.outputDir, layout.buildDir, command.entry))}),`).join("\n");
267
+ const commandManifests = JSON.stringify(publicCommands, null, 2);
268
+ const viewRuntimeImport = commands.view.length ? "import { createRaycastViewSession, __setRaycastViewContext } from './raycast-worker-runtime';" : "";
269
+ const raycastRunHandler = commands.noView.length ? `channel.handle('raycast:run', async (payload = {}) => {
270
+ const commandName = String(payload.commandName || '');
271
+ const loadCommandModule = commandModuleLoaders[commandName];
272
+ if (!loadCommandModule) {
273
+ throw new Error(\`Unknown Raycast command: \${commandName}\`);
274
+ }
275
+ const launchPayload = payload.options?.payload || {};
276
+ __setRaycastContext({
277
+ pluginName: ${JSON.stringify(packageName)},
278
+ commandName,
279
+ commandMode: 'no-view',
280
+ commands: commandManifests,
281
+ launchType: launchPayload.launchType,
282
+ preferences: payload.preferences || {},
283
+ supportPath: path.join(pluginRoot, '.raycast-compat'),
284
+ assetsPath: path.join(pluginRoot, 'assets'),
285
+ });
286
+ const commandModule = await loadCommandModule();
287
+ const run = commandModule.default;
288
+ if (typeof run !== 'function') {
289
+ throw new Error(\`Raycast command \${commandName} has no default function export\`);
290
+ }
291
+ return await run({
292
+ arguments: launchPayload.arguments || {},
293
+ fallbackText: launchPayload.fallbackText ?? payload.query ?? '',
294
+ launchContext: launchPayload.context ?? payload,
295
+ launchType: launchPayload.launchType || 'userInitiated',
296
+ });
297
+ });
298
+ ` : "";
299
+ const viewHandlers = commands.view.length ? `const viewSessions = new Map();
300
+
301
+ channel.handle('raycast:view:mount', async (payload = {}) => {
302
+ const commandName = String(payload.commandName || '');
303
+ const existingSession = viewSessions.get(commandName);
304
+ if (existingSession) {
305
+ existingSession.unmount();
306
+ viewSessions.delete(commandName);
307
+ }
308
+ const loadViewCommandModule = viewCommandModuleLoaders[commandName];
309
+ if (!loadViewCommandModule) {
310
+ throw new Error(\`Unknown Raycast view command: \${commandName}\`);
311
+ }
312
+ __setRaycastViewContext({
313
+ pluginName: ${JSON.stringify(packageName)},
314
+ commandName,
315
+ preferences: payload.preferences || {},
316
+ supportPath: path.join(pluginRoot, '.raycast-compat'),
317
+ assetsPath: path.join(pluginRoot, 'assets'),
318
+ launchProps: {
319
+ arguments: payload.options?.payload?.arguments || {},
320
+ fallbackText: payload.options?.payload?.fallbackText ?? payload.query ?? '',
321
+ launchContext: payload.options?.payload?.context ?? payload,
322
+ launchType: payload.options?.payload?.launchType || 'userInitiated',
323
+ },
324
+ });
325
+ const commandModule = await loadViewCommandModule();
326
+ const Command = commandModule.default;
327
+ if (typeof Command !== 'function') {
328
+ throw new Error(\`Raycast view command \${commandName} has no default function export\`);
329
+ }
330
+
331
+ const session = createRaycastViewSession({
332
+ emitSnapshot: (snapshot) => channel.emit('raycast:view:snapshot', snapshot),
333
+ emitPatch: (patches) => channel.emit('raycast:view:patch', patches),
334
+ });
335
+ viewSessions.set(commandName, session);
336
+ await session.mount(Command);
337
+ return true;
338
+ });
339
+
340
+ channel.handle('raycast:view:run-action', async (payload = {}) => {
341
+ const session = viewSessions.get(String(payload.commandName || ''));
342
+ if (!session) throw new Error(\`No Raycast view session for \${String(payload.commandName || '')}\`);
343
+ const rawArgs = payload.args;
344
+ const args = Array.isArray(rawArgs) ? rawArgs : [];
345
+ await session.dispatchHostEvent(String(payload.hostId || ''), String(payload.event || 'onAction'), args);
346
+ return true;
347
+ });
348
+
349
+ channel.handle('raycast:view:unmount', async (payload = {}) => {
350
+ const commandName = String(payload.commandName || '');
351
+ const session = viewSessions.get(commandName);
352
+ if (session) {
353
+ session.unmount();
354
+ viewSessions.delete(commandName);
355
+ }
356
+ // 无活跃 view 会话时退出 Worker,配合宿主按需加载下次再拉起线程
357
+ if (viewSessions.size === 0) {
358
+ queueMicrotask(() => process.exit(0));
359
+ }
360
+ return true;
361
+ });
362
+ ` : "";
363
+ return `import path from 'node:path';
364
+ import { fileURLToPath } from 'node:url';
365
+ import { channel } from '@public-tauri/api/node';
366
+ import { __setRaycastContext } from '@public-tauri/api/raycast';
367
+ ${viewRuntimeImport}
368
+
369
+ ${commands.noView.length ? `const commandModuleLoaders: Record<string, () => Promise<any>> = {
370
+ ${nvLoaders}
371
+ };
372
+ ` : "const commandModuleLoaders: Record<string, () => Promise<any>> = {};"}
373
+ ${commands.view.length ? `const viewCommandModuleLoaders: Record<string, () => Promise<any>> = {
374
+ ${vvLoaders}
375
+ };
376
+ ` : "const viewCommandModuleLoaders: Record<string, () => Promise<any>> = {};"}
377
+
378
+ const distDir = path.dirname(fileURLToPath(import.meta.url));
379
+ const pluginRoot = path.dirname(distDir);
380
+ const commandManifests = ${commandManifests};
381
+
382
+ ${raycastRunHandler}
383
+ ${viewHandlers}
384
+ `;
385
+ };
386
+ //#endregion
387
+ //#region src/generate/tsdown-config.ts
388
+ const formatAlias = (aliases) => Object.entries(aliases).map(([key, value]) => ` ${JSON.stringify(key)}: ${JSON.stringify(value)},`).join("\n");
389
+ const formatAliasProperty = (aliases) => {
390
+ const entries = formatAlias(aliases);
391
+ return entries ? ` alias: {\n${entries}\n },` : " alias: {},";
392
+ };
393
+ /** Server 入口:`@raycast/api` → `@public-tauri/api/raycast` 源码;view 与 no-view 一致。 */
394
+ const getServerAliases = (outputDir) => {
395
+ const apiSrc = path.join(outputDir, "node_modules", "@public-tauri", "api", "src");
396
+ return {
397
+ "@raycast/api": path.join(apiSrc, "raycast.ts"),
398
+ "@public-tauri/api/node": path.join(apiSrc, "node.ts")
399
+ };
400
+ };
401
+ const generateTsdownConfig = (options, flags) => {
402
+ const entries = [];
403
+ if (flags.hasPublicMain) entries.push(` {
404
+ entry: ${JSON.stringify(path.join(options.buildDir, "public-main.ts"))},
405
+ format: 'esm',
406
+ platform: 'browser',
407
+ target: 'es2022',
408
+ outDir: ${JSON.stringify(options.distDir)},
409
+ outExtensions: () => ({ js: '.js' }),
410
+ deps: {
411
+ alwaysBundle: () => true,
412
+ },
413
+ ${formatAliasProperty({})}
414
+ },`);
415
+ entries.push(` {
416
+ entry: ${JSON.stringify(path.join(options.buildDir, "server.ts"))},
417
+ format: 'esm',
418
+ platform: 'node',
419
+ target: 'es2022',
420
+ outDir: ${JSON.stringify(options.distDir)},
421
+ outExtensions: () => ({ js: '.js' }),
422
+ deps: {
423
+ alwaysBundle: () => true,
424
+ },
425
+ ${formatAliasProperty(getServerAliases(options.outputDir))}
426
+ },`);
427
+ return `export default [
428
+ ${entries.join("\n")}
429
+ ];
430
+ `;
431
+ };
432
+ //#endregion
433
+ //#region src/icons.ts
434
+ const isUrlLike = (value) => /^(?:https?:|data:|asset:|public-icon:)/.test(value);
435
+ const hasPathSegment = (value) => value.includes("/") || value.includes("\\");
436
+ const normalizeRaycastIcon = (icon) => {
437
+ if (!icon) return void 0;
438
+ if (isUrlLike(icon)) return icon;
439
+ if (icon.startsWith("./") || icon.startsWith("../") || icon.startsWith("/")) return icon;
440
+ if (hasPathSegment(icon)) return `./${icon}`;
441
+ return `./assets/${icon}`;
442
+ };
443
+ //#endregion
444
+ //#region src/options.ts
445
+ const resolveMode = (mode) => mode || "development";
446
+ /**
447
+ * 自 startDir 向上查找 monorepo 根(pnpm-workspace.yaml + packages/api),避免依赖 process.cwd()
448
+ *(例如从 src-node、src-node/src 启动 Node 时 cwd 并非仓库根)。
449
+ */
450
+ const findPublicTauriRepoRoot = (startDir) => {
451
+ let dir = path.resolve(startDir);
452
+ const { root } = path.parse(dir);
453
+ while (dir !== root) {
454
+ const ws = path.join(dir, "pnpm-workspace.yaml");
455
+ const apiPkg = path.join(dir, "packages", "api", "package.json");
456
+ if (fs$1.existsSync(ws) && fs$1.existsSync(apiPkg)) return dir;
457
+ dir = path.dirname(dir);
458
+ }
459
+ };
460
+ const resolveInvocationDir = (options) => {
461
+ if (options.invocationDir) return path.resolve(options.invocationDir);
462
+ if (resolveMode(options.mode) !== "development") return path.resolve(process.cwd());
463
+ const fromRoot = findPublicTauriRepoRoot(process.cwd());
464
+ if (fromRoot) return fromRoot;
465
+ throw new Error("development 模式需要定位 monorepo 根目录(含 pnpm-workspace.yaml 与 packages/api)。请在仓库内执行、设置 invocationDir,或改用 production 模式。");
466
+ };
467
+ const resolveConvertOptions = (options) => {
468
+ const inputDir = path.resolve(options.inputDir);
469
+ const outputDir = path.resolve(options.outputDir || `${inputDir}-public`);
470
+ const invocationDir = resolveInvocationDir(options);
471
+ const mode = resolveMode(options.mode);
472
+ return {
473
+ inputDir,
474
+ outputDir,
475
+ build: Boolean(options.build),
476
+ mode,
477
+ invocationDir,
478
+ publicApiDependency: mode === "development" ? `file:${path.join(invocationDir, "packages", "api")}` : "latest",
479
+ buildDir: path.join(outputDir, ".raycast-build"),
480
+ distDir: path.join(outputDir, "dist")
481
+ };
482
+ };
483
+ //#endregion
484
+ //#region src/package-json.ts
485
+ const rewriteDependencyMap = (dependencies) => {
486
+ const rewritten = { ...dependencies || {} };
487
+ const replacedRaycastApi = "@raycast/api" in rewritten;
488
+ if (replacedRaycastApi) delete rewritten["@raycast/api"];
489
+ return {
490
+ dependencies: rewritten,
491
+ replacedRaycastApi
492
+ };
493
+ };
494
+ const createConvertedPackage = (sourcePackage, publicPlugin, options) => {
495
+ const dependenciesResult = rewriteDependencyMap(sourcePackage.dependencies);
496
+ const devDependenciesResult = rewriteDependencyMap(sourcePackage.devDependencies);
497
+ if (dependenciesResult.replacedRaycastApi || devDependenciesResult.replacedRaycastApi) options.warnings.push({
498
+ type: "dependency",
499
+ message: "Replaced @raycast/api with @public-tauri/api (see tsdown alias); @raycast/utils is left as declared"
500
+ });
501
+ return {
502
+ ...sourcePackage,
503
+ name: options.convertedPackageName,
504
+ version: sourcePackage.version || "1.0.0",
505
+ type: "module",
506
+ private: true,
507
+ publicPlugin,
508
+ scripts: {
509
+ ...sourcePackage.scripts || {},
510
+ build: "tsdown --config tsdown.config.ts"
511
+ },
512
+ dependencies: {
513
+ ...dependenciesResult.dependencies,
514
+ "@public-tauri/api": dependenciesResult.dependencies["@public-tauri/api"] || options.publicApiDependency,
515
+ ...options.hasViewCommands ? {
516
+ react: dependenciesResult.dependencies.react || "^19.0.0",
517
+ "react-reconciler": dependenciesResult.dependencies["react-reconciler"] || "^0.31.0"
518
+ } : {}
519
+ },
520
+ devDependencies: {
521
+ ...devDependenciesResult.dependencies,
522
+ tsdown: devDependenciesResult.dependencies.tsdown || "^0.21.7",
523
+ ...options.hasViewCommands ? { "@types/react": devDependenciesResult.dependencies["@types/react"] || "^19.0.0" } : {}
524
+ }
525
+ };
526
+ };
527
+ //#endregion
528
+ //#region src/package-name.ts
529
+ const RAYCAST_CONVERTED_SCOPE = "@public-tauri-raycast";
530
+ /** npm 包名片段:与 npm 命名惯例对齐的小写、符号规整 */
531
+ const sanitizeSlug = (raw) => {
532
+ let segment = raw.trim().toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
533
+ if (!segment || /^[._]/.test(segment)) segment = `plugin-${segment || "unnamed"}`.replace(/^-+/, "");
534
+ return segment || "raycast-plugin";
535
+ };
536
+ /**
537
+ * 未 scope 的 Raycast 官方插件:一般为单段名字 → `@public-tauri-raycast/<slug>`。
538
+ * 若将来出现 scoped:`@my-scope/xxx` → `@public-tauri-raycast/my-scope_xxx`。
539
+ */
540
+ const resolveRaycastSlug = (sourcePackage, inputDir) => {
541
+ const raw = typeof sourcePackage.name === "string" ? sourcePackage.name.trim() : "";
542
+ if (raw.startsWith("@")) {
543
+ const slash = raw.indexOf("/");
544
+ if (slash !== -1) {
545
+ const scope = raw.slice(1, slash);
546
+ const pkg = raw.slice(slash + 1);
547
+ return `${sanitizeSlug(scope)}_${sanitizeSlug(pkg)}`;
548
+ }
549
+ return sanitizeSlug(raw.slice(1));
550
+ }
551
+ if (raw) return sanitizeSlug(raw);
552
+ return sanitizeSlug(path.basename(path.resolve(inputDir)));
553
+ };
554
+ const resolveConvertedPackageName = (sourcePackage, inputDir) => `${RAYCAST_CONVERTED_SCOPE}/${resolveRaycastSlug(sourcePackage, inputDir)}`;
555
+ //#endregion
556
+ //#region src/preferences.ts
557
+ const mapPreferenceType = (type, warnings) => {
558
+ switch (type) {
559
+ case "password": return "password";
560
+ case "textarea": return "textarea";
561
+ case "dropdown": return "select";
562
+ case "checkbox": return "select";
563
+ case "textfield":
564
+ case "appPicker":
565
+ case void 0: return "text";
566
+ default:
567
+ warnings.push({
568
+ type: "preference",
569
+ message: `Unsupported preference type "${type}", converted to text`
570
+ });
571
+ return "text";
572
+ }
573
+ };
574
+ const convertPreference = (preference, warnings) => {
575
+ const type = mapPreferenceType(preference.type, warnings);
576
+ const options = preference.type === "checkbox" ? [{
577
+ label: "Yes",
578
+ value: true
579
+ }, {
580
+ label: "No",
581
+ value: false
582
+ }] : preference.data?.map((item) => ({
583
+ label: item.title || item.label || String(item.value),
584
+ value: item.value
585
+ }));
586
+ return {
587
+ name: preference.name,
588
+ title: preference.title || preference.label || preference.name,
589
+ description: preference.description,
590
+ type,
591
+ required: Boolean(preference.required),
592
+ placeholder: preference.placeholder,
593
+ defaultValue: preference.defaultValue ?? preference.default,
594
+ ...options?.length ? { options } : {}
595
+ };
596
+ };
597
+ const mergePreferences = (pluginPreferences, commandPreferences, warnings) => {
598
+ const preferenceNames = /* @__PURE__ */ new Set();
599
+ return [...pluginPreferences, ...commandPreferences].map((preference) => convertPreference(preference, warnings)).filter((preference) => {
600
+ if (preferenceNames.has(preference.name)) {
601
+ warnings.push({
602
+ type: "preference",
603
+ message: `Duplicate preference "${preference.name}" was skipped`
604
+ });
605
+ return false;
606
+ }
607
+ preferenceNames.add(preference.name);
608
+ return true;
609
+ });
610
+ };
611
+ //#endregion
612
+ //#region src/index.ts
613
+ /**
614
+ * `development` 模式下 view 插件的 wujie 入口:指向 `@public-tauri/template` 的 Vite 默认端口下的 `raycast.html`,
615
+ * 便于先 `pnpm --filter @public-tauri/template dev` 再转换插件即可调试 UI,无需模板 `build`/拷贝 `dist/view`。
616
+ * `production` 模式使用 `./dist/view/raycast.html`。
617
+ */
618
+ const RAYCAST_VIEW_TEMPLATE_DEV_ENTRY = "http://localhost:5173/raycast.html";
619
+ const createPublicCommands = (commands, icon) => commands.map((command) => {
620
+ const raycastArguments = (command.arguments || []).map((arg) => ({
621
+ name: arg.name,
622
+ type: arg.type,
623
+ placeholder: arg.placeholder,
624
+ required: Boolean(arg.required),
625
+ data: arg.type === "dropdown" ? (arg.data || []).map((item) => ({
626
+ title: item.title || item.value,
627
+ value: item.value
628
+ })) : void 0
629
+ }));
630
+ return {
631
+ name: command.name,
632
+ title: command.title || command.name,
633
+ subtitle: command.subtitle || command.description,
634
+ description: command.description,
635
+ icon: normalizeRaycastIcon(command.icon) || icon,
636
+ mode: command.mode === "no-view" && raycastArguments.length === 0 ? "none" : "view",
637
+ raycastTargetMode: command.mode,
638
+ raycastArguments,
639
+ matches: [{
640
+ type: "text",
641
+ keywords: command.keywords?.length ? command.keywords : [command.title || command.name]
642
+ }]
643
+ };
644
+ });
645
+ const convertRaycastPlugin = async (rawOptions) => {
646
+ const options = resolveConvertOptions(rawOptions);
647
+ const warnings = [];
648
+ const sourcePackage = await readJson(path.join(options.inputDir, "package.json"));
649
+ const convertedPackageName = resolveConvertedPackageName(sourcePackage, options.inputDir);
650
+ const sourceCommands = sourcePackage.commands || [];
651
+ const { convertedCommands, skippedCommands } = await resolveSupportedCommands(options.inputDir, sourceCommands);
652
+ const noViewCommands = convertedCommands.filter((command) => command.mode === "no-view");
653
+ const viewCommands = convertedCommands.filter((command) => command.mode === "view");
654
+ const hasViewCommands = viewCommands.length > 0;
655
+ const viewHtmlUsesDevServer = hasViewCommands && options.mode === "development";
656
+ await fs.rm(options.outputDir, {
657
+ recursive: true,
658
+ force: true
659
+ });
660
+ await copyPluginSourceToOutput(options.inputDir, options.outputDir);
661
+ await fs.mkdir(options.buildDir, { recursive: true });
662
+ await fs.mkdir(options.distDir, { recursive: true });
663
+ const icon = normalizeRaycastIcon(sourcePackage.icon || convertedCommands[0]?.icon) || "extension";
664
+ const commandPreferences = convertedCommands.flatMap((command) => command.preferences || []);
665
+ const preferences = mergePreferences(sourcePackage.preferences || [], commandPreferences, warnings);
666
+ const publicCommands = createPublicCommands(convertedCommands, icon);
667
+ const publicPlugin = {
668
+ title: sourcePackage.title || sourcePackage.name || convertedPackageName,
669
+ subtitle: sourcePackage.description || sourcePackage.name || convertedPackageName,
670
+ description: sourcePackage.description,
671
+ icon,
672
+ ...noViewCommands.length ? { main: "./dist/public-main.js" } : {},
673
+ server: "./dist/server.js",
674
+ ...viewCommands.length ? { html: viewHtmlUsesDevServer ? RAYCAST_VIEW_TEMPLATE_DEV_ENTRY : "./dist/view/raycast.html" } : {},
675
+ ...preferences.length ? { preferences } : {},
676
+ commands: publicCommands
677
+ };
678
+ await writeJson(path.join(options.outputDir, "package.json"), createConvertedPackage(sourcePackage, publicPlugin, {
679
+ convertedPackageName,
680
+ publicApiDependency: options.publicApiDependency,
681
+ warnings,
682
+ hasViewCommands
683
+ }));
684
+ if (noViewCommands.length) await fs.writeFile(path.join(options.buildDir, "public-main.ts"), generatePublicMain(), "utf8");
685
+ await fs.writeFile(path.join(options.buildDir, "server.ts"), generateServerModule({
686
+ noView: noViewCommands,
687
+ view: viewCommands
688
+ }, convertedPackageName, publicCommands, {
689
+ inputDir: options.inputDir,
690
+ outputDir: options.outputDir,
691
+ buildDir: options.buildDir
692
+ }), "utf8");
693
+ let raycastViewRepoRoot = null;
694
+ if (hasViewCommands) {
695
+ copyRaycastWorkerViewBundle(options.buildDir);
696
+ if (viewHtmlUsesDevServer) raycastViewRepoRoot = null;
697
+ else {
698
+ raycastViewRepoRoot = findPublicTauriRepoRoot$1(options.invocationDir) ?? findPublicTauriRepoRoot$1(process.cwd());
699
+ if (!raycastViewRepoRoot) throw new Error("Could not locate Public Tauri repo root (pnpm-workspace.yaml + packages/api). Run raycast-convert from the monorepo, set invocationDir, or copy packages/template dist into dist/view manually.");
700
+ ensureRaycastViewTemplateBuilt(raycastViewRepoRoot);
701
+ if (!options.build) copyRaycastViewTemplateAppDist(raycastViewRepoRoot, options.distDir);
702
+ }
703
+ }
704
+ await fs.writeFile(path.join(options.outputDir, "tsdown.config.ts"), generateTsdownConfig(options, { hasPublicMain: noViewCommands.length > 0 }), "utf8");
705
+ const report = {
706
+ source: options.inputDir,
707
+ output: options.outputDir,
708
+ sourcePackageName: typeof sourcePackage.name === "string" ? sourcePackage.name : void 0,
709
+ convertedPackageName,
710
+ convertedCommands: convertedCommands.map((command) => {
711
+ const outputEntry = path.join(options.outputDir, path.relative(path.resolve(options.inputDir), path.resolve(command.entry)));
712
+ const entry = path.relative(options.outputDir, outputEntry).split(path.sep).join("/") || ".";
713
+ return {
714
+ name: command.name,
715
+ entry
716
+ };
717
+ }),
718
+ skippedCommands,
719
+ warnings
720
+ };
721
+ await writeJson(path.join(options.outputDir, "raycast-conversion-report.json"), report);
722
+ if (options.build) {
723
+ installAndBuild(options);
724
+ if (hasViewCommands && raycastViewRepoRoot) copyRaycastViewTemplateAppDist(raycastViewRepoRoot, options.distDir);
725
+ }
726
+ return report;
727
+ };
728
+ //#endregion
729
+ export { resolveRaycastSlug as a, resolveConvertedPackageName as i, convertRaycastPlugin as n, sanitizeSlug as o, RAYCAST_CONVERTED_SCOPE as r, RAYCAST_VIEW_TEMPLATE_DEV_ENTRY as t };