>(): T {
- return getEntrypointPreferences()
+export function showHud(display: string): void {
+ return showHudWindow(display)
}
-export interface GeneratedCommand {
- id: string
+export interface GeneratedEntrypoint {
name: string
- icon: ArrayBuffer | undefined
- fn: () => void
+ actions: GeneratedEntrypointAction[]
+ icon?: ArrayBuffer
+ accessories?: GeneratedEntrypointAccessory[]
}
+export type GeneratedEntrypointAction = GeneratedEntrypointActionRun | GeneratedEntrypointActionView
+
+export interface GeneratedEntrypointActionRun {
+ ref?: string
+ label: string
+ run: () => void
+}
+
+export interface GeneratedEntrypointActionView {
+ ref?: string
+ label: string
+ view: FC
+}
+
+export type GeneratedEntrypointAccessory = GeneratedEntrypointTextAccessory | GeneratedEntrypointIconAccessory;
+
+export interface GeneratedEntrypointTextAccessory {
+ text: string
+ icon?: string
+ tooltip?: string
+}
+
+export interface GeneratedEntrypointIconAccessory {
+ icon: string
+ tooltip?: string
+}
+
+export type GeneratorContext = {
+ add: (id: string, data: GeneratedEntrypoint) => void,
+ remove: (id: string) => void,
+ get: (id: string) => GeneratedEntrypoint | undefined
+ getAll: () => { [id: string]: GeneratedEntrypoint },
+ pluginPreferences: P,
+ entrypointPreferences: E,
+};
+
+export type CommandContext
= {
+ pluginPreferences: P,
+ entrypointPreferences: E,
+};
+
export const Clipboard: Clipboard = {
- read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }> {
- const data = await InternalApi.clipboard_read();
+ read: async function (): Promise<{ "text/plain"?: string | undefined; "image/png"?: ArrayBuffer | undefined; }> {
+ const data = await clipboard_read();
- const result: { "text/plain"?: string; "image/png"?: Blob; } = {};
+ const result: { "text/plain"?: string; "image/png"?: ArrayBuffer; } = {};
if (data.text_data) {
result["text/plain"] = data.text_data;
}
if (data.png_data) {
- result["image/png"] = data.png_data; // TODO arraybuffer? fix when migrating to deno's op2
+ result["image/png"] = data.png_data;
}
return result
},
readText: async function (): Promise {
- return await InternalApi.clipboard_read_text()
+ return await clipboard_read_text()
},
- write: async function (data: { "text/plain"?: string | undefined; "image/png"?: Blob | undefined; }): Promise {
+ write: async function (data: { "text/plain"?: string | undefined; "image/png"?: ArrayBuffer | undefined; }): Promise {
const text_data = data["text/plain"];
const png_data = data["image/png"];
- return await InternalApi.clipboard_write({
- text_data: text_data,
- png_data: png_data != undefined ? Array.from(new Uint8Array(png_data as any)) : undefined, // TODO arraybuffer? fix when migrating to deno's op2
- })
+
+ const write_data: { text_data?: string, png_data?: ArrayBuffer } = {};
+
+ if (text_data) {
+ write_data.text_data = text_data;
+ }
+
+ if (png_data) {
+ write_data.png_data = png_data;
+ }
+
+ return await clipboard_write(write_data)
},
writeText: async function (data: string): Promise {
- return await InternalApi.clipboard_write_text(data)
+ return await clipboard_write_text(data)
},
clear: async function (): Promise {
- await InternalApi.clipboard_clear()
+ await clipboard_clear()
}
}
export interface Clipboard {
- read(): Promise<{ ["text/plain"]?: string, ["image/png"]?: Blob }>;
+ read(): Promise<{ ["text/plain"]?: string, ["image/png"]?: ArrayBuffer }>;
readText(): Promise;
- write(data: { ["text/plain"]?: string, ["image/png"]?: Blob }): Promise;
+ write(data: { ["text/plain"]?: string, ["image/png"]?: ArrayBuffer }): Promise;
writeText(data: string): Promise;
clear(): Promise;
}
+
+export const Environment: Environment = {
+ get gauntletVersion(): number {
+ return environment_gauntlet_version()
+ },
+ get isDevelopment(): boolean {
+ return environment_is_development()
+ },
+ get pluginDataDir(): string {
+ return environment_plugin_data_dir()
+ },
+ get pluginCacheDir(): string {
+ return environment_plugin_cache_dir()
+ },
+}
+
+export interface Environment {
+ get gauntletVersion(): number;
+ get isDevelopment(): boolean;
+ get pluginDataDir(): string;
+ get pluginCacheDir(): string;
+}
+
diff --git a/js/api/src/hooks.ts b/js/api/src/hooks.ts
index 8e6c399..a68d87b 100644
--- a/js/api/src/hooks.ts
+++ b/js/api/src/hooks.ts
@@ -1,6 +1,6 @@
-import { ReactNode } from 'react';
+import { ReactNode, useRef, useId, useState, useCallback, useEffect, MutableRefObject, Dispatch, SetStateAction } from 'react';
// @ts-ignore TODO how to add declaration for this?
-import { useGauntletContext } from "gauntlet:renderer";
+import { useGauntletContext } from "ext:gauntlet/renderer.js";
export function useNavigation(): { popView: () => void, pushView: (component: ReactNode) => void } {
const { popView, pushView }: { popView: () => void, pushView: (component: ReactNode) => void } = useGauntletContext();
@@ -14,3 +14,389 @@ export function useNavigation(): { popView: () => void, pushView: (component: Re
}
}
}
+
+export function usePluginPreferences>(): T {
+ const { pluginPreferences }: { pluginPreferences: () => T } = useGauntletContext();
+
+ return pluginPreferences()
+}
+
+export function useEntrypointPreferences>(): T {
+ const { entrypointPreferences }: { entrypointPreferences: () => T } = useGauntletContext();
+
+ return entrypointPreferences()
+}
+
+export type AsyncState = {
+ isLoading: boolean;
+ error?: unknown;
+ data?: T;
+};
+
+export type MutatePromiseFn = (
+ asyncUpdate: Promise,
+ options?: {
+ optimisticUpdate?: (data: T | undefined) => T; // undefined, if options.execute is false and function was never called, needs to be pure
+ rollbackOnError?: boolean | ((data: T | undefined) => T); // only used if optimisticUpdate is specified, needs to be pure
+ shouldRevalidateAfter?: boolean; // only matters for successful updates
+ },
+) => Promise;
+
+export function usePromise(
+ fn: (...args: Args) => Promise,
+ args?: Args,
+ options?: {
+ abortable?: MutableRefObject;
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: Return) => void;
+ onWillExecute?: (...args: Args) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+} {
+ const execute = options?.execute !== false; // execute by default
+
+ const [state, setState] = useState>({ isLoading: execute });
+
+ return usePromiseInternal(
+ fn,
+ state,
+ setState,
+ args || ([] as any),
+ execute,
+ options?.abortable,
+ options?.onError,
+ options?.onData,
+ options?.onWillExecute
+ )
+}
+
+export function useCachedPromise(
+ fn: (...args: Args) => Promise,
+ args?: Args,
+ options?: {
+ initialState?: Return | (() => Return),
+ abortable?: MutableRefObject;
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: Return) => void;
+ onWillExecute?: (...args: Args) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+} {
+ const execute = options?.execute !== false; // execute by default
+
+ const id = useId();
+
+ const { entrypointId }: { entrypointId: () => string } = useGauntletContext();
+
+ // same store is fetched and updated between command runs
+ const [state, setState] = useCache>("useCachedPromise" + entrypointId() + id, (): AsyncState => {
+ const initialState = options?.initialState;
+ if (initialState) {
+ if (initialState instanceof Function) {
+ return { isLoading: execute, data: initialState() }
+ } else {
+ return { isLoading: execute, data: initialState }
+ }
+ } else {
+ return { isLoading: execute }
+ }
+ });
+
+ return usePromiseInternal(
+ fn,
+ state,
+ setState,
+ args || ([] as any),
+ execute,
+ options?.abortable,
+ options?.onError,
+ options?.onData,
+ options?.onWillExecute
+ )
+}
+
+function usePromiseInternal(
+ fn: (...args: Args) => Promise,
+ state: AsyncState,
+ setState: Dispatch>>,
+ args: Args,
+ execute: boolean,
+ abortable?: MutableRefObject,
+ onError?: (error: unknown) => void,
+ onData?: (data: Return) => void,
+ onWillExecute?: (...args: Args) => void,
+): AsyncState & {
+ revalidate: () => void; // will execute even if options.execute is false
+ mutate: MutatePromiseFn; // will execute even if options.execute is false
+} {
+
+ const promiseRef = useRef>();
+
+ useEffect(() => {
+ return () => {
+ abortable?.current?.abort();
+ };
+ }, [abortable]);
+
+ const callback = useCallback(async (...args: Args): Promise => {
+ if (abortable) {
+ abortable.current?.abort();
+ abortable.current = new AbortController()
+ }
+
+ onWillExecute?.(...args);
+
+ const promise = fn(...args);
+
+ setState(prevState => ({ ...prevState, isLoading: true }));
+
+ promiseRef.current = promise;
+
+ let promiseResult: Return;
+ try {
+ promiseResult = await promise;
+ } catch (error) {
+ // We dont want to handle result/error of non-latest function
+ // this approach helps to avoid race conditions
+ if (promise === promiseRef.current) {
+ setState({ error, isLoading: false })
+
+ if (abortable) {
+ abortable.current = undefined;
+ }
+
+ console.error("Error happened when executing promise: ", error)
+
+ onError?.(error);
+ }
+ return
+ }
+
+ // We dont want to handle result/error of non-latest function
+ // this approach helps to avoid race conditions
+ if (promise === promiseRef.current) {
+ setState({ data: promiseResult, isLoading: false });
+
+ if (abortable) {
+ abortable.current = undefined;
+ }
+
+ onData?.(promiseResult)
+ }
+ }, args);
+
+ useEffect(() => {
+ if (execute) {
+ callback(...args);
+ }
+ }, [callback, execute]);
+
+ return {
+ revalidate: () => {
+ callback(...args);
+ },
+ mutate: async (
+ asyncUpdate: Promise,
+ options?: {
+ optimisticUpdate?: (data: Return | undefined) => Return;
+ rollbackOnError?: boolean | ((data: Return | undefined) => Return);
+ shouldRevalidateAfter?: boolean;
+ },
+ ): Promise => {
+ const prevData = state.data;
+
+ const optimisticUpdate = options?.optimisticUpdate;
+ const rollbackOnError = options?.rollbackOnError;
+ const shouldRevalidateAfter = options?.shouldRevalidateAfter !== false;
+
+ if (optimisticUpdate) {
+ const newData = optimisticUpdate(state.data);
+ setState({ data: newData, isLoading: true })
+
+ try {
+ const asyncUpdateResult = await asyncUpdate;
+
+ if (shouldRevalidateAfter) {
+ callback(...args);
+ } else {
+ // set loading false, only when not revalidating, because revalidate will unset it itself
+ setState(prevState => ({ ...prevState, isLoading: false }));
+ }
+
+ return asyncUpdateResult
+ } catch (e) {
+ switch (typeof rollbackOnError) {
+ case "undefined": {
+ setState({ data: prevData, isLoading: false })
+ break;
+ }
+ case "boolean": {
+ if (rollbackOnError) {
+ setState({ data: prevData, isLoading: false })
+ }
+ break;
+ }
+ case "function": {
+ const rolledBackData = rollbackOnError(state.data);
+ setState({ data: rolledBackData, isLoading: false })
+ break;
+ }
+ }
+
+ throw e
+ }
+ } else {
+ setState(prevState => ({ ...prevState, isLoading: true }));
+
+ const asyncUpdateResult = await asyncUpdate;
+
+ if (shouldRevalidateAfter) {
+ callback(...args);
+ } else {
+ // set loading false, only when not revalidating, because revalidate will unset it itself
+ setState(prevState => ({ ...prevState, isLoading: false }));
+ }
+
+ return asyncUpdateResult
+ }
+ },
+ ...state
+ };
+}
+
+// persistent, uses localStorage under the hood
+export function useStorage(key: string, initialState: T | (() => T)): [T, Dispatch>] {
+ return useWebStorage(key, initialState, localStorage)
+}
+
+// ephemeral, uses sessionStorage under the hood
+export function useCache(key: string, initialState: T | (() => T)): [T, Dispatch>] {
+ return useWebStorage(key, initialState, sessionStorage)
+}
+
+// keys are shared per plugin, across all entrypoints
+// uses JSON.serialize
+function useWebStorage(
+ key: string,
+ initialState: T | (() => T),
+ storageObject: Storage
+): [T, Dispatch>] {
+
+ const [value, setValue] = useState(() => {
+ const jsonValue = storageObject.getItem(key)
+
+ if (jsonValue != null) {
+ return JSON.parse(jsonValue) as T
+ }
+
+ if (initialState instanceof Function) {
+ return initialState()
+ } else {
+ return initialState
+ }
+ })
+
+ useEffect(() => {
+ if (value === undefined) {
+ storageObject.removeItem(key)
+ return
+ }
+ storageObject.setItem(key, JSON.stringify(value))
+ }, [key, value, storageObject])
+
+ return [value, setValue]
+}
+
+export function useFetch(
+ url: RequestInfo | URL,
+ options?: {
+ request?: RequestInit,
+ parse?: (response: Response) => T | Promise;
+ initialState?: T | (() => T),
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: T) => void;
+ onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+};
+export function useFetch(
+ url: RequestInfo | URL,
+ options: {
+ request?: RequestInit,
+ parse?: (response: Response) => V | Promise;
+ map: (result: V) => T | Promise;
+ initialState?: T | (() => T),
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: T) => void;
+ onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+};
+export function useFetch(
+ url: RequestInfo | URL,
+ options?: {
+ request?: RequestInit,
+ parse?: (response: Response) => V | Promise;
+ map?: (result: V) => T | Promise;
+ initialState?: T | V | (() => T | V),
+ execute?: boolean;
+ onError?: (error: unknown) => void;
+ onData?: (data: T | V) => void;
+ onWillExecute?: (input: RequestInfo | URL, init?: RequestInit) => void;
+ },
+): AsyncState & {
+ revalidate: () => void;
+ mutate: MutatePromiseFn;
+} {
+ const abortable = useRef();
+
+ return useCachedPromise(
+ async (inputParam: RequestInfo | URL): Promise => {
+ const response = await fetch(inputParam, { ...options?.request, signal: abortable.current?.signal });
+
+ if (options?.parse) {
+ const parsed: V = await options?.parse(response)
+
+ if (options?.map) {
+ return options?.map(parsed)
+ } else {
+ return parsed
+ }
+ } else {
+ const content = response.headers.get("content-type");
+ if (!content || !content.includes("application/json")) {
+ throw new Error("Content-Type is not 'application/json', please specify custom options.parse")
+ }
+
+ const parsed: V = await response.json()
+
+ if (options?.map) {
+ return options?.map(parsed)
+ } else {
+ return parsed
+ }
+ }
+ },
+ [url],
+ {
+ initialState: options?.initialState,
+ abortable,
+ execute: options?.execute,
+ onError: options?.onError,
+ onData: options?.onData,
+ onWillExecute: options?.onWillExecute,
+ }
+ )
+}
diff --git a/js/api_build/package.json b/js/api_build/package.json
index 813d333..6d732f8 100644
--- a/js/api_build/package.json
+++ b/js/api_build/package.json
@@ -4,12 +4,12 @@
"type": "module",
"scripts": {
"build": "npm run generate-json && npm run build-generator && npm run run-generator",
- "generate-json": "cd ../.. && cargo run --package component_model -- ./js/api_build/component_model.json",
+ "generate-json": "cd ../.. && cargo run --package gauntlet-component-model -- ./js/api_build/component_model.json",
"build-generator": "tsc",
"run-generator": "node dist/index.js"
},
"devDependencies": {
- "@types/node": "^18.17.1",
- "typescript": "^5.3.3"
+ "@types/node": "^22.10.2",
+ "typescript": "^5.7.2"
}
}
diff --git a/js/api_build/src/index.ts b/js/api_build/src/index.ts
index 7322513..dae197c 100644
--- a/js/api_build/src/index.ts
+++ b/js/api_build/src/index.ts
@@ -241,31 +241,6 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
const root = modelInput.find((component): component is RootComponent => component.type === "root");
if (root != null) {
- // image special case
- // export type ImageSource = { asset: string } | { url: string };
-
- const imageSourceDeclaration = ts.factory.createTypeAliasDeclaration(
- [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
- ts.factory.createIdentifier("ImageSource"),
- undefined,
- ts.factory.createUnionTypeNode([
- ts.factory.createTypeLiteralNode([ts.factory.createPropertySignature(
- undefined,
- ts.factory.createIdentifier("asset"),
- undefined,
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
- )]),
- ts.factory.createTypeLiteralNode([ts.factory.createPropertySignature(
- undefined,
- ts.factory.createIdentifier("url"),
- undefined,
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
- )])
- ])
- );
-
- publicDeclarations.push(imageSourceDeclaration)
-
for (const [name, sharedType] of Object.entries(root.sharedTypes)) {
switch (sharedType.type) {
@@ -295,7 +270,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
undefined,
ts.factory.createIdentifier(propName),
undefined,
- makeType(type)
+ makeType(type, "no")
)
})
)
@@ -304,6 +279,19 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
publicDeclarations.push(declaration)
break;
}
+ case "union": {
+ const declaration = ts.factory.createTypeAliasDeclaration(
+ [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
+ ts.factory.createIdentifier(name),
+ undefined,
+ ts.factory.createUnionTypeNode(
+ sharedType.items.map(type => makeType(type, "no"))
+ )
+ )
+
+ publicDeclarations.push(declaration)
+ break;
+ }
default: {
throw new Error("unreachable");
}
@@ -354,7 +342,7 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
const properties = component.props
.map(prop => {
- if (prop.type.type === "component") {
+ if (!isInProperty(prop.type)) {
return null
}
@@ -372,22 +360,23 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
.filter((prop): prop is ts.JsxAttribute => prop != null);
const children = []
- if (component.children.type != "none") {
- const componentProps = component.props.filter(prop => prop.type.type === "component");
- if (componentProps.length !== 0) {
- children.push(
- ...componentProps.map(prop => (
- ts.factory.createAsExpression(
- ts.factory.createPropertyAccessExpression(
- ts.factory.createIdentifier("props"),
- ts.factory.createIdentifier(prop.name)
- ),
- ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)
- )
- ))
- );
- }
+ const componentProps = component.props.filter(prop => !isInProperty(prop.type));
+ if (componentProps.length !== 0) {
+ children.push(
+ ...componentProps.map(prop => (
+ ts.factory.createAsExpression(
+ ts.factory.createPropertyAccessExpression(
+ ts.factory.createIdentifier("props"),
+ ts.factory.createIdentifier(prop.name)
+ ),
+ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)
+ )
+ ))
+ );
+ }
+
+ if (component.children.type != "none") {
children.push(ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier("props"),
ts.factory.createIdentifier("children")
@@ -406,10 +395,11 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
let componentType: ts.TypeReferenceNode | ts.IntersectionTypeNode;
if (component.children.type == "members" || component.children.type == "string_or_members") {
+ const members = { ...component.children.ordered_members, ...component.children.per_type_members }
componentType = ts.factory.createIntersectionTypeNode([
componentFCType,
ts.factory.createTypeLiteralNode(
- Object.entries(component.children.members).map(([memberName, member]) => {
+ Object.entries(members).map(([memberName, member]) => {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier(memberName),
@@ -431,7 +421,8 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
switch (component.children.type) {
case "string_or_members":
case "members": {
- memberAssignments = Object.entries(component.children.members).map(([memberName, member]) => {
+ const members = { ...component.children.ordered_members, ...component.children.per_type_members }
+ memberAssignments = Object.entries(members).map(([memberName, member]) => {
return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier(component.name),
@@ -545,22 +536,26 @@ function makeComponents(modelInput: Component[]): ts.SourceFile {
function makePropertyTypes(component: StandardComponent, componentPropsInChildren: boolean): ts.TypeElement[] {
const props = component.props
- .filter(property => property.type.type === "component" ? !componentPropsInChildren : true)
+ .filter(property => !isInProperty(property.type) ? !componentPropsInChildren : true)
.map(property => {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier(property.name),
- !property.optional ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken),
- makeType(property.type)
+ property.optional == "no" ? undefined : ts.factory.createToken(ts.SyntaxKind.QuestionToken),
+ makeType(property.type, property.optional)
)
});
- const additionalComponentRefs = component.props
- .map(property => property.type)
- .filter((type): type is TypeComponent => componentPropsInChildren && type.type === "component")
- .map(type => type.reference)
+ let additionalComponentRefs: ComponentRef[];
+ if (componentPropsInChildren) {
+ additionalComponentRefs = component.props
+ .map(property => property.type)
+ .flatMap(type => collectAllComponentRefs(type));
+ } else {
+ additionalComponentRefs = [];
+ }
- if (component.children.type != "none") {
+ if (component.children.type != "none" || additionalComponentRefs.length > 0) {
props.unshift(ts.factory.createPropertySignature(
undefined,
ts.factory.createIdentifier("children"),
@@ -576,11 +571,13 @@ function makePropertyTypes(component: StandardComponent, componentPropsInChildre
function makeChildrenType(type: Children, additionalComponentRefs: ComponentRef[]): ts.TypeNode {
switch (type.type) {
case "members": {
+ const members = { ...type.ordered_members, ...type.per_type_members }
+
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("ElementComponent"),
[
ts.factory.createUnionTypeNode(
- [...additionalComponentRefs, ...Object.values(type.members)].map(member => (
+ [...additionalComponentRefs, ...Object.values(members)].map(member => (
ts.factory.createTypeQueryNode(
ts.factory.createIdentifier(member.componentName),
undefined
@@ -591,11 +588,13 @@ function makeChildrenType(type: Children, additionalComponentRefs: ComponentRef[
)
}
case "string_or_members": {
+ const members = { ...type.ordered_members, ...type.ordered_members }
+
return ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("StringOrElementComponent"),
[
ts.factory.createUnionTypeNode(
- [...additionalComponentRefs, ...Object.values(type.members)].map(member => (
+ [...additionalComponentRefs, ...Object.values(members)].map(member => (
ts.factory.createTypeQueryNode(
ts.factory.createIdentifier(member.componentName),
undefined
@@ -612,43 +611,68 @@ function makeChildrenType(type: Children, additionalComponentRefs: ComponentRef[
)
}
case "none": {
- throw new Error("Cannot construct none children")
+ if (additionalComponentRefs.length > 0) {
+ return ts.factory.createTypeReferenceNode(
+ ts.factory.createIdentifier("ElementComponent"),
+ [
+ ts.factory.createUnionTypeNode(
+ additionalComponentRefs.map(member => (
+ ts.factory.createTypeQueryNode(
+ ts.factory.createIdentifier(member.componentName),
+ undefined
+ )
+ ))
+ )
+ ]
+ )
+ } else {
+ throw new Error("Cannot construct none children")
+ }
}
}
}
-function makeType(type: PropertyType): ts.TypeNode {
+function makeType(type: PropertyType, optional: Property["optional"]): ts.TypeNode {
+ let result: ts.TypeNode
switch (type.type) {
case "boolean": {
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword)
+ result = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword)
+ break;
}
case "number": {
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
+ result = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
+ break;
}
case "string": {
- return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
+ result = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
+ break;
}
case "function": {
const params = type.arguments.map(arg => {
+ if (arg.optional != "no" && arg.optional != "yes") {
+ throw new Error("following optional type is not supported here: " + arg.optional)
+ }
+
return ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier(arg.name),
undefined,
- !arg.optional ? makeType(arg.type) : ts.factory.createUnionTypeNode([makeType(arg.type), ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword)]),
+ arg.optional == "no" ? makeType(arg.type, "no") : ts.factory.createUnionTypeNode([makeType(arg.type, arg.optional), ts.factory.createLiteralTypeNode(ts.factory.createNull())]),
undefined
)
});
- return ts.factory.createFunctionTypeNode(
+ result = ts.factory.createFunctionTypeNode(
undefined,
params,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)
)
+ break;
}
case "component": {
- return ts.factory.createTypeReferenceNode(
+ result = ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier("ElementComponent"),
[
ts.factory.createTypeQueryNode(
@@ -657,33 +681,103 @@ function makeType(type: PropertyType): ts.TypeNode {
)
]
)
+ break;
}
- case "image_source": {
- return ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier("ImageSource"),
- undefined
- )
+ case "array": {
+ result = ts.factory.createArrayTypeNode(makeType(type.item, "no"))
+ break;
}
- case "enum": {
- return ts.factory.createTypeReferenceNode(
- ts.factory.createIdentifier(type.name),
- undefined
- )
- }
- case "object": {
- return ts.factory.createTypeReferenceNode(
+ case "shared_type_ref": {
+ result = ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier(type.name),
undefined
)
+ break;
}
case "union": {
- return ts.factory.createUnionTypeNode(type.items.map(value => makeType(value)))
+ result = ts.factory.createUnionTypeNode(type.items.map(value => makeType(value, "no")))
+ break
}
default: {
throw new Error(`unsupported type ${JSON.stringify(type)}`)
}
}
+ if (optional == "yes_but_complicated") {
+ return ts.factory.createUnionTypeNode([result, ts.factory.createLiteralTypeNode(ts.factory.createNull())]);
+ } else {
+ return result
+ }
+}
+
+function isInProperty(propertyType: PropertyType) {
+ switch (propertyType.type) {
+ case "boolean": {
+ return true
+ }
+ case "number": {
+ return true
+ }
+ case "string": {
+ return true
+ }
+ case "function": {
+ return true // different from the rust side
+ }
+ case "component": {
+ return false
+ }
+ case "array": {
+ return isInProperty(propertyType.item)
+ }
+ case "shared_type_ref": {
+ return true
+ }
+ case "union": {
+ if (propertyType.items.every(value => isInProperty(value))) {
+ return true
+ } else if (propertyType.items.every(value => !isInProperty(value))) {
+ return false
+ } else {
+ throw new Error("")
+ }
+ }
+ default: {
+ throw new Error(`unsupported type ${JSON.stringify(propertyType)}`)
+ }
+ }
+}
+
+function collectAllComponentRefs(propertyType: PropertyType): ComponentRef[] {
+ switch (propertyType.type) {
+ case "boolean": {
+ return []
+ }
+ case "number": {
+ return []
+ }
+ case "string": {
+ return []
+ }
+ case "function": {
+ return []
+ }
+ case "component": {
+ return [propertyType.reference]
+ }
+ case "array": {
+ return collectAllComponentRefs(propertyType.item)
+ }
+ case "shared_type_ref": {
+ return []
+ }
+ case "union": {
+ return propertyType.items.flatMap(value => collectAllComponentRefs(value))
+ }
+ default: {
+ throw new Error(`unsupported type ${JSON.stringify(propertyType)}`)
+ }
+ }
}
const genDir = "../api/src/gen";
@@ -691,4 +785,4 @@ if (!existsSync(genDir)) {
mkdirSync(genDir);
}
-generate("./component_model.json", `${genDir}/components.tsx`)
\ No newline at end of file
+generate("./component_model.json", `${genDir}/components.tsx`)
diff --git a/js/bridge_build/.gitignore b/js/bridge_build/.gitignore
new file mode 100644
index 0000000..d77bc98
--- /dev/null
+++ b/js/bridge_build/.gitignore
@@ -0,0 +1,2 @@
+component_model.json
+dist
\ No newline at end of file
diff --git a/js/bridge_build/package.json b/js/bridge_build/package.json
new file mode 100644
index 0000000..dfbc31d
--- /dev/null
+++ b/js/bridge_build/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@project-gauntlet/bridge-build",
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "build": "npm run build-generator && npm run run-generator",
+ "build-generator": "tsc",
+ "run-generator": "node dist/index.js"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.2",
+ "typescript": "^5.7.2"
+ }
+}
diff --git a/js/bridge_build/src/index.ts b/js/bridge_build/src/index.ts
new file mode 100644
index 0000000..111bd0d
--- /dev/null
+++ b/js/bridge_build/src/index.ts
@@ -0,0 +1,218 @@
+import ts, {
+ ExpressionStatement,
+ ImportDeclaration,
+ isExportDeclaration,
+ isExportSpecifier,
+ isIdentifier,
+ isNamedExports,
+ ScriptKind,
+ Statement
+} from "typescript";
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+
+
+function generate(outFile: string, sourceFile: ts.SourceFile) {
+ const resultFile = ts.createSourceFile("unused", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.JS);
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
+
+ const result = printer.printNode(ts.EmitHint.Unspecified, sourceFile, resultFile);
+
+ writeFileSync(outFile, result)
+}
+
+function collectExports(inputFile: string): string[] {
+ const sourceFile = ts.createSourceFile(
+ "input.js",
+ readFileSync(inputFile).toString(),
+ ts.ScriptTarget.ESNext,
+ /*setParentNodes */ false,
+ ScriptKind.JS
+ );
+
+ const result: string[] = []
+
+ for (const statement of sourceFile.statements) {
+ if (isExportDeclaration(statement)) {
+ const exportClause = statement.exportClause;
+ if (exportClause) {
+ if (isNamedExports(exportClause)) {
+ for (const element of exportClause.elements) {
+ if (isExportSpecifier(element)) {
+ if (isIdentifier(element.name)) {
+ if (typeof element.name.escapedText === "string") {
+ if (element.name.escapedText.startsWith("___")) {
+ result.push(element.name.escapedText.slice(1)) // remove one _, typescript special case
+ } else {
+ result.push(element.name.escapedText)
+ }
+ } else {
+ throw new Error(`unexpected export clause element element name type: ${JSON.stringify(element)}`)
+ }
+ } else {
+ throw new Error(`unknown export clause element element name type: ${JSON.stringify(element)}`)
+ }
+ } else {
+ throw new Error(`unknown export clause element: ${JSON.stringify(element)}`)
+ }
+ }
+ } else {
+ throw new Error(`unknown export clause: ${JSON.stringify(exportClause)}`)
+ }
+ }
+ }
+ }
+
+ return result
+}
+
+
+function generateInternal(exportConfig: Record): ts.SourceFile {
+
+ function createImport(exports: string[], namespace:string, importString: string): ImportDeclaration {
+ return ts.factory.createImportDeclaration(
+ undefined,
+ ts.factory.createImportClause(
+ false,
+ undefined,
+ ts.factory.createNamedImports(exports.map(value => {
+ return ts.factory.createImportSpecifier(
+ false,
+ ts.factory.createIdentifier(value),
+ ts.factory.createIdentifier(`${namespace}_${value}`)
+ )
+ }))
+ ),
+ ts.factory.createStringLiteral(importString),
+ undefined
+ )
+ }
+
+ const initialDeclarations: Statement[] = Object.entries(exportConfig)
+ .map(([namespace, { importUrl, exports }]) => createImport(exports, namespace, importUrl));
+
+ function createAssignment(namespace: string, variableName: string): ExpressionStatement {
+ return ts.factory.createExpressionStatement(ts.factory.createBinaryExpression(
+ ts.factory.createPropertyAccessExpression(
+ ts.factory.createIdentifier("globalThis"),
+ ts.factory.createIdentifier(`${namespace}_${variableName}`)
+ ),
+ ts.factory.createToken(ts.SyntaxKind.EqualsToken),
+ ts.factory.createIdentifier(`${namespace}_${variableName}`)
+ ))
+ }
+
+ const assignments: Statement[] = Object.entries(exportConfig)
+ .flatMap(([namespace, { exports }]) => exports.map(value => createAssignment(namespace, value)));
+
+ return ts.factory.createSourceFile(
+ [
+ ...initialDeclarations,
+ ...assignments,
+ ],
+ ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
+ ts.NodeFlags.None
+ )
+}
+
+function generateExternal(namespace: string, exports: string[]): ts.SourceFile {
+
+ const assignments = exports.map(value => {
+
+ return ts.factory.createVariableStatement(
+ undefined,
+ ts.factory.createVariableDeclarationList(
+ [ts.factory.createVariableDeclaration(
+ ts.factory.createIdentifier(`${namespace}_${value}`),
+ undefined,
+ undefined,
+ ts.factory.createPropertyAccessExpression(
+ ts.factory.createIdentifier("globalThis"),
+ ts.factory.createIdentifier(`${namespace}_${value}`)
+ )
+ )],
+ ts.NodeFlags.Const
+ )
+ );
+ });
+
+ const exportDeclaration = ts.factory.createExportDeclaration(
+ undefined,
+ false,
+ ts.factory.createNamedExports(exports.map(value => {
+ return ts.factory.createExportSpecifier(
+ false,
+ ts.factory.createIdentifier(`${namespace}_${value}`),
+ ts.factory.createIdentifier(value)
+ )
+ })),
+ undefined,
+ undefined
+ );
+
+ return ts.factory.createSourceFile(
+ [...assignments, exportDeclaration],
+ ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
+ ts.NodeFlags.None
+ )
+}
+
+const outDir = `./dist`;
+
+if (!existsSync(outDir)) {
+ mkdirSync(outDir);
+}
+
+const componentExports = collectExports(`../api/dist/gen/components.js`);
+const helpersExports = collectExports(`../api/dist/helpers.js`);
+const hooksExports = collectExports(`../api/dist/hooks.js`);
+const coreExports = collectExports(`../core/dist/core.js`);
+
+// prod bundle exports are identical and hopefully it stays like this in future
+const reactExports = collectExports(`../react/dist/dev/react.development.js`);
+const reactJsxRuntimeExports = collectExports(`../react/dist/dev/react-jsx-runtime.development.js`);
+
+const internalAllExports = collectExports(`../core/dist/internal-all.js`);
+const internalLinuxExports = collectExports(`../core/dist/internal-linux.js`);
+const internalMacosExports = collectExports(`../core/dist/internal-macos.js`);
+const internalWindowsExports = collectExports(`../core/dist/internal-windows.js`);
+
+generate(
+ `${outDir}/bridge-bootstrap.js`,
+ generateInternal({
+ "GauntletComponents": { importUrl: "ext:gauntlet/api/components.js", exports: componentExports },
+ "GauntletHelpers": { importUrl: "ext:gauntlet/api/helpers.js", exports: helpersExports },
+ "GauntletHooks": { importUrl: "ext:gauntlet/api/hooks.js", exports: hooksExports },
+ "GauntletCore": { importUrl: "ext:gauntlet/core.js", exports: coreExports },
+ "GauntletReact": { importUrl: "ext:gauntlet/react.js", exports: reactExports },
+ "GauntletReactJsxRuntime": { importUrl: "ext:gauntlet/react-jsx-runtime.js", exports: reactJsxRuntimeExports },
+ })
+)
+
+generate(`${outDir}/bridge-internal-all-bootstrap.js`, generateInternal({
+ "GauntletInternalAll": { importUrl: "ext:gauntlet/internal-all.js", exports: internalAllExports }
+}))
+generate(`${outDir}/bridge-internal-linux-bootstrap.js`, generateInternal({
+ "GauntletInternalLinux": { importUrl: "ext:gauntlet/internal-linux.js", exports: internalLinuxExports }
+}))
+generate(`${outDir}/bridge-internal-macos-bootstrap.js`, generateInternal({
+ "GauntletInternalMacos": { importUrl: "ext:gauntlet/internal-macos.js", exports: internalMacosExports }
+}))
+
+generate(`${outDir}/bridge-internal-windows-bootstrap.js`, generateInternal({
+ "GauntletInternalWindows": { importUrl: "ext:gauntlet/internal-windows.js", exports: internalWindowsExports }
+}))
+
+
+generate(`${outDir}/bridge-components.js`, generateExternal("GauntletComponents", componentExports))
+generate(`${outDir}/bridge-helpers.js`, generateExternal("GauntletHelpers", helpersExports))
+generate(`${outDir}/bridge-hooks.js`, generateExternal("GauntletHooks", hooksExports))
+generate(`${outDir}/bridge-core.js`, generateExternal("GauntletCore", coreExports))
+generate(`${outDir}/bridge-react.js`, generateExternal("GauntletReact", reactExports))
+generate(`${outDir}/bridge-react-jsx-runtime.js`, generateExternal("GauntletReactJsxRuntime", reactJsxRuntimeExports))
+
+generate(`${outDir}/bridge-internal-all.js`, generateExternal("GauntletInternalAll", internalAllExports))
+generate(`${outDir}/bridge-internal-linux.js`, generateExternal("GauntletInternalLinux", internalLinuxExports))
+generate(`${outDir}/bridge-internal-macos.js`, generateExternal("GauntletInternalMacos", internalMacosExports))
+generate(`${outDir}/bridge-internal-windows.js`, generateExternal("GauntletInternalWindows", internalWindowsExports))
+
+
diff --git a/js/deno/tsconfig.json b/js/bridge_build/tsconfig.json
similarity index 65%
rename from js/deno/tsconfig.json
rename to js/bridge_build/tsconfig.json
index 2854811..1433d49 100644
--- a/js/deno/tsconfig.json
+++ b/js/bridge_build/tsconfig.json
@@ -5,8 +5,10 @@
"esModuleInterop": true,
"target": "ES2022",
"moduleResolution": "bundler",
- "outDir": "./builddist"
+ "jsx": "react",
+ "types": ["@types/node"],
+ "outDir": "./dist"
},
"lib": ["ES2020"],
- "include": ["./generator"]
+ "include": ["./src"]
}
\ No newline at end of file
diff --git a/js/build/package.json b/js/build/package.json
index 1b1f96c..2b7b24e 100644
--- a/js/build/package.json
+++ b/js/build/package.json
@@ -4,24 +4,28 @@
"build-this": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript",
"build-linux-project": "npm run build-this && node dist/main.js build-linux",
"build-macos-project": "npm run build-this && node dist/main.js build-macos",
+ "build-windows-project": "npm run build-this && node dist/main.js build-windows",
"publish-init-project": "npm run build-this && node dist/main.js publish-init",
"publish-linux-project": "npm run build-this && node dist/main.js publish-linux",
"publish-macos-project": "npm run build-this && node dist/main.js publish-macos",
+ "publish-windows-project": "npm run build-this && node dist/main.js publish-windows",
"publish-final-project": "npm run build-this && node dist/main.js publish-final"
},
"type": "module",
"dependencies": {
- "@actions/core": "^1.10.1",
- "commander": "^11.1.0",
- "octokit": "^3.1.2",
- "simple-git": "^3.22.0"
+ "@actions/core": "^1.11.1",
+ "commander": "^12.1.0",
+ "octokit": "^4.0.2",
+ "simple-git": "^3.27.0",
+ "cross-spawn": "^7.0.6"
},
"devDependencies": {
- "@rollup/plugin-commonjs": "^25.0.7",
- "@rollup/plugin-node-resolve": "^15.2.3",
- "@rollup/plugin-typescript": "^11.1.5",
- "@types/node": "^18.17.1",
- "tslib": "^2.6.2",
- "typescript": "^5.3.3"
+ "@rollup/plugin-commonjs": "^28.0.2",
+ "@rollup/plugin-node-resolve": "^16.0.0",
+ "@rollup/plugin-typescript": "^12.1.2",
+ "@types/node": "^22.10.2",
+ "@types/cross-spawn": "^6.0.6",
+ "tslib": "^2.8.1",
+ "typescript": "^5.7.2"
}
}
diff --git a/js/build/src/main.ts b/js/build/src/main.ts
index a3bbfc2..28d0a37 100644
--- a/js/build/src/main.ts
+++ b/js/build/src/main.ts
@@ -3,11 +3,12 @@ import { readFile, writeFile } from "node:fs/promises";
import { simpleGit } from 'simple-git';
import { EOL } from "node:os";
import { Octokit } from 'octokit';
-import { spawnSync } from "node:child_process";
+import { sync as spawnSync } from "cross-spawn";
import path from "node:path";
import { mkdirSync, readFileSync } from "fs";
-import { copyFileSync, writeFileSync } from "node:fs";
+import { copyFileSync, existsSync, rmdirSync, writeFileSync } from "node:fs";
import * as core from '@actions/core';
+import { SpawnSyncOptions } from "child_process";
const program = new Command();
@@ -30,6 +31,11 @@ program.command('publish-macos')
await doPublishMacOS()
});
+program.command('publish-windows')
+ .action(async () => {
+ await doPublishWindows()
+ });
+
program.command('publish-final')
.action(async () => {
await doPublishFinal()
@@ -45,13 +51,17 @@ program.command('build-macos')
await doBuildMacOS()
});
+program.command('build-windows')
+ .action(async () => {
+ await doBuildWindows()
+ });
+
await program.parseAsync(process.argv);
-async function doBuild(arch: string) {
+async function doBuild(projectRoot: string, arch: string, profile: string) {
console.log("Building Gauntlet...")
- const projectRoot = getProjectRoot();
- build(projectRoot, true, arch)
+ build(projectRoot, arch, profile)
}
async function doPublishInit() {
@@ -59,105 +69,151 @@ async function doPublishInit() {
const projectRoot = getProjectRoot()
- const { newVersion, releaseNotes } = await makeRepoChanges(projectRoot);
+ const githubReleaseId = process.env.PROVIDED_GITHUB_RELEASE_ID;
- await createRelease(newVersion, releaseNotes)
+ if (githubReleaseId) {
+ core.setOutput("github-release-id", `${githubReleaseId}`)
+ } else {
+ const { newVersion, releaseNotes } = await makeRepoChanges(projectRoot);
+
+ const releaseId = await createRelease(newVersion, releaseNotes);
+
+ console.log(`GitHub release id: ${releaseId}`)
+
+ core.setOutput("github-release-id", `${releaseId}`)
+ }
}
async function doPublishLinux() {
- console.log("Publishing Gauntlet... Linux...")
-
const projectRoot = getProjectRoot()
+ const git = simpleGit(projectRoot);
+
+ console.log("git pull...")
+ await git.pull()
+
const arch = 'x86_64-unknown-linux-gnu';
+ const profile = 'release-size';
- build(projectRoot, false, arch)
-
- const { fileName, filePath } = packageForLinux(projectRoot, arch)
+ build(projectRoot, arch, profile)
+ const { fileName, filePath } = packageForLinux(projectRoot, arch, profile)
await addFileToRelease(filePath, fileName)
}
async function doBuildLinux() {
- await doBuild('x86_64-unknown-linux-gnu')
+ const arch = 'x86_64-unknown-linux-gnu';
+ const projectRoot = getProjectRoot();
+ const profile = 'release';
+
+ await doBuild(projectRoot, arch, profile)
+ packageForLinux(projectRoot, arch, profile)
}
async function doPublishMacOS() {
const projectRoot = getProjectRoot();
- const arch = 'aarch64-apple-darwin';
+ const git = simpleGit(projectRoot);
- build(projectRoot, false, arch)
+ console.log("git pull...")
+ await git.pull()
- const { fileName, filePath } = await packageForMacos(projectRoot, arch)
+ const archArm = 'aarch64-apple-darwin';
+ const archIntel = 'x86_64-apple-darwin';
+ const profile = 'release-size';
+
+ buildJs(projectRoot)
+ buildRust(projectRoot, archArm, profile)
+ buildRust(projectRoot, archIntel, profile)
+
+ const { fileName, filePath } = await packageForMacos(
+ projectRoot,
+ [archArm, archIntel],
+ profile,
+ true,
+ true
+ )
await addFileToRelease(filePath, fileName)
}
async function doBuildMacOS() {
- await doBuild('aarch64-apple-darwin')
+ const projectRoot = getProjectRoot();
+ const archArm = 'aarch64-apple-darwin';
+ const archIntel = 'x86_64-apple-darwin';
+ const profile = 'release';
+
+ buildJs(projectRoot)
+ buildRust(projectRoot, archArm, profile)
+ buildRust(projectRoot, archIntel, profile)
+
+ await packageForMacos(
+ projectRoot,
+ [archArm, archIntel],
+ profile,
+ false,
+ false
+ )
}
-async function undraftRelease() {
- const octokit = getOctokit();
+async function doPublishWindows() {
+ const projectRoot = getProjectRoot();
- const response = await octokit.rest.repos.getRelease({
- ...getGithubRepo(),
- release_id: getGithubReleaseId(),
- });
+ const git = simpleGit(projectRoot);
- await octokit.rest.repos.updateRelease({
- ...getGithubRepo(),
- release_id: response.data.id,
- origin: response.data.upload_url,
- draft: false
- });
+ console.log("git pull...")
+ await git.pull()
+
+ const arch = 'x86_64-pc-windows-msvc';
+ const profile = 'release-size';
+
+ build(projectRoot, arch, profile)
+
+ const { fileName, filePath } = await packageForWindows(projectRoot, arch, profile)
+
+ await addFileToRelease(filePath, fileName)
+}
+
+async function doBuildWindows() {
+ const projectRoot = getProjectRoot();
+ const arch = 'x86_64-pc-windows-msvc';
+ const profile = 'release';
+
+ await doBuild(projectRoot, arch, profile)
+ await packageForWindows(projectRoot, arch, profile)
}
async function doPublishFinal() {
- console.log("Publishing Gauntlet... Finishing up...")
const projectRoot = getProjectRoot()
+ const git = simpleGit(projectRoot);
+
+ console.log("git pull...")
+ await git.pull()
+
+ console.log("Publishing Gauntlet npm packages...")
+
buildJs(projectRoot)
- publishNpmPackage(projectRoot)
-
- await undraftRelease()
+ await publishNpmPackage(projectRoot)
}
-function build(projectRoot: string, check: boolean, arch: string) {
+function build(projectRoot: string, arch: string, profile: string) {
buildJs(projectRoot)
- if (check) {
- console.log("Checking rust...")
- const cargoCheckResult = spawnSync('cargo', ['check', '--features', 'release', '--target', arch], {
- stdio: "inherit",
- cwd: projectRoot
- });
-
- if (cargoCheckResult.status !== 0) {
- throw new Error(`Unable to check, status: ${JSON.stringify(cargoCheckResult)}`);
- }
- }
+ buildRust(projectRoot, arch, profile)
+}
+function buildRust(projectRoot: string, arch: string, profile: string) {
console.log("Building rust...")
- const cargoBuildResult = spawnSync('cargo', ['build', '--release', '--features', 'release', '--target', arch], {
- stdio: "inherit",
+ spawnWithErrors('cargo', ['build', '--profile', profile, '--features', 'release', '--target', arch], {
cwd: projectRoot
});
-
- if (cargoBuildResult.status !== 0) {
- throw new Error(`Unable to build rust, status: ${JSON.stringify(cargoBuildResult)}`);
- }
}
function buildJs(projectRoot: string) {
console.log("Building js...")
- const npmRunResult = spawnSync('npm', ['run', 'build'], { stdio: "inherit", cwd: projectRoot });
-
- if (npmRunResult.status !== 0) {
- throw new Error(`Unable to build js, status: ${JSON.stringify(npmRunResult)}`);
- }
+ spawnWithErrors('npm', ['run', 'build'], { cwd: projectRoot });
}
function getProjectRoot(): string {
@@ -225,26 +281,10 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str
console.log("Writing changelog file...")
await writeFile(changelogFilePath, newChangelog.join(EOL))
- const bumpNpmPackage = (packageDir: string) => {
- const npmVersionResult = spawnSync('npm', ['version', `0.${newVersion}.0`], { stdio: "inherit", cwd: packageDir })
-
- if (npmVersionResult.status !== 0) {
- throw new Error(`Unable to run npm version, status: ${JSON.stringify(npmVersionResult)}`);
- }
- }
-
- console.log("Bump version for deno subproject...")
- const denoProjectPath = path.join(projectRoot, "js", "deno");
- bumpNpmPackage(denoProjectPath)
-
- console.log("Bump version for api subproject...")
- const apiProjectPath = path.join(projectRoot, "js", "api");
- bumpNpmPackage(apiProjectPath)
-
console.log("git add all files...")
await git.raw('add', '-A')
console.log("git commit...")
- await git.commit(`Release v${newVersion}`);
+ await git.commit(`Prepare for v${newVersion} release`);
console.log("git add version tag...")
await git.addTag(`v${newVersion}`)
console.log("git push...")
@@ -258,36 +298,62 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str
}
}
-function packageForLinux(projectRoot: string, arch: string): { filePath: string; fileName: string } {
- const releaseDirPath = path.join(projectRoot, 'target', arch, 'release');
- const executableFileName = 'gauntlet';
- const archiveFileName = "gauntlet-x86_64-linux.tar.gz"
- const archiveFilePath = path.join(releaseDirPath, archiveFileName);
+function packageForLinux(projectRoot: string, arch: string, profile: string): { filePath: string; fileName: string } {
+ const releaseDirPath = path.join(projectRoot, 'target', arch, profile);
+ const assetsDirPath = path.join(projectRoot, 'assets', 'linux');
- const tarResult = spawnSync(`tar`, ['-czvf', archiveFileName, executableFileName], {
- stdio: "inherit",
- cwd: releaseDirPath
+ const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet');
+ const sourceDesktopFilePath = path.join(assetsDirPath, 'gauntlet.desktop');
+ const sourceServiceFilePath = path.join(assetsDirPath, 'gauntlet.service');
+ const sourceLogoFilePath = path.join(assetsDirPath, 'icon_256.png');
+
+ const bundleDir = path.join(releaseDirPath, 'archive');
+
+ const targetExecutableFileName = 'gauntlet';
+ const targetExecutableFilePath = path.join(bundleDir, targetExecutableFileName);
+
+ const targetDesktopFileName = 'gauntlet.desktop';
+ const targetDesktopFilePath = path.join(bundleDir, targetDesktopFileName);
+
+ const targetServiceFileName = 'gauntlet.service';
+ const targetServiceFilePath = path.join(bundleDir, targetServiceFileName);
+
+ const targetLogoFileName = 'gauntlet.png';
+ const targetLogoFilePath = path.join(bundleDir, targetLogoFileName);
+
+ const archiveFileName = 'gauntlet-x86_64-linux.tar.gz';
+ const archiveFilePath = path.join(bundleDir, archiveFileName);
+
+ mkdirSync(bundleDir)
+
+ copyFileSync(sourceExecutableFilePath, targetExecutableFilePath)
+ copyFileSync(sourceDesktopFilePath, targetDesktopFilePath)
+ copyFileSync(sourceServiceFilePath, targetServiceFilePath)
+ copyFileSync(sourceLogoFilePath, targetLogoFilePath)
+
+ spawnWithErrors(`tar`, ['-czvf', archiveFileName, targetExecutableFileName, targetDesktopFileName, targetServiceFileName, targetLogoFileName], {
+ cwd: bundleDir
})
- if (tarResult.status !== 0) {
- throw new Error(`Unable to package for linux, status: ${JSON.stringify(tarResult)}`);
- }
-
return {
filePath: archiveFilePath,
fileName: archiveFileName
}
}
-async function packageForMacos(projectRoot: string, arch: string): Promise<{ filePath: string; fileName: string }> {
- const releaseDirPath = path.join(projectRoot, 'target', arch, 'release');
- const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet');
- const outFileName = "gauntlet-aarch64-macos.dmg"
- const outFilePath = path.join(releaseDirPath, outFileName);
- const sourceInfoFilePath = path.join(projectRoot, 'assets', 'Info.plist');
- const sourceIconFilePath = path.join(projectRoot, 'assets', 'AppIcon.icns');
+async function packageForMacos(projectRoot: string, arch: string[], profile: string, sign: boolean, notarize: boolean): Promise<{ filePath: string; fileName: string }> {
+ const targetDirPath = path.join(projectRoot, 'target');
+ const outDirPath = path.join(targetDirPath, 'out');
+ const outFileName = "gauntlet-universal-macos.dmg"
+ const outFilePath = path.join(targetDirPath, outFileName);
- const bundleDir = path.join(releaseDirPath, 'Gauntlet.app');
+ const assetsDirPath = path.join(projectRoot, 'assets', 'macos');
+ const sourceInfoFilePath = path.join(assetsDirPath, 'Info.plist');
+ const sourceIconFilePath = path.join(assetsDirPath, 'AppIcon.icns');
+ const dmgBackground = path.join(assetsDirPath, 'dmg-background.png');
+ const entitlementsPath = path.join(assetsDirPath, 'entitlements.plist');
+
+ const bundleDir = path.join(outDirPath, 'Gauntlet.app');
const contentsDir = path.join(bundleDir, 'Contents');
const macosContentsDir = path.join(contentsDir, 'MacOS');
const resourcesContentsDir = path.join(contentsDir, 'Resources');
@@ -295,16 +361,29 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil
const targetInfoFilePath = path.join(contentsDir, 'Info.plist');
const targetIconFilePath = path.join(resourcesContentsDir, 'AppIcon.icns');
- const dmgBackground = path.join(projectRoot, 'assets', 'dmg-background.png');
+ const sourceExecutableFilePaths = arch.map(arch => path.join(targetDirPath, arch, profile, 'gauntlet'));
const version = await readVersion(projectRoot)
+ if (existsSync(outDirPath)) {
+ rmdirSync(outDirPath)
+ }
+
+ mkdirSync(outDirPath)
mkdirSync(bundleDir)
mkdirSync(contentsDir)
mkdirSync(macosContentsDir)
mkdirSync(resourcesContentsDir)
- copyFileSync(sourceExecutableFilePath, targetExecutableFilePath)
+ spawnWithErrors(`lipo`, [
+ ...sourceExecutableFilePaths,
+ '-create',
+ '-output',
+ targetExecutableFilePath
+ ], {
+ cwd: outDirPath
+ })
+
copyFileSync(sourceInfoFilePath, targetInfoFilePath)
copyFileSync(sourceIconFilePath, targetIconFilePath)
@@ -312,7 +391,37 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil
const infoResult = infoSource.replace('__VERSION__', `${version}.0.0`);
writeFileSync(targetInfoFilePath, infoResult,'utf8');
- const createDmgResult = spawnSync(`create-dmg`, [
+ const signKeyPath = path.join(outDirPath, 'signKey.pem');
+ const signCertPath = path.join(outDirPath, 'signCert.pem');
+ const connectApiKeyPath = path.join(outDirPath, 'connectApiKey.json');
+
+ const signKeyContent = process.env.APPLE_SIGNING_KEY_PEM;
+ const signCertContent = process.env.APPLE_SIGNING_CERT_PEM;
+ const connectApiKeyContent = process.env.APP_STORE_CONNECT_KEY;
+
+ if (sign) {
+ const key = JSON.parse(signKeyContent!!).content;
+ const cert = JSON.parse(signCertContent!!).content;
+
+ writeFileSync(signKeyPath, key);
+ writeFileSync(signCertPath, cert);
+
+ spawnWithErrors(`rcodesign`, [
+ 'sign',
+ '--pem-file',
+ signKeyPath,
+ '--pem-file',
+ signCertPath,
+ '--for-notarization',
+ '--entitlements-xml-file',
+ entitlementsPath,
+ bundleDir
+ ], {
+ cwd: outDirPath
+ })
+ }
+
+ spawnWithErrors(`create-dmg`, [
'--volname', 'Gauntlet Installer',
'--window-size', '660', '400',
'--background', dmgBackground,
@@ -323,12 +432,35 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil
outFileName,
bundleDir
], {
- stdio: "inherit",
- cwd: releaseDirPath
+ cwd: targetDirPath
})
- if (createDmgResult.status !== 0) {
- throw new Error(`Unable to package for macos, status: ${JSON.stringify(createDmgResult)}`);
+ if (sign) {
+ spawnWithErrors(`rcodesign`, [
+ 'sign',
+ '--pem-file',
+ signKeyPath,
+ '--pem-file',
+ signCertPath,
+ '--for-notarization',
+ outFilePath
+ ], {
+ cwd: outDirPath
+ })
+ }
+
+ if (notarize) {
+ writeFileSync(connectApiKeyPath, connectApiKeyContent!!);
+
+ spawnWithErrors(`rcodesign`, [
+ 'notary-submit',
+ '--api-key-file',
+ connectApiKeyPath,
+ '--staple',
+ outFilePath
+ ], {
+ cwd: outDirPath
+ })
}
return {
@@ -337,25 +469,60 @@ async function packageForMacos(projectRoot: string, arch: string): Promise<{ fil
}
}
-function publishNpmPackage(projectRoot: string) {
- console.log("Publishing npm deno package...")
- const denoProjectPath = path.join(projectRoot, "js", "deno");
- const denoNpmPublish = spawnSync('npm', ['publish'], { stdio: "inherit", cwd: denoProjectPath })
+async function packageForWindows(projectRoot: string, arch: string, profile: string): Promise<{ filePath: string; fileName: string }> {
+ const releaseDirPath = path.join(projectRoot, 'target', arch, profile);
+ const sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet.exe');
+ const outFileName = "gauntlet-x86_64-windows.msi"
+ const outFilePath = path.join(releaseDirPath, outFileName);
- if (denoNpmPublish.status !== 0) {
- throw new Error(`Unable to publish deno package, status: ${JSON.stringify(denoNpmPublish)}`);
- }
+ const assetsDirPath = path.join(projectRoot, 'assets', 'windows');
+ const sourceWxsFilePath = path.join(assetsDirPath, 'main.wxs');
+ const iconFilePath = path.join(projectRoot, 'assets', 'linux', 'icon_256.png');
- console.log("Publishing npm api package...")
- const apiProjectPath = path.join(projectRoot, "js", "api");
- const apiNpmPublish = spawnSync('npm', ['publish'], { stdio: "inherit", cwd: apiProjectPath })
+ const targetWxsFilePath = path.join(releaseDirPath, 'main.wxs');
+ const targetIconFilePath = path.join(releaseDirPath, 'icon.ico');
- if (apiNpmPublish.status !== 0) {
- throw new Error(`Unable to publish api package, status: ${JSON.stringify(apiNpmPublish)}`);
+ const version = await readVersion(projectRoot)
+
+ copyFileSync(sourceWxsFilePath, targetWxsFilePath)
+
+ spawnWithErrors("magick.exe", [iconFilePath, '-define', 'icon:auto-resize=256,128,48,32,16', targetIconFilePath], {
+ cwd: releaseDirPath
+ })
+
+ spawnWithErrors("wix", [
+ 'build',
+ targetWxsFilePath,
+ '-out', outFilePath,
+ '-define', `TargetBinaryPath=${sourceExecutableFilePath}`,
+ '-define', `TargetIconPath=${targetIconFilePath}`,
+ '-define', `TargetVersion=${version}.0`,
+ '-ext', "WixToolset.Util.wixext",
+ '-arch', "x64",
+ ], {
+ cwd: releaseDirPath
+ })
+
+ return {
+ filePath: outFilePath,
+ fileName: outFileName
}
}
-async function createRelease(newVersion: number, releaseNotes: string) {
+
+async function publishNpmPackage(projectRoot: string) {
+ const version = await readVersion(projectRoot)
+
+ const apiProjectPath = path.join(projectRoot, "js", "api");
+
+ console.log("Bump version for api subproject...")
+ spawnWithErrors('npm', ['version', `0.${version}.0`], { cwd: apiProjectPath })
+
+ console.log("Publishing npm api package...")
+ spawnWithErrors('npm', ['publish'], { cwd: apiProjectPath })
+}
+
+async function createRelease(newVersion: number, releaseNotes: string): Promise {
const octokit = getOctokit();
console.log("Creating github release...")
@@ -366,10 +533,10 @@ async function createRelease(newVersion: number, releaseNotes: string) {
target_commitish: 'main',
name: `v${newVersion}`,
body: releaseNotes,
- draft: true
+ draft: true // release needs to be undrafted manually after each release
});
- core.setOutput("github-release-id", `${response.data.id}`)
+ return response.data.id;
}
async function addFileToRelease(filePath: string, fileName: string) {
@@ -420,4 +587,14 @@ async function readVersion(projectRoot: string): Promise {
async function writeVersion(projectRoot: string, version: number) {
const versionFilePath = path.join(projectRoot, "VERSION");
await writeFile(versionFilePath, `${version}`)
-}
\ No newline at end of file
+}
+
+function spawnWithErrors(command: string, args: string[], options: SpawnSyncOptions) {
+ console.log(`running ${command} ${args}`)
+
+ const npmRunResult = spawnSync(command, args, { ...options, stdio: "inherit", });
+
+ if (npmRunResult.status !== 0) {
+ throw new Error(`Unable to run ${command} ${args}, status: ${JSON.stringify(npmRunResult, null, 2)}`);
+ }
+}
diff --git a/js/core/package.json b/js/core/package.json
index 59445a3..1398cae 100644
--- a/js/core/package.json
+++ b/js/core/package.json
@@ -5,15 +5,16 @@
"build": "tsc --noEmit && rollup --config rollup.config.ts --configPlugin typescript"
},
"devDependencies": {
- "@rollup/plugin-commonjs": "^25.0.7",
- "@rollup/plugin-node-resolve": "^15.2.3",
- "@rollup/plugin-typescript": "^11.1.5",
- "@types/react": "^18.2.35",
"@project-gauntlet/api": "*",
"@project-gauntlet/typings": "*",
- "@project-gauntlet/deno": "*",
- "rollup": "^4.3.0",
- "tslib": "^2.6.2",
- "typescript": "^5.3.3"
+ "@rollup/plugin-alias": "^5.1.1",
+ "@rollup/plugin-commonjs": "^28.0.2",
+ "@rollup/plugin-node-resolve": "^16.0.0",
+ "@rollup/plugin-typescript": "^12.1.2",
+ "@types/deno": "^2.0.0",
+ "@types/react": "^18.3.18",
+ "rollup": "^4.28.1",
+ "tslib": "^2.8.1",
+ "typescript": "^5.7.2"
}
}
diff --git a/js/core/rollup.config.ts b/js/core/rollup.config.ts
index 68a33d4..760a198 100644
--- a/js/core/rollup.config.ts
+++ b/js/core/rollup.config.ts
@@ -2,10 +2,16 @@ import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from "rollup";
+import alias from '@rollup/plugin-alias';
export default defineConfig({
input: [
- 'src/init.tsx',
+ 'src/core.tsx',
+ 'src/init.ts',
+ 'src/internal-all.ts',
+ 'src/internal-linux.ts',
+ 'src/internal-macos.ts',
+ 'src/internal-windows.ts',
],
output: [
{
@@ -14,12 +20,18 @@ export default defineConfig({
sourcemap: 'inline',
}
],
- external: ["react", "react/jsx-runtime"],
+ external: [/^ext:.+/],
plugins: [
nodeResolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
}),
+ alias({
+ entries: [
+ { find: 'react/jsx-runtime', replacement: 'ext:gauntlet/react-jsx-runtime.js' },
+ { find: 'react', replacement: 'ext:gauntlet/react.js' },
+ ]
+ }),
]
})
diff --git a/js/core/src/command-generator.ts b/js/core/src/command-generator.ts
deleted file mode 100644
index b20757e..0000000
--- a/js/core/src/command-generator.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-// @ts-expect-error does typescript support such symbol declarations?
-const denoCore: DenoCore = Deno[Deno.internal].core;
-const InternalApi = denoCore.ops;
-
-interface GeneratedCommand { // TODO Add this type to api
- id: string
- name: string
- icon: ArrayBuffer | undefined
- fn: () => void
-}
-
-let storedGeneratedCommands: (GeneratedCommand & { lookupId: string, uuid: string })[] = []
-
-export async function runCommandGenerators(): Promise {
- storedGeneratedCommands = []
-
- const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids();
- for (const generatorEntrypointId of entrypointIds) {
- try {
- const generator: () => GeneratedCommand[] = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default;
-
- const generatedCommands = generator()
- .map(value => {
- return {
- lookupId: generatorEntrypointId + ":" + value.id,
- uuid: crypto.randomUUID(),
- ...value
- }
- });
-
- storedGeneratedCommands.push(...generatedCommands)
- } catch (e) {
- console.error("Error occurred when calling command generator for entrypoint: " + generatorEntrypointId, e)
- }
- }
-}
-
-export function generatedCommandSearchIndex(): AdditionalSearchItem[] {
- return storedGeneratedCommands.map(value => ({
- entrypoint_id: value.lookupId,
- entrypoint_uuid: value.uuid,
- entrypoint_name: value.name,
- entrypoint_icon: value.icon,
- }))
-}
-
-export function runGeneratedCommand(entrypointId: string) {
- const generatedCommand = storedGeneratedCommands.find(value => value.lookupId === entrypointId);
-
- if (generatedCommand) {
- generatedCommand.fn()
- } else {
- throw new Error("Generated command with entrypoint id '" + entrypointId + "' not found")
- }
-}
\ No newline at end of file
diff --git a/js/core/src/core.tsx b/js/core/src/core.tsx
new file mode 100644
index 0000000..2afa162
--- /dev/null
+++ b/js/core/src/core.tsx
@@ -0,0 +1,179 @@
+import type { FC } from "react";
+import { runEntrypointGenerators, runGeneratedEntrypoint, runGeneratedEntrypointAction } from "./entrypoint-generator";
+import { reloadSearchIndex } from "./search-index";
+import {
+ closeView,
+ handleEvent,
+ handlePluginViewKeyboardEvent, popMainView,
+ renderInlineView,
+ renderView,
+} from "./render";
+import {
+ entrypoint_preferences_required,
+ get_entrypoint_preferences,
+ get_plugin_preferences,
+ op_entrypoint_names,
+ op_inline_view_entrypoint_id,
+ op_log_trace,
+ op_plugin_get_pending_event,
+ plugin_preferences_required,
+ show_plugin_error_view,
+ show_preferences_required_view
+} from "ext:core/ops";
+
+
+async function handleKeyboardEvent(event: NotReactsKeyboardEvent) {
+ op_log_trace("plugin_event_handler", `Handling keyboard event: ${Deno.inspect(event)}`);
+ switch (event.origin) {
+ case "MainView": {
+ runGeneratedEntrypointAction(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta)
+ break;
+ }
+ case "PluginView": {
+ handlePluginViewKeyboardEvent(event.entrypointId, event.key, event.modifierShift, event.modifierControl, event.modifierAlt, event.modifierMeta)
+ break;
+ }
+ }
+}
+
+async function checkRequiredPreferences(entrypointId: string): Promise {
+ const pluginPreferencesRequired = plugin_preferences_required();
+ const entrypointPreferencesRequired = entrypoint_preferences_required(entrypointId);
+
+ return pluginPreferencesRequired || entrypointPreferencesRequired;
+}
+
+async function checkRequiredPreferencesAndAsk(entrypointId: string): Promise {
+ const pluginPreferencesRequired = await plugin_preferences_required();
+ const entrypointPreferencesRequired = await entrypoint_preferences_required(entrypointId);
+
+ const required = pluginPreferencesRequired || entrypointPreferencesRequired;
+ if (required) {
+ show_preferences_required_view(entrypointId, pluginPreferencesRequired, entrypointPreferencesRequired)
+ }
+
+ return required;
+}
+
+export async function runPluginLoop() {
+ await runEntrypointGenerators();
+
+ // runtime is stopped using tokio cancellation
+ // noinspection InfiniteLoopJS
+ while (true) {
+ op_log_trace("plugin_loop", "Waiting for next plugin event...")
+ const pluginEvent = await op_plugin_get_pending_event();
+ op_log_trace("plugin_loop", `Received plugin event: ${Deno.inspect(pluginEvent)}`)
+ switch (pluginEvent.type) {
+ case "ViewEvent": {
+ try {
+ handleEvent(pluginEvent)
+ } catch (e) {
+ console.error("Error occurred when receiving view event to handle", e)
+ }
+ break;
+ }
+ case "KeyboardEvent": {
+ try {
+ await handleKeyboardEvent(pluginEvent)
+ } catch (e) {
+ console.error("Error occurred when receiving keyboard event to handle", e)
+ }
+ break;
+ }
+ case "OpenView": {
+ const entrypointId = pluginEvent.entrypointId
+ try {
+ if (await checkRequiredPreferencesAndAsk(entrypointId)) {
+ break;
+ }
+
+ const view: FC = (await import(`gauntlet:entrypoint?${entrypointId}`)).default;
+ renderView(entrypointId, getEntrypointName(entrypointId), view)
+ } catch (e) {
+ console.error("Error occurred when rendering view", entrypointId, e)
+ show_plugin_error_view(entrypointId, "View")
+ }
+ break;
+ }
+ case "CloseView": {
+ closeView()
+ break;
+ }
+ case "PopView": {
+ const entrypointId = pluginEvent.entrypointId
+ try {
+ popMainView()
+ } catch (e) {
+ console.error("Error occurred when popping view", entrypointId, e)
+ show_plugin_error_view(entrypointId, "View")
+ }
+ break;
+ }
+ case "RunCommand": {
+ try {
+ if (await checkRequiredPreferencesAndAsk(pluginEvent.entrypointId)) {
+ break;
+ }
+
+ type CommandContext = {
+ pluginPreferences: P,
+ entrypointPreferences: E,
+ };
+
+ const pluginPreferences = get_plugin_preferences();
+ const entrypointPreferences = get_entrypoint_preferences(pluginEvent.entrypointId);
+
+ const command: (context: CommandContext) => Promise | void = (await import(`gauntlet:entrypoint?${pluginEvent.entrypointId}`)).default;
+ command({ pluginPreferences, entrypointPreferences })
+ } catch (e) {
+ console.error("Error occurred when running a command", pluginEvent.entrypointId, e)
+ }
+ break;
+ }
+ case "RunGeneratedEntrypoint": {
+ try {
+ runGeneratedEntrypoint(pluginEvent.entrypointId, pluginEvent.actionIndex)
+ } catch (e) {
+ console.error("Error occurred when running a generated command", pluginEvent.entrypointId, e)
+ }
+ break;
+ }
+ case "OpenInlineView": {
+ const entrypointId = op_inline_view_entrypoint_id();
+
+ if (entrypointId) {
+ if (await checkRequiredPreferences(entrypointId)) {
+ break;
+ }
+
+ try {
+ type InlineView = { text: string };
+ const handler: FC