>(): T {
- return getEntrypointPreferences()
+export function showHud(display: string): void {
+ return showHudWindow(display)
}
-export interface GeneratedCommand {
- id: string
+export interface GeneratedEntrypoint {
name: string
+ actions: GeneratedEntrypointAction[]
icon?: ArrayBuffer
- fn: () => void
+ 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 0828044..2b7b24e 100644
--- a/js/build/package.json
+++ b/js/build/package.json
@@ -13,19 +13,19 @@
},
"type": "module",
"dependencies": {
- "@actions/core": "^1.10.1",
- "commander": "^11.1.0",
- "octokit": "^3.1.2",
- "simple-git": "^3.22.0",
- "cross-spawn": "^7.0.3"
+ "@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",
+ "@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.6.2",
- "typescript": "^5.3.3"
+ "tslib": "^2.8.1",
+ "typescript": "^5.7.2"
}
}
diff --git a/js/build/src/main.ts b/js/build/src/main.ts
index 9569bba..28d0a37 100644
--- a/js/build/src/main.ts
+++ b/js/build/src/main.ts
@@ -6,7 +6,7 @@ import { Octokit } from 'octokit';
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";
@@ -58,10 +58,10 @@ program.command('build-windows')
await program.parseAsync(process.argv);
-async function doBuild(projectRoot: string, arch: string) {
+async function doBuild(projectRoot: string, arch: string, profile: string) {
console.log("Building Gauntlet...")
- build(projectRoot, arch)
+ build(projectRoot, arch, profile)
}
async function doPublishInit() {
@@ -85,15 +85,18 @@ async function doPublishInit() {
}
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, arch)
-
- const { fileName, filePath } = packageForLinux(projectRoot, arch)
+ build(projectRoot, arch, profile)
+ const { fileName, filePath } = packageForLinux(projectRoot, arch, profile)
await addFileToRelease(filePath, fileName)
}
@@ -101,39 +104,72 @@ async function doPublishLinux() {
async function doBuildLinux() {
const arch = 'x86_64-unknown-linux-gnu';
const projectRoot = getProjectRoot();
+ const profile = 'release';
- await doBuild(projectRoot, arch)
- packageForLinux(projectRoot, arch)
+ 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, arch)
+ console.log("git pull...")
+ await git.pull()
- const { fileName, filePath } = await packageForMacos(projectRoot, arch, true, true)
+ 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() {
const projectRoot = getProjectRoot();
- const arch = 'aarch64-apple-darwin';
+ const archArm = 'aarch64-apple-darwin';
+ const archIntel = 'x86_64-apple-darwin';
+ const profile = 'release';
- await doBuild(projectRoot, arch)
- await packageForMacos(projectRoot, arch, true, false)
+ buildJs(projectRoot)
+ buildRust(projectRoot, archArm, profile)
+ buildRust(projectRoot, archIntel, profile)
+
+ await packageForMacos(
+ projectRoot,
+ [archArm, archIntel],
+ profile,
+ false,
+ false
+ )
}
async function doPublishWindows() {
const projectRoot = getProjectRoot();
+ const git = simpleGit(projectRoot);
+
+ console.log("git pull...")
+ await git.pull()
+
const arch = 'x86_64-pc-windows-msvc';
+ const profile = 'release-size';
- build(projectRoot, arch)
+ build(projectRoot, arch, profile)
- const { fileName, filePath } = await packageForWindows(projectRoot, arch)
+ const { fileName, filePath } = await packageForWindows(projectRoot, arch, profile)
await addFileToRelease(filePath, fileName)
}
@@ -141,25 +177,36 @@ async function doPublishWindows() {
async function doBuildWindows() {
const projectRoot = getProjectRoot();
const arch = 'x86_64-pc-windows-msvc';
+ const profile = 'release';
- await doBuild(projectRoot, arch)
- await packageForWindows(projectRoot, arch)
+ await doBuild(projectRoot, arch, profile)
+ await packageForWindows(projectRoot, arch, profile)
}
async function doPublishFinal() {
- console.log("Publishing Gauntlet npm packages...")
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 publishNpmPackage(projectRoot)
}
-function build(projectRoot: string, arch: string) {
+function build(projectRoot: string, arch: string, profile: string) {
buildJs(projectRoot)
+ buildRust(projectRoot, arch, profile)
+}
+
+function buildRust(projectRoot: string, arch: string, profile: string) {
console.log("Building rust...")
- spawnWithErrors('cargo', ['build', '--release', '--features', 'release', '--target', arch], {
+ spawnWithErrors('cargo', ['build', '--profile', profile, '--features', 'release', '--target', arch], {
cwd: projectRoot
});
}
@@ -234,18 +281,6 @@ async function makeRepoChanges(projectRoot: string): Promise<{ releaseNotes: str
console.log("Writing changelog file...")
await writeFile(changelogFilePath, newChangelog.join(EOL))
- const bumpNpmPackage = (packageDir: string) => {
- spawnWithErrors('npm', ['version', `0.${newVersion}.0`], { cwd: packageDir })
- }
-
- 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...")
@@ -263,8 +298,8 @@ 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');
+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 sourceExecutableFilePath = path.join(releaseDirPath, 'gauntlet');
@@ -306,11 +341,11 @@ function packageForLinux(projectRoot: string, arch: string): { filePath: string;
}
}
-async function packageForMacos(projectRoot: string, arch: string, sign: boolean, notarize: boolean): 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);
+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 assetsDirPath = path.join(projectRoot, 'assets', 'macos');
const sourceInfoFilePath = path.join(assetsDirPath, 'Info.plist');
@@ -318,7 +353,7 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
const dmgBackground = path.join(assetsDirPath, 'dmg-background.png');
const entitlementsPath = path.join(assetsDirPath, 'entitlements.plist');
- const bundleDir = path.join(releaseDirPath, 'Gauntlet.app');
+ 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');
@@ -326,14 +361,29 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
const targetInfoFilePath = path.join(contentsDir, 'Info.plist');
const targetIconFilePath = path.join(resourcesContentsDir, 'AppIcon.icns');
+ 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)
@@ -341,9 +391,9 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
const infoResult = infoSource.replace('__VERSION__', `${version}.0.0`);
writeFileSync(targetInfoFilePath, infoResult,'utf8');
- const signKeyPath = path.join(releaseDirPath, 'signKey.pem');
- const signCertPath = path.join(releaseDirPath, 'signCert.pem');
- const connectApiKeyPath = path.join(releaseDirPath, 'connectApiKey.json');
+ 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;
@@ -367,7 +417,7 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
entitlementsPath,
bundleDir
], {
- cwd: releaseDirPath
+ cwd: outDirPath
})
}
@@ -382,7 +432,7 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
outFileName,
bundleDir
], {
- cwd: releaseDirPath
+ cwd: targetDirPath
})
if (sign) {
@@ -395,7 +445,7 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
'--for-notarization',
outFilePath
], {
- cwd: releaseDirPath
+ cwd: outDirPath
})
}
@@ -409,7 +459,7 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
'--staple',
outFilePath
], {
- cwd: releaseDirPath
+ cwd: outDirPath
})
}
@@ -419,8 +469,8 @@ async function packageForMacos(projectRoot: string, arch: string, sign: boolean,
}
}
-async function packageForWindows(projectRoot: string, arch: string): Promise<{ filePath: string; fileName: string }> {
- const releaseDirPath = path.join(projectRoot, 'target', arch, 'release');
+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);
@@ -460,13 +510,15 @@ async function packageForWindows(projectRoot: string, arch: string): Promise<{ f
}
-function publishNpmPackage(projectRoot: string) {
- console.log("Publishing npm deno package...")
- const denoProjectPath = path.join(projectRoot, "js", "deno");
- spawnWithErrors('npm', ['publish'], { cwd: denoProjectPath })
+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...")
- const apiProjectPath = path.join(projectRoot, "js", "api");
spawnWithErrors('npm', ['publish'], { cwd: apiProjectPath })
}
@@ -545,4 +597,4 @@ function spawnWithErrors(command: string, args: string[], options: SpawnSyncOpti
if (npmRunResult.status !== 0) {
throw new Error(`Unable to run ${command} ${args}, status: ${JSON.stringify(npmRunResult, null, 2)}`);
}
-}
\ No newline at end of file
+}
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 fdf4009..0000000
--- a/js/core/src/command-generator.ts
+++ /dev/null
@@ -1,63 +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 is it possible to import api here
- id: string
- name: string
- icon?: ArrayBuffer
- fn: () => void
-}
-
-type ProcessedGeneratedCommand = GeneratedCommand & { lookupId: string, uuid: string };
-
-let storedGeneratedCommands: ProcessedGeneratedCommand[] = []
-
-export async function runCommandGenerators(): Promise {
- let localGeneratedCommands: ProcessedGeneratedCommand[] = []
-
- const entrypointIds = await InternalApi.get_command_generator_entrypoint_ids();
- for (const generatorEntrypointId of entrypointIds) {
- try {
- const generator: () => Promise | GeneratedCommand[] = (await import(`gauntlet:entrypoint?${generatorEntrypointId}`)).default;
-
- InternalApi.op_log_info("command_generator", `Running command generator for entrypoint ${generatorEntrypointId}`)
-
- const generatedCommands = (await generator())
- .map(value => {
- return {
- lookupId: generatorEntrypointId + ":" + value.id,
- uuid: crypto.randomUUID(),
- ...value
- }
- });
-
- InternalApi.op_log_info("command_generator", `Finished running command generator for entrypoint ${generatorEntrypointId}, amount: ${generatedCommands.length}`)
-
- localGeneratedCommands.push(...generatedCommands)
- } catch (e) {
- console.error("Error occurred when calling command generator for entrypoint: " + generatorEntrypointId, e)
- }
- }
-
- storedGeneratedCommands = localGeneratedCommands
-}
-
-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