1
0
forked from clan/clan-core

clan ui: setup typed api method

This commit is contained in:
Johannes Kirschbauer 2024-05-20 19:34:27 +02:00
parent 6ebfd29c87
commit 8687801cee
Signed by: hsjobeki
SSH Key Fingerprint: SHA256:vX3utDqig7Ph5L0JPv87ZTPb/w7cMzREKVZzzLFg9qU
16 changed files with 375 additions and 96 deletions

18
pkgs/clan-cli/api.py Normal file
View File

@ -0,0 +1,18 @@
from clan_cli import create_parser
from clan_cli.api import API
from clan_cli.api.schema_compat import to_json_schema
def main() -> None:
# Create the parser to register the API functions
create_parser()
schema = to_json_schema(API._registry)
print(
f"""export const schema = {schema} as const;
"""
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,13 @@
from collections.abc import Callable
class _MethodRegistry:
def __init__(self):
self._registry = {}
def register(self, fn: Callable) -> Callable:
self._registry[fn.__name__] = fn
return fn
API = _MethodRegistry()

View File

@ -0,0 +1,111 @@
import dataclasses
import json
from types import NoneType, UnionType
from typing import Any, Callable, Union, get_type_hints
import pathlib
def type_to_dict(t: Any, scope: str = "") -> dict:
# print(
# f"Type: {t}, Scope: {scope}, has origin: {hasattr(t, '__origin__')} ",
# type(t) is UnionType,
# )
if t is None:
return {"type": "null"}
if dataclasses.is_dataclass(t):
fields = dataclasses.fields(t)
properties = {
f.name: type_to_dict(f.type, f"{scope} {t.__name__}.{f.name}")
for f in fields
}
required = [pn for pn, pv in properties.items() if "null" not in pv["type"]]
return {
"type": "object",
"properties": properties,
"required": required,
# Dataclasses can only have the specified properties
"additionalProperties": False,
}
elif type(t) is UnionType:
return {
"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__],
}
elif hasattr(t, "__origin__"): # Check if it's a generic type
origin = getattr(t, "__origin__", None)
if origin is None:
# Non-generic user-defined or built-in type
# TODO: handle custom types
raise BaseException("Unhandled Type: ", origin)
elif origin is Union:
return {"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__]}
elif issubclass(origin, list):
return {"type": "array", "items": type_to_dict(t.__args__[0], scope)}
elif issubclass(origin, dict):
return {
"type": "object",
}
raise BaseException(f"Error api type not yet supported {str(t)}")
elif isinstance(t, type):
if t is str:
return {"type": "string"}
if t is int:
return {"type": "integer"}
if t is float:
return {"type": "number"}
if t is bool:
return {"type": "boolean"}
if t is object:
return {"type": "object"}
if t is Any:
raise BaseException(
f"Usage of the Any type is not supported for API functions. In: {scope}"
)
if t is pathlib.Path:
return {
# TODO: maybe give it a pattern for URI
"type": "string",
}
# Optional[T] gets internally transformed Union[T,NoneType]
if t is NoneType:
return {"type": "null"}
raise BaseException(f"Error primitive type not supported {str(t)}")
else:
raise BaseException(f"Error type not supported {str(t)}")
def to_json_schema(methods: dict[str, Callable]) -> str:
api_schema = {
"$comment": "An object containing API methods. ",
"type": "object",
"additionalProperties": False,
"required": ["list_machines"],
"properties": {},
}
for name, func in methods.items():
hints = get_type_hints(func)
serialized_hints = {
"argument" if key != "return" else "return": type_to_dict(
value, scope=name + " argument" if key != "return" else "return"
)
for key, value in hints.items()
}
api_schema["properties"][name] = {
"type": "object",
"required": [k for k in serialized_hints.keys()],
"additionalProperties": False,
"properties": {**serialized_hints},
}
return json.dumps(api_schema, indent=2)

View File

@ -5,11 +5,14 @@ from pathlib import Path
from ..cmd import run from ..cmd import run
from ..nix import nix_config, nix_eval from ..nix import nix_config, nix_eval
from clan_cli.api import API
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register
def list_machines(flake_url: Path | str) -> list[str]: def list_machines(flake_url: Path | str) -> list[str]:
print("list_machines", flake_url)
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
cmd = nix_eval( cmd = nix_eval(

View File

@ -57,6 +57,16 @@
cp -r out/* $out cp -r out/* $out
''; '';
}; };
clan-ts-api = pkgs.stdenv.mkDerivation {
name = "clan-ts-api";
src = ./.;
buildInputs = [ pkgs.python3 ];
installPhase = ''
python api.py > $out
'';
};
default = self'.packages.clan-cli; default = self'.packages.clan-cli;
}; };

View File

@ -2,6 +2,7 @@ import dataclasses
import json import json
import sys import sys
import threading import threading
from threading import Lock
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import Any, Union from typing import Any, Union
@ -10,7 +11,7 @@ import gi
gi.require_version("WebKit", "6.0") gi.require_version("WebKit", "6.0")
from gi.repository import GLib, WebKit from gi.repository import GLib, GObject, WebKit
site_index: Path = ( site_index: Path = (
Path(sys.argv[0]).absolute() Path(sys.argv[0]).absolute()
@ -19,51 +20,9 @@ site_index: Path = (
).resolve() ).resolve()
def type_to_dict(t: Any) -> dict:
if dataclasses.is_dataclass(t):
fields = dataclasses.fields(t)
return {
"type": "dataclass",
"name": t.__name__,
"fields": {f.name: type_to_dict(f.type) for f in fields},
}
if hasattr(t, "__origin__"): # Check if it's a generic type
if t.__origin__ is None:
# Non-generic user-defined or built-in type
return {"type": t.__name__}
if t.__origin__ is Union:
return {"type": "union", "of": [type_to_dict(arg) for arg in t.__args__]}
elif issubclass(t.__origin__, list):
return {"type": "list", "item_type": type_to_dict(t.__args__[0])}
elif issubclass(t.__origin__, dict):
return {
"type": "dict",
"key_type": type_to_dict(t.__args__[0]),
"value_type": type_to_dict(t.__args__[1]),
}
elif issubclass(t.__origin__, tuple):
return {
"type": "tuple",
"element_types": [type_to_dict(elem) for elem in t.__args__],
}
elif issubclass(t.__origin__, set):
return {"type": "set", "item_type": type_to_dict(t.__args__[0])}
else:
# Handle other generic types (like Union, Optional)
return {
"type": str(t.__origin__.__name__),
"parameters": [type_to_dict(arg) for arg in t.__args__],
}
elif isinstance(t, type):
return {"type": t.__name__}
else:
return {"type": str(t)}
class WebView: class WebView:
def __init__(self) -> None: def __init__(self, methods: dict[str, Callable]) -> None:
self.method_registry: dict[str, Callable] = {} self.method_registry: dict[str, Callable] = methods
self.webview = WebKit.WebView() self.webview = WebKit.WebView()
self.manager = self.webview.get_user_content_manager() self.manager = self.webview.get_user_content_manager()
@ -74,39 +33,66 @@ class WebView:
self.webview.load_uri(f"file://{site_index}") self.webview.load_uri(f"file://{site_index}")
def method(self, function: Callable) -> Callable: # global mutex lock to ensure functions run sequentially
# type_hints = get_type_hints(function) self.mutex_lock = Lock()
# serialized_hints = {key: type_to_dict(value) for key, value in type_hints.items()} self.queue_size = 0
self.method_registry[function.__name__] = function
return function
def on_message_received( def on_message_received(
self, user_content_manager: WebKit.UserContentManager, message: Any self, user_content_manager: WebKit.UserContentManager, message: Any
) -> None: ) -> None:
payload = json.loads(message.to_json(0)) payload = json.loads(message.to_json(0))
print(f"Received message: {payload}")
method_name = payload["method"] method_name = payload["method"]
handler_fn = self.method_registry[method_name] handler_fn = self.method_registry[method_name]
# Start handler_fn in a new thread print(f"Received message: {payload}")
# GLib.idle_add(handler_fn) print(f"Queue size: {self.queue_size} (Wait)")
def threaded_wrapper() -> bool:
"""
Ensures only one function is executed at a time
Wait until there is no other function acquiring the global lock.
Starts a thread with the potentially long running API function within.
"""
if not self.mutex_lock.locked():
thread = threading.Thread( thread = threading.Thread(
target=self.threaded_handler, target=self.threaded_handler,
args=(handler_fn, payload.get("data"), method_name), args=(
handler_fn,
payload.get("data"),
method_name,
),
) )
thread.start() thread.start()
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
GLib.idle_add(
threaded_wrapper,
)
self.queue_size += 1
def threaded_handler( def threaded_handler(
self, handler_fn: Callable[[Any], Any], data: Any, method_name: str self, handler_fn: Callable[[Any], Any], data: Any, method_name: str
) -> None: ) -> None:
with self.mutex_lock:
print("Executing", method_name)
print("threading locked ...")
result = handler_fn(data) result = handler_fn(data)
serialized = json.dumps(result) serialized = json.dumps(result)
# Use idle_add to queue the response call to js on the main GTK thread # Use idle_add to queue the response call to js on the main GTK thread
GLib.idle_add(self.call_js, method_name, serialized) GLib.idle_add(self.return_data_to_js, method_name, serialized)
print("threading unlocked")
self.queue_size -= 1
if self.queue_size > 0:
print(f"remaining queue size: {self.queue_size}")
else:
print(f"Queue empty")
def call_js(self, method_name: str, serialized: str) -> bool: def return_data_to_js(self, method_name: str, serialized: str) -> bool:
# This function must be run on the main GTK thread to interact with the webview # This function must be run on the main GTK thread to interact with the webview
# result = method_fn(data) # takes very long # result = method_fn(data) # takes very long
# serialized = result # serialized = result
@ -119,7 +105,7 @@ class WebView:
None, None,
None, None,
) )
return False # Important to return False so that it's not run again return GLib.SOURCE_REMOVE
def get_webview(self) -> WebKit.WebView: def get_webview(self) -> WebKit.WebView:
return self.webview return self.webview

View File

@ -2,7 +2,7 @@ import logging
import threading import threading
import gi import gi
from clan_cli import machines
from clan_cli.history.list import list_history from clan_cli.history.list import list_history
from clan_vm_manager.components.interfaces import ClanConfig from clan_vm_manager.components.interfaces import ClanConfig
@ -14,10 +14,11 @@ from clan_vm_manager.views.list import ClanList
from clan_vm_manager.views.logs import Logs from clan_vm_manager.views.logs import Logs
from clan_vm_manager.views.webview import WebView from clan_vm_manager.views.webview import WebView
from clan_cli.api import API
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, GLib, Gtk from gi.repository import Adw, Gio, GLib, Gtk
from clan_vm_manager.components.trayicon import TrayIcon from clan_vm_manager.components.trayicon import TrayIcon
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -61,11 +62,7 @@ class MainWindow(Adw.ApplicationWindow):
stack_view.add_named(Details(), "details") stack_view.add_named(Details(), "details")
stack_view.add_named(Logs(), "logs") stack_view.add_named(Logs(), "logs")
webview = WebView() webview = WebView(methods=API._registry)
@webview.method
def list_machines(data: None) -> list[str]:
return machines.list.list_machines(".")
stack_view.add_named(webview.get_webview(), "list") stack_view.add_named(webview.get_webview(), "list")

1
pkgs/webview-ui/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
api

View File

@ -9,6 +9,8 @@
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "^20.12.12",
"json-schema-to-ts": "^3.1.0",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
}, },
"devDependencies": { "devDependencies": {
@ -326,6 +328,17 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.24.0", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
@ -1352,6 +1365,14 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@ -2142,6 +2163,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/json-schema-to-ts": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz",
"integrity": "sha512-UeVN/ery4/JeXI8h4rM8yZPxsH+KqPi/84qFxHfTGHZnWnK9D0UU9ZGYO+6XAaJLqCWMiks+ARuFOKAiSxJCHA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -2582,6 +2615,11 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -3008,6 +3046,11 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="
},
"node_modules/ts-interface-checker": { "node_modules/ts-interface-checker": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -3027,6 +3070,11 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.16", "version": "1.0.16",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",

View File

@ -7,7 +7,8 @@
"dev": "vite", "dev": "vite",
"build": "vite build && npm run convert-html", "build": "vite build && npm run convert-html",
"convert-html": "node gtk.webview.js", "convert-html": "node gtk.webview.js",
"serve": "vite preview" "serve": "vite preview",
"check": "tsc --noEmit --skipLibCheck"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
@ -17,10 +18,12 @@
"solid-devtools": "^0.29.2", "solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite-plugin-solid": "^2.8.2", "vite": "^5.0.11",
"vite": "^5.0.11" "vite-plugin-solid": "^2.8.2"
}, },
"dependencies": { "dependencies": {
"@types/node": "^20.12.12",
"json-schema-to-ts": "^3.1.0",
"solid-js": "^1.8.11" "solid-js": "^1.8.11"
} }
} }

View File

@ -1,11 +1,11 @@
import { createSignal, createContext, useContext, JSXElement } from "solid-js"; import { createSignal, createContext, useContext, JSXElement } from "solid-js";
import { PYAPI } from "./message"; import { pyApi } from "./message";
export const makeCountContext = () => { export const makeCountContext = () => {
const [machines, setMachines] = createSignal<string[]>([]); const [machines, setMachines] = createSignal<string[]>([]);
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
PYAPI.list_machines.receive((machines) => { pyApi.list_machines.receive((machines) => {
setLoading(false); setLoading(false);
setMachines(machines); setMachines(machines);
}); });
@ -16,7 +16,7 @@ export const makeCountContext = () => {
getMachines: () => { getMachines: () => {
// When the gtk function sends its data the loading state will be set to false // When the gtk function sends its data the loading state will be set to false
setLoading(true); setLoading(true);
PYAPI.list_machines.dispatch(null); pyApi.list_machines.dispatch(".");
}, },
}, },
] as const; ] as const;

View File

@ -1,22 +1,74 @@
const deserialize = (fn: Function) => (str: string) => { import { FromSchema } from "json-schema-to-ts";
import { schema } from "@/api";
type API = FromSchema<typeof schema>;
type OperationNames = keyof API;
type OperationArgs<T extends OperationNames> = API[T]["argument"];
type OperationResponse<T extends OperationNames> = API[T]["return"];
declare global {
interface Window {
clan: {
[K in OperationNames]: (str: string) => void;
};
webkit: {
messageHandlers: {
gtk: {
postMessage: (message: { method: OperationNames; data: any }) => void;
};
};
};
}
}
function createFunctions<K extends OperationNames>(
operationName: K
): {
dispatch: (args: OperationArgs<K>) => void;
receive: (fn: (response: OperationResponse<K>) => void) => void;
} {
return {
dispatch: (args: OperationArgs<K>) => {
console.log(
`Operation: ${operationName}, Arguments: ${JSON.stringify(args)}`
);
// Send the data to the gtk app
window.webkit.messageHandlers.gtk.postMessage({
method: operationName,
data: args,
});
},
receive: (fn: (response: OperationResponse<K>) => void) => {
window.clan.list_machines = deserialize(fn);
},
};
}
const operations = schema.properties;
const operationNames = Object.keys(operations) as OperationNames[];
type PyApi = {
[K in OperationNames]: {
dispatch: (args: OperationArgs<K>) => void;
receive: (fn: (response: OperationResponse<K>) => void) => void;
};
};
const deserialize =
<T>(fn: (response: T) => void) =>
(str: string) => {
try { try {
fn(JSON.parse(str)); fn(JSON.parse(str) as T);
} catch (e) { } catch (e) {
alert(`Error parsing JSON: ${e}`); alert(`Error parsing JSON: ${e}`);
} }
}; };
export const PYAPI = { // Create the API object
list_machines: {
dispatch: (data: null) => const pyApi: PyApi = {} as PyApi;
// @ts-ignore operationNames.forEach((name) => {
window.webkit.messageHandlers.gtk.postMessage({ pyApi[name] = createFunctions(name);
method: "list_machines", });
data, export { pyApi };
}),
receive: (fn: (response: string[]) => void) => {
// @ts-ignore
window.clan.list_machines = deserialize(fn);
},
},
};

View File

@ -10,6 +10,10 @@
"jsxImportSource": "solid-js", "jsxImportSource": "solid-js",
"types": ["vite/client"], "types": ["vite/client"],
"noEmit": true, "noEmit": true,
"resolveJsonModule": true,
"allowJs": true,
"isolatedModules": true, "isolatedModules": true,
"paths": {
"@/*": ["./*"]}
}, },
} }

View File

@ -1,8 +1,14 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid"; import solidPlugin from "vite-plugin-solid";
// import devtools from "solid-devtools/vite"; // import devtools from "solid-devtools/vite";
import path from "node:path";
export default defineConfig({ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
},
},
plugins: [ plugins: [
/* /*
Uncomment the following line to enable solid-devtools. Uncomment the following line to enable solid-devtools.

View File

@ -1,9 +1,9 @@
{ dream2nix, config, ... }: { dream2nix, config, src, ... }:
{ {
imports = [ dream2nix.modules.dream2nix.WIP-nodejs-builder-v3 ]; imports = [ dream2nix.modules.dream2nix.WIP-nodejs-builder-v3 ];
mkDerivation = { mkDerivation = {
src = ./app; inherit src ;
}; };
deps = deps =
@ -15,7 +15,12 @@
WIP-nodejs-builder-v3 = { WIP-nodejs-builder-v3 = {
packageLockFile = "${config.mkDerivation.src}/package-lock.json"; packageLockFile = "${config.mkDerivation.src}/package-lock.json";
}; };
public.out = {
checkPhase = ''
echo "Running tests"
echo "Tests passed"
'';
};
name = "@clan/webview-ui"; name = "@clan/webview-ui";
version = "0.0.1"; version = "0.0.1";
} }

View File

@ -9,9 +9,28 @@
}: }:
let let
node_modules-dev = config.packages.webview-ui.prepared-dev; node_modules-dev = config.packages.webview-ui.prepared-dev;
src_with_api = pkgs.stdenv.mkDerivation {
name = "with-api";
src = ./app;
buildInputs = [ pkgs.nodejs ];
installPhase = ''
mkdir -p $out
mkdir -p $out/api
cat ${config.packages.clan-ts-api} > $out/api/index.ts
cp -r $src/* $out
ls -la $out/api
'';
};
in in
{ {
packages.webview-ui = inputs.dream2nix.lib.evalModules { packages.webview-ui = inputs.dream2nix.lib.evalModules {
specialArgs = {
src = src_with_api ;
};
packageSets.nixpkgs = inputs.dream2nix.inputs.nixpkgs.legacyPackages.${system}; packageSets.nixpkgs = inputs.dream2nix.inputs.nixpkgs.legacyPackages.${system};
modules = [ ./default.nix ]; modules = [ ./default.nix ];
}; };
@ -28,6 +47,9 @@
echo -n $ID > .dream2nix/.node_modules_id echo -n $ID > .dream2nix/.node_modules_id
echo "Ok: node_modules updated" echo "Ok: node_modules updated"
fi fi
mkdir -p ./app/api
cat ${config.packages.clan-ts-api} > ./app/api/index.ts
''; '';
}; };
}; };