mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-08-31 11:17:27 +00:00
feat: add sidecar protocol i/o checking with Zod
Introduces a shared `@raycast-linux/protocol` package using pnpm workspaces to define and validate the communication contract between the sidecar and the frontend.
This commit is contained in:
parent
d817e7a57a
commit
46868539fa
16 changed files with 765 additions and 81 deletions
|
@ -15,6 +15,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@raycast-linux/protocol": "workspace:*",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "~2.2.2",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
|
|
17
packages/protocol/package.json
Normal file
17
packages/protocol/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@raycast-linux/protocol",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.25.63"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.1"
|
||||
}
|
73
packages/protocol/src/index.ts
Normal file
73
packages/protocol/src/index.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { z } from 'zod/v4';
|
||||
|
||||
const CreateInstancePayloadSchema = z.object({
|
||||
id: z.number(),
|
||||
type: z.string(),
|
||||
props: z.record(z.string(), z.unknown()),
|
||||
children: z.array(z.number()).optional(),
|
||||
namedChildren: z.record(z.string(), z.number()).optional()
|
||||
});
|
||||
|
||||
const CreateTextInstancePayloadSchema = z.object({
|
||||
id: z.number(),
|
||||
type: z.literal('TEXT'),
|
||||
text: z.string()
|
||||
});
|
||||
|
||||
const ParentChildPayloadSchema = z.object({
|
||||
parentId: z.union([z.literal('root'), z.number()]),
|
||||
childId: z.number()
|
||||
});
|
||||
|
||||
const InsertBeforePayloadSchema = z.object({
|
||||
parentId: z.union([z.literal('root'), z.number()]),
|
||||
childId: z.number(),
|
||||
beforeId: z.number()
|
||||
});
|
||||
|
||||
const UpdatePropsPayloadSchema = z.object({
|
||||
id: z.number(),
|
||||
props: z.record(z.string(), z.unknown()),
|
||||
namedChildren: z.record(z.string(), z.number()).optional()
|
||||
});
|
||||
|
||||
const UpdateTextPayloadSchema = z.object({
|
||||
id: z.number(),
|
||||
text: z.string()
|
||||
});
|
||||
|
||||
const ReplaceChildrenPayloadSchema = z.object({
|
||||
parentId: z.union([z.string(), z.number()]),
|
||||
childrenIds: z.array(z.number())
|
||||
});
|
||||
|
||||
const ClearContainerPayloadSchema = z.object({
|
||||
containerId: z.string()
|
||||
});
|
||||
|
||||
export const CommandSchema = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('CREATE_INSTANCE'), payload: CreateInstancePayloadSchema }),
|
||||
z.object({ type: z.literal('CREATE_TEXT_INSTANCE'), payload: CreateTextInstancePayloadSchema }),
|
||||
z.object({ type: z.literal('APPEND_CHILD'), payload: ParentChildPayloadSchema }),
|
||||
z.object({ type: z.literal('INSERT_BEFORE'), payload: InsertBeforePayloadSchema }),
|
||||
z.object({ type: z.literal('REMOVE_CHILD'), payload: ParentChildPayloadSchema }),
|
||||
z.object({ type: z.literal('UPDATE_PROPS'), payload: UpdatePropsPayloadSchema }),
|
||||
z.object({ type: z.literal('UPDATE_TEXT'), payload: UpdateTextPayloadSchema }),
|
||||
z.object({ type: z.literal('REPLACE_CHILDREN'), payload: ReplaceChildrenPayloadSchema }),
|
||||
z.object({ type: z.literal('CLEAR_CONTAINER'), payload: ClearContainerPayloadSchema })
|
||||
]);
|
||||
export type Command = z.infer<typeof CommandSchema>;
|
||||
|
||||
export const BatchUpdateSchema = z.object({
|
||||
type: z.literal('BATCH_UPDATE'),
|
||||
payload: z.array(CommandSchema)
|
||||
});
|
||||
export type BatchUpdate = z.infer<typeof BatchUpdateSchema>;
|
||||
|
||||
const LogMessageSchema = z.object({
|
||||
type: z.literal('log'),
|
||||
payload: z.unknown()
|
||||
});
|
||||
|
||||
export const SidecarMessageSchema = z.union([BatchUpdateSchema, CommandSchema, LogMessageSchema]);
|
||||
export type SidecarMessage = z.infer<typeof SidecarMessageSchema>;
|
9
packages/protocol/tsconfig.json
Normal file
9
packages/protocol/tsconfig.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
582
pnpm-lock.yaml
generated
582
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
packages:
|
||||
- 'packages/*'
|
||||
- 'sidecar'
|
|
@ -22,6 +22,7 @@
|
|||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@raycast-linux/protocol": "workspace:*",
|
||||
"msgpackr": "^1.11.4",
|
||||
"react": "^19.1.0",
|
||||
"react-reconciler": "^0.32.0"
|
||||
|
|
|
@ -15,7 +15,7 @@ export const getRaycastApi = () => {
|
|||
|
||||
const createWrapperComponent = (name: string) => {
|
||||
const Component = ({ children, ...rest }: { children?: React.ReactNode }) =>
|
||||
jsx(name, { ...rest, children });
|
||||
jsx(name as any, { ...rest, children });
|
||||
Component.displayName = name;
|
||||
return Component;
|
||||
};
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import type { Fiber, HostConfig } from 'react-reconciler';
|
||||
import type { Fiber, HostConfig, OpaqueHandle, ReactContext } from 'react-reconciler';
|
||||
import type {
|
||||
ComponentType,
|
||||
ComponentProps,
|
||||
Container,
|
||||
RaycastInstance,
|
||||
TextInstance,
|
||||
UpdatePayload,
|
||||
ParentInstance,
|
||||
AnyInstance
|
||||
} from './types';
|
||||
import {
|
||||
instances,
|
||||
getNextInstanceId,
|
||||
commitBuffer,
|
||||
addToCommitBuffer,
|
||||
clearCommitBuffer
|
||||
clearCommitBuffer,
|
||||
commitBuffer
|
||||
} from './state';
|
||||
import { writeOutput } from './io';
|
||||
import { serializeProps, optimizeCommitBuffer, getComponentDisplayName } from './utils';
|
||||
import React from 'react';
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
const appendChildToParent = (parent: ParentInstance, child: AnyInstance) => {
|
||||
const existingIndex = parent.children.findIndex(({ id }) => id === child.id);
|
||||
|
@ -70,7 +69,8 @@ function createInstanceFromElement(
|
|||
element: React.ReactElement
|
||||
): RaycastInstance | RaycastInstance[] {
|
||||
if (element.type === React.Fragment) {
|
||||
const childElements = React.Children.toArray(element.props.children);
|
||||
const props = element.props as { children?: ReactNode };
|
||||
const childElements = React.Children.toArray(props.children);
|
||||
return childElements
|
||||
.filter(React.isValidElement)
|
||||
.flatMap((child) => createInstanceFromElement(child as React.ReactElement));
|
||||
|
@ -79,16 +79,14 @@ function createInstanceFromElement(
|
|||
const componentType = getComponentDisplayName(element.type as ComponentType);
|
||||
const id = getNextInstanceId();
|
||||
|
||||
const childElements = React.Children.toArray(
|
||||
'children' in element.props ? element.props.children : []
|
||||
);
|
||||
const props = (element.props ?? {}) as Record<string, unknown>;
|
||||
const children = ('children' in props ? props.children : []) as ReactNode;
|
||||
const childElements = React.Children.toArray(children);
|
||||
const childInstances = childElements
|
||||
.filter(React.isValidElement)
|
||||
.flatMap((child) => createInstanceFromElement(child as React.ReactElement));
|
||||
|
||||
const { propsToSerialize, namedChildren } = processProps(
|
||||
element.props as Record<string, unknown>
|
||||
);
|
||||
const { propsToSerialize, namedChildren } = processProps(props);
|
||||
|
||||
const instance: RaycastInstance = {
|
||||
id,
|
||||
|
@ -115,7 +113,7 @@ function createInstanceFromElement(
|
|||
return instance;
|
||||
}
|
||||
|
||||
function processProps(props: Record<string, any>) {
|
||||
function processProps(props: Record<string, unknown>) {
|
||||
const propsToSerialize: Record<string, unknown> = {};
|
||||
const namedChildren: { [key: string]: number } = {};
|
||||
|
||||
|
@ -140,20 +138,20 @@ function processProps(props: Record<string, any>) {
|
|||
}
|
||||
|
||||
export const hostConfig: HostConfig<
|
||||
ComponentType,
|
||||
ComponentProps,
|
||||
Container,
|
||||
RaycastInstance,
|
||||
TextInstance,
|
||||
never,
|
||||
never,
|
||||
RaycastInstance,
|
||||
object,
|
||||
UpdatePayload,
|
||||
unknown,
|
||||
Record<string, unknown>,
|
||||
NodeJS.Timeout,
|
||||
number
|
||||
ComponentType, // 1. Type
|
||||
ComponentProps, // 2. Props
|
||||
Container, // 3. Container
|
||||
RaycastInstance, // 4. Instance
|
||||
TextInstance, // 5. TextInstance
|
||||
never, // 6. SuspenseInstance
|
||||
never, // 7. HydratableInstance
|
||||
never, // 8. FormInstance
|
||||
RaycastInstance | TextInstance, // 9. PublicInstance
|
||||
object, // 10. HostContext
|
||||
never, // 11. ChildSet
|
||||
NodeJS.Timeout, // 12. TimeoutHandle
|
||||
number, // 13. NoTimeout
|
||||
null // 14. TransitionStatus
|
||||
> = {
|
||||
getPublicInstance(instance) {
|
||||
return instance;
|
||||
|
@ -177,7 +175,7 @@ export const hostConfig: HostConfig<
|
|||
}
|
||||
},
|
||||
|
||||
createInstance(type, props, root, hostContext, internalInstanceHandle) {
|
||||
createInstance(type, props, root, hostContext, internalInstanceHandle: OpaqueHandle) {
|
||||
const componentType =
|
||||
typeof type === 'string' ? type : type.displayName || type.name || 'Anonymous';
|
||||
const id = getNextInstanceId();
|
||||
|
@ -193,7 +191,7 @@ export const hostConfig: HostConfig<
|
|||
namedChildren
|
||||
};
|
||||
|
||||
internalInstanceHandle.stateNode = instance;
|
||||
(internalInstanceHandle as Fiber).stateNode = instance;
|
||||
instances.set(id, instance);
|
||||
|
||||
addToCommitBuffer({
|
||||
|
@ -212,7 +210,10 @@ export const hostConfig: HostConfig<
|
|||
const id = getNextInstanceId();
|
||||
const instance: TextInstance = { id, type: 'TEXT', text };
|
||||
instances.set(id, instance);
|
||||
addToCommitBuffer({ type: 'CREATE_TEXT_INSTANCE', payload: instance });
|
||||
addToCommitBuffer({
|
||||
type: 'CREATE_TEXT_INSTANCE',
|
||||
payload: { id: instance.id, type: instance.type, text: instance.text }
|
||||
});
|
||||
return instance;
|
||||
},
|
||||
|
||||
|
@ -256,8 +257,8 @@ export const hostConfig: HostConfig<
|
|||
},
|
||||
|
||||
scheduleTimeout: setTimeout,
|
||||
cancelTimeout: (id) => clearTimeout(id as NodeJS.Timeout),
|
||||
noTimeout: -1 as unknown as NodeJS.Timeout,
|
||||
cancelTimeout: (id) => clearTimeout(id),
|
||||
noTimeout: -1,
|
||||
|
||||
isPrimaryRenderer: true,
|
||||
supportsMutation: true,
|
||||
|
@ -282,7 +283,7 @@ export const hostConfig: HostConfig<
|
|||
resolveUpdatePriority: () => 1,
|
||||
maySuspendCommit: () => false,
|
||||
NotPendingTransition: null,
|
||||
HostTransitionContext: React.createContext(0),
|
||||
HostTransitionContext: React.createContext(null) as unknown as ReactContext<null>,
|
||||
|
||||
resetFormInstance: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Reconciler, { type RootTag } from 'react-reconciler';
|
||||
import Reconciler from 'react-reconciler';
|
||||
import type React from 'react';
|
||||
import { root } from './state';
|
||||
import { hostConfig } from './hostConfig';
|
||||
|
@ -13,11 +13,12 @@ const onRecoverableError = (error: Error) => {
|
|||
};
|
||||
|
||||
export const container = reconciler.createContainer(
|
||||
root as unknown as RootTag,
|
||||
0,
|
||||
root,
|
||||
0, // LegacyRoot
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
|
||||
'',
|
||||
onRecoverableError,
|
||||
null
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { AnyInstance, Commit, Container } from './types';
|
||||
import type { AnyInstance, Container } from './types';
|
||||
import type { Command } from '@raycast-linux/protocol';
|
||||
|
||||
export const instances = new Map<number, AnyInstance>();
|
||||
export const root: Container = { id: 'root', children: [] };
|
||||
|
@ -6,12 +7,12 @@ export const root: Container = { id: 'root', children: [] };
|
|||
let instanceCounter = 0;
|
||||
export const getNextInstanceId = (): number => ++instanceCounter;
|
||||
|
||||
export let commitBuffer: Commit[] = [];
|
||||
export let commitBuffer: Command[] = [];
|
||||
|
||||
export const clearCommitBuffer = (): void => {
|
||||
commitBuffer = [];
|
||||
};
|
||||
|
||||
export const addToCommitBuffer = (commit: Commit): void => {
|
||||
export const addToCommitBuffer = (commit: Command): void => {
|
||||
commitBuffer.push(commit);
|
||||
};
|
||||
|
|
|
@ -30,11 +30,6 @@ export type AnyInstance = RaycastInstance | TextInstance;
|
|||
export type ParentInstance = RaycastInstance | Container;
|
||||
export type UpdatePayload = Record<string, unknown>;
|
||||
|
||||
export interface Commit {
|
||||
type: string;
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
export interface SerializedReactElement {
|
||||
$$typeof: 'react.element.serialized';
|
||||
type: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import type { ComponentType, Commit, SerializedReactElement, ParentInstance } from './types';
|
||||
import type { ComponentType, ParentInstance } from './types';
|
||||
import { root, instances } from './state';
|
||||
import { writeLog } from './io';
|
||||
import type { Command } from '@raycast-linux/protocol';
|
||||
|
||||
export const getComponentDisplayName = (type: ComponentType): string => {
|
||||
if (typeof type === 'string') {
|
||||
|
@ -10,17 +10,6 @@ export const getComponentDisplayName = (type: ComponentType): string => {
|
|||
return type.displayName ?? type.name ?? 'Anonymous';
|
||||
};
|
||||
|
||||
const isSerializableReactElement = (value: unknown): value is React.ReactElement =>
|
||||
React.isValidElement(value);
|
||||
|
||||
function serializeReactElement(element: React.ReactElement): SerializedReactElement {
|
||||
return {
|
||||
$$typeof: 'react.element.serialized',
|
||||
type: getComponentDisplayName(element.type as ComponentType),
|
||||
props: serializeProps(element.props as Record<string, unknown>)
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeProps(props: Record<string, unknown>): Record<string, unknown> {
|
||||
const serialized: Record<string, unknown> = {};
|
||||
|
||||
|
@ -63,19 +52,17 @@ export function serializeProps(props: Record<string, unknown>): Record<string, u
|
|||
return serialized;
|
||||
}
|
||||
|
||||
export function optimizeCommitBuffer(buffer: Commit[]): Commit[] {
|
||||
export function optimizeCommitBuffer(buffer: Command[]): Command[] {
|
||||
const OPTIMIZATION_THRESHOLD = 10;
|
||||
const childOpsByParent = new Map<ParentInstance['id'], Commit[]>();
|
||||
const otherOps: Commit[] = [];
|
||||
const childOpsByParent = new Map<ParentInstance['id'], Command[]>();
|
||||
const otherOps: Command[] = [];
|
||||
|
||||
for (const op of buffer) {
|
||||
const { type, payload } = op;
|
||||
const parentId = (payload as { parentId?: ParentInstance['id'] })?.parentId;
|
||||
|
||||
const isChildOp =
|
||||
type === 'APPEND_CHILD' || type === 'REMOVE_CHILD' || type === 'INSERT_BEFORE';
|
||||
op.type === 'APPEND_CHILD' || op.type === 'REMOVE_CHILD' || op.type === 'INSERT_BEFORE';
|
||||
|
||||
if (isChildOp && parentId) {
|
||||
if (isChildOp) {
|
||||
const parentId = op.payload.parentId;
|
||||
childOpsByParent.set(parentId, (childOpsByParent.get(parentId) ?? []).concat(op));
|
||||
} else {
|
||||
otherOps.push(op);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Command, type Child } from '@tauri-apps/plugin-shell';
|
||||
import { Unpackr } from 'msgpackr';
|
||||
import { uiStore } from '$lib/ui.svelte';
|
||||
import { SidecarMessageSchema } from '@raycast-linux/protocol';
|
||||
|
||||
class SidecarService {
|
||||
#sidecarChild: Child | null = $state(null);
|
||||
|
@ -89,13 +90,23 @@ class SidecarService {
|
|||
}
|
||||
};
|
||||
|
||||
#routeMessage = (message: any) => {
|
||||
if (message.type === 'log') {
|
||||
this.#log(`SIDECAR: ${message.payload}`);
|
||||
#routeMessage = (message: unknown) => {
|
||||
const result = SidecarMessageSchema.safeParse(message);
|
||||
|
||||
if (!result.success) {
|
||||
this.#log(`ERROR: Received invalid message from sidecar: ${result.error.message}`);
|
||||
console.error('Invalid sidecar message:', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = message.type === 'BATCH_UPDATE' ? message.payload : [message];
|
||||
const typedMessage = result.data;
|
||||
|
||||
if (typedMessage.type === 'log') {
|
||||
this.#log(`SIDECAR: ${typedMessage.payload}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = typedMessage.type === 'BATCH_UPDATE' ? typedMessage.payload : [typedMessage];
|
||||
if (commands.length > 0) {
|
||||
uiStore.applyCommands(commands);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { UINode } from '$lib/types';
|
||||
import type { Command } from '@raycast-linux/protocol';
|
||||
|
||||
function createUiStore() {
|
||||
// we're not using SvelteMap here because we're making a lot of mutations to the tree
|
||||
|
@ -8,7 +9,7 @@ function createUiStore() {
|
|||
let rootNodeId = $state<number | null>(null);
|
||||
let selectedNodeId = $state<number | undefined>(undefined);
|
||||
|
||||
const applyCommands = (commands: any[]) => {
|
||||
const applyCommands = (commands: Command[]) => {
|
||||
const tempTree = new Map(uiTree);
|
||||
const tempState = { rootNodeId };
|
||||
|
||||
|
@ -42,7 +43,7 @@ function createUiStore() {
|
|||
};
|
||||
|
||||
function processSingleCommand(
|
||||
command: any,
|
||||
command: Command,
|
||||
tempTree: Map<number, UINode>,
|
||||
tempState: { rootNodeId: number | null },
|
||||
getMutableNode: (id: number) => UINode | undefined
|
||||
|
@ -50,7 +51,7 @@ function createUiStore() {
|
|||
switch (command.type) {
|
||||
case 'REPLACE_CHILDREN': {
|
||||
const { parentId, childrenIds } = command.payload;
|
||||
const parentNode = getMutableNode(parentId);
|
||||
const parentNode = getMutableNode(parentId as number);
|
||||
if (parentNode) {
|
||||
parentNode.children = childrenIds;
|
||||
}
|
||||
|
@ -83,7 +84,7 @@ function createUiStore() {
|
|||
if (parentId === 'root') {
|
||||
tempState.rootNodeId = childId;
|
||||
} else {
|
||||
const parentNode = getMutableNode(parentId);
|
||||
const parentNode = getMutableNode(parentId as number);
|
||||
if (parentNode) {
|
||||
const existingIdx = parentNode.children.indexOf(childId);
|
||||
if (existingIdx > -1) parentNode.children.splice(existingIdx, 1);
|
||||
|
@ -94,7 +95,7 @@ function createUiStore() {
|
|||
}
|
||||
case 'REMOVE_CHILD': {
|
||||
const { parentId, childId } = command.payload;
|
||||
const parentNode = getMutableNode(parentId);
|
||||
const parentNode = getMutableNode(parentId as number);
|
||||
if (parentNode) {
|
||||
const index = parentNode.children.indexOf(childId);
|
||||
if (index > -1) parentNode.children.splice(index, 1);
|
||||
|
@ -103,7 +104,7 @@ function createUiStore() {
|
|||
}
|
||||
case 'INSERT_BEFORE': {
|
||||
const { parentId, childId, beforeId } = command.payload;
|
||||
const parentNode = getMutableNode(parentId);
|
||||
const parentNode = getMutableNode(parentId as number);
|
||||
if (parentNode) {
|
||||
const oldIndex = parentNode.children.indexOf(childId);
|
||||
if (oldIndex > -1) parentNode.children.splice(oldIndex, 1);
|
||||
|
@ -118,8 +119,12 @@ function createUiStore() {
|
|||
}
|
||||
case 'CREATE_TEXT_INSTANCE':
|
||||
break;
|
||||
case 'UPDATE_TEXT':
|
||||
break;
|
||||
case 'CLEAR_CONTAINER':
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown command type in ui.store:', command.type);
|
||||
console.warn('Unknown command type in ui.store:', (command as Command).type);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, searchForWorkspaceRoot } from 'vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
|
@ -28,6 +28,9 @@ export default defineConfig(async () => ({
|
|||
watch: {
|
||||
// 3. tell vite to ignore watching `src-tauri`
|
||||
ignored: ['**/src-tauri/**']
|
||||
},
|
||||
fs: {
|
||||
allow: [searchForWorkspaceRoot(process.cwd())]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue