@wandelbots/nova-js 3.9.0-pr.280.2a86180 → 3.9.0-pr.280.5d405fb

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wandelbots/nova-js",
3
3
  "type": "module",
4
- "version": "3.9.0-pr.280.2a86180",
4
+ "version": "3.9.0-pr.280.5d405fb",
5
5
  "description": "Official JS client for the Wandelbots API",
6
6
  "sideEffects": false,
7
7
  "packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d",
@@ -31,10 +31,10 @@
31
31
  "lint": "biome check --error-on-warnings .",
32
32
  "format": "biome format . --write",
33
33
  "knip": "knip",
34
- "build": "tsdown src/index.ts src/lib/deprecated/v1/index.ts src/lib/v2/index.ts --format esm,cjs --clean --sourcemap",
34
+ "build": "tsdown",
35
35
  "test": "pnpm run build && vitest run",
36
36
  "e2e": "vitest run --config vitest.e2e.config.ts",
37
- "checks": "pnpm format && concurrently --group --timings --names audit,lint,tsc,knip,test \"pnpm audit\" \"pnpm lint\" \"pnpm tsc\" \"pnpm knip\" \"pnpm test\""
37
+ "checks": "pnpm format && pnpm build && concurrently --group --timings --names audit,lint,tsc,knip,test \"pnpm audit\" \"pnpm lint\" \"pnpm tsc\" \"pnpm knip\" \"vitest run\""
38
38
  },
39
39
  "repository": {
40
40
  "type": "git",
@@ -51,6 +51,7 @@
51
51
  "@biomejs/biome": "^2.4.15",
52
52
  "@types/lodash-es": "^4.17.12",
53
53
  "@types/node": "^25.9.1",
54
+ "@types/ws": "^8.18.1",
54
55
  "concurrently": "^9.2.1",
55
56
  "conventional-changelog-conventionalcommits": "^9.3.1",
56
57
  "knip": "^6.14.2",
@@ -59,6 +60,7 @@
59
60
  "semantic-release": "^25.0.3",
60
61
  "semantic-release-export-data": "^1.2.0",
61
62
  "tsdown": "^0.22.0",
63
+ "tsnapi": "^0.3.3",
62
64
  "typescript": "^6.0.3",
63
65
  "vite": "^8.0.14",
64
66
  "vitest": "^4.1.7",
@@ -1,4 +1,3 @@
1
- /** biome-ignore-all lint/style/noNonNullAssertion: legacy code */
2
1
  import type { Configuration as BaseConfiguration } from "@wandelbots/nova-api/v2"
3
2
  import type { AxiosRequestConfig } from "axios"
4
3
  import axios, { isAxiosError } from "axios"
@@ -10,18 +9,13 @@ import { parseNovaInstanceUrl } from "../converters"
10
9
  import { NovaAPIClient } from "./NovaAPIClient"
11
10
  import { MockNovaInstance } from "./mock/MockNovaInstance"
12
11
 
13
- export type NovaClientConfig = {
12
+ export type NovaConfig = {
14
13
  /**
15
14
  * Url of the deployed NOVA instance to connect to
16
15
  * e.g. https://saeattii.instance.wandelbots.io
17
16
  */
18
17
  instanceUrl: string
19
18
 
20
- /**
21
- * Id of the cell to connect to
22
- */
23
- cellId: string
24
-
25
19
  /**
26
20
  * Access token for Bearer authentication.
27
21
  * If running on a NOVA instance, this can be automatically retrieved from
@@ -36,13 +30,13 @@ export type NovaClientConfig = {
36
30
  */
37
31
  export class Nova {
38
32
  readonly api: NovaAPIClient
39
- readonly config: NovaClientConfig
33
+ readonly config: NovaConfig
40
34
  readonly mock?: MockNovaInstance
41
35
  readonly instanceUrl: URL
42
36
  authPromise: Promise<string | null> | null = null
43
37
  accessToken: string | null = null
44
38
 
45
- constructor(config: NovaClientConfig) {
39
+ constructor(config: NovaConfig) {
46
40
  this.config = config
47
41
  this.accessToken =
48
42
  config.accessToken ||
@@ -116,7 +110,7 @@ export class Nova {
116
110
  )
117
111
  }
118
112
 
119
- this.api = new NovaAPIClient(this.config.cellId, {
113
+ this.api = new NovaAPIClient({
120
114
  ...config,
121
115
  basePath: urlJoin(this.instanceUrl.href, "/api/v2"),
122
116
  isJsonMime: (mime: string) => {
@@ -126,7 +120,10 @@ export class Nova {
126
120
  ...(this.mock
127
121
  ? ({
128
122
  adapter: (config) => {
129
- return this.mock!.handleAPIRequest(config)
123
+ if (!this.mock) {
124
+ throw new Error("Mock adapter used without a mock instance")
125
+ }
126
+ return this.mock.handleAPIRequest(config)
130
127
  },
131
128
  } satisfies AxiosRequestConfig)
132
129
  : {}),
@@ -165,13 +162,7 @@ export class Nova {
165
162
  }
166
163
 
167
164
  makeWebsocketURL(path: string): string {
168
- const url = new URL(
169
- urlJoin(
170
- this.instanceUrl.href,
171
- `/api/v2/cells/${this.config.cellId}`,
172
- path,
173
- ),
174
- )
165
+ const url = new URL(urlJoin(this.instanceUrl.href, `/api/v2`, path))
175
166
  url.protocol = url.protocol.replace("http", "ws")
176
167
  url.protocol = url.protocol.replace("https", "wss")
177
168
 
@@ -1,43 +1,13 @@
1
- /** biome-ignore-all lint/suspicious/noExplicitAny: legacy code */
2
- /** biome-ignore-all lint/style/noNonNullAssertion: legacy code */
3
1
  import type {
4
2
  BaseAPI,
5
3
  Configuration as BaseConfiguration,
6
4
  } from "@wandelbots/nova-api/v2"
7
- import {
8
- ApplicationApi,
9
- BUSInputsOutputsApi,
10
- CellApi,
11
- ControllerApi,
12
- ControllerInputsOutputsApi,
13
- JoggingApi,
14
- KinematicsApi,
15
- LicenseApi,
16
- MotionGroupApi,
17
- MotionGroupModelsApi,
18
- NOVACloudApi,
19
- ProgramApi,
20
- RobotConfigurationsApi,
21
- StoreCollisionComponentsApi,
22
- StoreCollisionSetupsApi,
23
- StoreObjectApi,
24
- SystemApi,
25
- TrajectoryCachingApi,
26
- TrajectoryExecutionApi,
27
- TrajectoryPlanningApi,
28
- VersionApi,
29
- VirtualControllerApi,
30
- VirtualControllerBehaviorApi,
31
- VirtualControllerInputsOutputsApi,
32
- } from "@wandelbots/nova-api/v2"
5
+ import * as novaApiV2 from "@wandelbots/nova-api/v2"
33
6
  import type { AxiosInstance } from "axios"
34
7
  import axios from "axios"
35
8
 
36
- type OmitFirstArg<F> = F extends (x: any, ...args: infer P) => infer R
37
- ? (...args: P) => R
38
- : never
39
-
40
- type UnwrapAxiosResponseReturn<T> = T extends (...a: any) => any
9
+ // biome-ignore lint/suspicious/noExplicitAny: metamagic
10
+ type UnwrapAxiosResponseReturn<T> = T extends (...a: any[]) => any
41
11
  ? (
42
12
  ...a: Parameters<T>
43
13
  ) => Promise<Awaited<ReturnType<T>> extends { data: infer D } ? D : never>
@@ -48,107 +18,140 @@ type WithUnwrappedAxiosResponse<T> = {
48
18
  }
49
19
 
50
20
  /**
51
- * API client providing type-safe access to all the endpoints of a NOVA
52
- * instance.
21
+ * Filters nova-api/v2 exports to just the API class constructors,
22
+ * excluding helper factories and param creators.
53
23
  */
54
- export class NovaAPIClient {
55
- constructor(
56
- readonly cellId: string,
57
- readonly opts: BaseConfiguration & {
58
- axiosInstance?: AxiosInstance
59
- mock?: boolean
60
- },
61
- ) {}
62
-
63
- private withUnwrappedResponsesOnly<T extends BaseAPI>(
64
- ApiConstructor: new (
65
- config: BaseConfiguration,
66
- basePath: string,
67
- axios: AxiosInstance,
68
- ) => T,
69
- ) {
70
- const apiClient = new ApiConstructor(
71
- {
72
- ...this.opts,
73
- isJsonMime: (mime: string) => {
74
- return mime === "application/json"
75
- },
76
- },
77
- this.opts.basePath ?? "",
78
- this.opts.axiosInstance ?? axios.create(),
79
- ) as {
80
- [key: string | symbol]: any
81
- }
82
-
83
- for (const key of Reflect.ownKeys(Reflect.getPrototypeOf(apiClient)!)) {
84
- if (key !== "constructor" && typeof apiClient[key] === "function") {
85
- const originalFunction = apiClient[key]
86
- apiClient[key] = (...args: any[]) => {
87
- return originalFunction
88
- .apply(apiClient, args)
89
- .then((res: any) => res.data)
90
- }
91
- }
92
- }
93
-
94
- return apiClient as WithUnwrappedAxiosResponse<T>
95
- }
96
-
97
- readonly system = this.withUnwrappedResponsesOnly(SystemApi)
98
- readonly cell = this.withUnwrappedResponsesOnly(CellApi)
99
-
100
- readonly motionGroup = this.withUnwrappedResponsesOnly(MotionGroupApi)
101
- readonly motionGroupModels =
102
- this.withUnwrappedResponsesOnly(MotionGroupModelsApi)
103
-
104
- readonly controller = this.withUnwrappedResponsesOnly(ControllerApi)
105
-
106
- readonly controllerIOs = this.withUnwrappedResponsesOnly(
107
- ControllerInputsOutputsApi,
108
- )
109
-
110
- readonly trajectoryPlanning = this.withUnwrappedResponsesOnly(
111
- TrajectoryPlanningApi,
112
- )
113
- readonly trajectoryExecution = this.withUnwrappedResponsesOnly(
114
- TrajectoryExecutionApi,
115
- )
116
- readonly trajectoryCaching =
117
- this.withUnwrappedResponsesOnly(TrajectoryCachingApi)
118
-
119
- readonly application = this.withUnwrappedResponsesOnly(ApplicationApi)
120
- readonly applicationGlobal = this.withUnwrappedResponsesOnly(ApplicationApi)
24
+ type ApiConstructors = {
25
+ [K in keyof typeof novaApiV2 as K extends `${string}Api`
26
+ ? K extends
27
+ | `${string}Factory${string}`
28
+ | `${string}Fp${string}`
29
+ | `${string}ParamCreator${string}`
30
+ ? never
31
+ : K
32
+ : never]: (typeof novaApiV2)[K]
33
+ }
121
34
 
122
- readonly jogging = this.withUnwrappedResponsesOnly(JoggingApi)
35
+ /**
36
+ * Lowercases leading uppercase characters to produce a camelCase property name.
37
+ * e.g. "BUSInputsOutputs" -> "busInputsOutputs", "NOVACloud" -> "novaCloud",
38
+ * "Application" -> "application"
39
+ */
40
+ type CamelCase<S extends string> =
41
+ S extends `${Uppercase<infer A>}${Uppercase<infer B>}${infer Rest}`
42
+ ? Rest extends `${Lowercase<string>}${string}`
43
+ ? `${Lowercase<A>}${CamelCase<`${Uppercase<B>}${Rest}`>}`
44
+ : `${Lowercase<A>}${CamelCase<`${Uppercase<B>}${Rest}`>}`
45
+ : Uncapitalize<S>
123
46
 
124
- readonly kinematics = this.withUnwrappedResponsesOnly(KinematicsApi)
47
+ /**
48
+ * Maps API class names to property names by stripping "Api" and converting
49
+ * the leading acronym to lowercase camelCase.
50
+ */
51
+ type ApiProperties = {
52
+ readonly [K in keyof ApiConstructors as K extends `${infer P}Api`
53
+ ? CamelCase<P>
54
+ : never]: ApiConstructors[K] extends new (
55
+ // biome-ignore lint/suspicious/noExplicitAny: needed for contravariant parameter matching
56
+ ...args: any[]
57
+ ) => infer I
58
+ ? WithUnwrappedAxiosResponse<I>
59
+ : never
60
+ }
125
61
 
126
- readonly busInputsOutputs =
127
- this.withUnwrappedResponsesOnly(BUSInputsOutputsApi)
62
+ type NovaAPIClientOpts = BaseConfiguration & {
63
+ axiosInstance?: AxiosInstance
64
+ mock?: boolean
65
+ }
128
66
 
129
- readonly virtualController =
130
- this.withUnwrappedResponsesOnly(VirtualControllerApi)
131
- readonly virtualControllerBehavior = this.withUnwrappedResponsesOnly(
132
- VirtualControllerBehaviorApi,
133
- )
134
- readonly virtualControllerIOs = this.withUnwrappedResponsesOnly(
135
- VirtualControllerInputsOutputsApi,
67
+ function isApiClass(
68
+ name: string,
69
+ value: unknown,
70
+ ): value is new (
71
+ config: BaseConfiguration,
72
+ basePath: string,
73
+ axios: AxiosInstance,
74
+ ) => BaseAPI {
75
+ return (
76
+ name.endsWith("Api") &&
77
+ typeof value === "function" &&
78
+ !name.includes("Factory") &&
79
+ !name.includes("Fp") &&
80
+ !name.includes("ParamCreator")
136
81
  )
82
+ }
137
83
 
138
- readonly storeObject = this.withUnwrappedResponsesOnly(StoreObjectApi)
139
- readonly storeCollisionComponents = this.withUnwrappedResponsesOnly(
140
- StoreCollisionComponentsApi,
141
- )
142
- readonly storeCollisionSetups = this.withUnwrappedResponsesOnly(
143
- StoreCollisionSetupsApi,
144
- )
84
+ function wrapApi<T extends BaseAPI>(
85
+ ApiConstructor: new (
86
+ config: BaseConfiguration,
87
+ basePath: string,
88
+ axios: AxiosInstance,
89
+ ) => T,
90
+ opts: NovaAPIClientOpts,
91
+ ): WithUnwrappedAxiosResponse<T> {
92
+ const apiClient = new ApiConstructor(
93
+ {
94
+ ...opts,
95
+ isJsonMime: (mime: string) => mime === "application/json",
96
+ },
97
+ opts.basePath ?? "",
98
+ opts.axiosInstance ?? axios.create(),
99
+ ) as Record<string | symbol, unknown>
100
+
101
+ for (const key of Reflect.ownKeys(
102
+ Reflect.getPrototypeOf(apiClient) as object,
103
+ )) {
104
+ if (key !== "constructor" && typeof apiClient[key] === "function") {
105
+ const originalFunction = apiClient[key] as (
106
+ ...args: unknown[]
107
+ ) => Promise<{ data: unknown }>
108
+ apiClient[key] = (...args: unknown[]) =>
109
+ originalFunction.apply(apiClient, args).then((res) => res.data)
110
+ }
111
+ }
145
112
 
146
- readonly program = this.withUnwrappedResponsesOnly(ProgramApi)
113
+ return apiClient as WithUnwrappedAxiosResponse<T>
114
+ }
147
115
 
148
- readonly license = this.withUnwrappedResponsesOnly(LicenseApi)
149
- readonly novaCloud = this.withUnwrappedResponsesOnly(NOVACloudApi)
150
- readonly robotConfigurations = this.withUnwrappedResponsesOnly(
151
- RobotConfigurationsApi,
152
- )
153
- readonly version = this.withUnwrappedResponsesOnly(VersionApi)
116
+ /**
117
+ * API client providing type-safe access to all the endpoints of a NOVA
118
+ * instance. API sections are auto-discovered from @wandelbots/nova-api/v2
119
+ * at runtime, so new sections added upstream are exposed automatically.
120
+ */
121
+ export const NovaAPIClient = class NovaAPIClient {
122
+ readonly opts: NovaAPIClientOpts
123
+
124
+ constructor(opts: NovaAPIClientOpts) {
125
+ this.opts = opts
126
+
127
+ for (const [name, value] of Object.entries(novaApiV2)) {
128
+ if (isApiClass(name, value)) {
129
+ const stripped = name.slice(0, -3)
130
+ // Lowercase leading acronym run: "BUSInputsOutputs" -> "busInputsOutputs"
131
+ let i = 0
132
+ while (
133
+ i < stripped.length &&
134
+ stripped[i] === stripped[i].toUpperCase() &&
135
+ stripped[i] !== stripped[i].toLowerCase()
136
+ ) {
137
+ i++
138
+ }
139
+ // If multiple uppercase chars, keep the last one uppercase (it starts the next word)
140
+ const boundary = i > 1 ? i - 1 : i
141
+ const propName =
142
+ stripped.slice(0, boundary).toLowerCase() + stripped.slice(boundary)
143
+ // biome-ignore lint/suspicious/noExplicitAny: dynamically assigning discovered API properties
144
+ ;(this as any)[propName] = wrapApi(value, opts)
145
+ }
146
+ }
147
+ }
148
+ } as {
149
+ new (
150
+ opts: NovaAPIClientOpts,
151
+ ): ApiProperties & {
152
+ readonly cellId: string
153
+ readonly opts: NovaAPIClientOpts
154
+ }
154
155
  }
156
+
157
+ export type NovaAPIClient = InstanceType<typeof NovaAPIClient>
@@ -3,5 +3,5 @@ export * from "../deprecated/v2/NovaCellAPIClient"
3
3
  export * from "../deprecated/v2/NovaClient"
4
4
  export * from "../deprecated/v2/wandelscriptUtils"
5
5
  export { Nova } from "./Nova"
6
- export type { NovaClientConfig } from "./Nova"
6
+ export type { NovaConfig } from "./Nova"
7
7
  export { NovaAPIClient } from "./NovaAPIClient"