@nebulahome/package-sdk 0.0.0-stage → 1.0.0
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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/dist/chunks/context.d.ts +369 -0
- package/dist/chunks/host.js +53 -0
- package/dist/chunks/json.d.ts +657 -0
- package/dist/chunks/node-build.js +20009 -0
- package/dist/chunks/props.d.ts +68 -0
- package/dist/chunks/version.js +12 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3556 -0
- package/dist/components.d.ts +381 -0
- package/dist/components.js +210 -0
- package/dist/index.d.ts +205 -0
- package/dist/index.js +243 -0
- package/dist/server.d.ts +30 -0
- package/dist/server.js +17 -0
- package/dist/styles.css +549 -0
- package/dist/testing.d.ts +117 -0
- package/dist/testing.js +608 -0
- package/dist/ui.d.ts +320 -0
- package/dist/ui.js +180 -0
- package/dist/vite.d.ts +88 -0
- package/dist/vite.js +2 -0
- package/package.json +110 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @nebulahome/package-sdk
|
|
2
|
+
|
|
3
|
+
The SDK's version is the version of the package API the platform implements: a minor version adds
|
|
4
|
+
to it, a major one breaks it, and a patch leaves it as it is.
|
|
5
|
+
|
|
6
|
+
## 1.0.0
|
|
7
|
+
|
|
8
|
+
The first public release.
|
|
9
|
+
|
|
10
|
+
- `nebula.config.ts`: `definePackage`, `defineSettings`, `defineCard`, `defineEntityType`,
|
|
11
|
+
`definePage`, `defineContainer`, `defineCompose` and `field` declare a package, from which the
|
|
12
|
+
build writes `nebula.package.json` and `setup.json`.
|
|
13
|
+
- `@nebulahome/package-sdk/server`: `defineServer`, and the context `start` is given - settings,
|
|
14
|
+
devices and entities, the package's own kinds of entity, methods for the interface and services
|
|
15
|
+
between packages.
|
|
16
|
+
- `@nebulahome/package-sdk/ui`: the props of a page, a card and an entity view, and the hooks for
|
|
17
|
+
the package, its methods and settings, the house, the viewer, the theme and navigation.
|
|
18
|
+
- `@nebulahome/package-sdk/components` and `/styles.css`: the part of the interface's design
|
|
19
|
+
system a package may draw with, and its Tailwind tokens.
|
|
20
|
+
- `@nebulahome/package-sdk/vite`: `nebula()`, the Vite plugin that builds the interface with its
|
|
21
|
+
CSS scoped to the package and bundles the server.
|
|
22
|
+
- `nebulahome-package validate`, `build` and `pack`: check the declaration the way the platform
|
|
23
|
+
does, build the release into `dist/`, and pack it into the `<id>-<version>.tgz` a GitHub release
|
|
24
|
+
carries with its entry for the package index.
|
|
25
|
+
- `@nebulahome/package-sdk/testing`: `createTestContext`, to run `start` in a unit test with every
|
|
26
|
+
effect recorded.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NebulaHome
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nebulahome/package-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
What a NebulaHome package is written with. A **package** is something an installation adds to
|
|
4
|
+
itself that is not a device brand - a broker, a recorder, a 3D view of the house, a music player.
|
|
5
|
+
It runs a Node process, containers or a compose file, or nothing, and may add a page of its own,
|
|
6
|
+
dashboard cards and views of its own kinds of entity to the interface.
|
|
7
|
+
|
|
8
|
+
Start from the template, [nebulahome/pkg-template](https://github.com/nebulahome/pkg-template):
|
|
9
|
+
a small package that uses each part of the SDK once. The full reference is `PACKAGES_DOCS.md` in
|
|
10
|
+
the NebulaHome documentation.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @nebulahome/package-sdk
|
|
14
|
+
pnpm add -D vite typescript react @types/react @vitejs/plugin-react tailwindcss @tailwindcss/vite
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## A package in three files
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// nebula.config.ts - what the package is; the build writes the manifest from it
|
|
21
|
+
import { defineCard, definePackage, defineSettings, field } from '@nebulahome/package-sdk';
|
|
22
|
+
|
|
23
|
+
export const settings = defineSettings({
|
|
24
|
+
city: field.text({ label: 'City', isRequired: true }),
|
|
25
|
+
refreshMinutes: field.number({ label: 'Refresh every', unit: 'min', default: 30, isLive: true }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const forecast = defineCard({
|
|
29
|
+
id: 'forecast',
|
|
30
|
+
name: 'Forecast',
|
|
31
|
+
icon: 'cloud-sun',
|
|
32
|
+
sizes: ['wide'],
|
|
33
|
+
options: { days: field.number({ label: 'Days', min: 1, max: 7, default: 3 }) },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export default definePackage({
|
|
37
|
+
id: 'weather',
|
|
38
|
+
name: 'Weather',
|
|
39
|
+
description: 'Forecasts for where the house is.',
|
|
40
|
+
category: 'climate',
|
|
41
|
+
platform: '^1.0.0',
|
|
42
|
+
server: './src/server/index.ts',
|
|
43
|
+
settings,
|
|
44
|
+
cards: [forecast],
|
|
45
|
+
ui: { cards: { forecast: async () => import('./src/ui/forecast-card.js') } },
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/server/index.ts - runs in a process of its own
|
|
51
|
+
import { defineServer } from '@nebulahome/package-sdk/server';
|
|
52
|
+
|
|
53
|
+
import { settings } from '../../nebula.config.js';
|
|
54
|
+
|
|
55
|
+
export default defineServer({
|
|
56
|
+
settings,
|
|
57
|
+
start(ctx) {
|
|
58
|
+
ctx.rpc.method('forecast', { access: 'read' }, async () => fetchDays(ctx.settings.city));
|
|
59
|
+
|
|
60
|
+
return undefined;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
// src/ui/forecast-card.tsx - any React, any CSS; the interface draws only the frame
|
|
67
|
+
import { usePackageRpc, type CardProps } from '@nebulahome/package-sdk/ui';
|
|
68
|
+
|
|
69
|
+
import type { forecast } from '../../nebula.config.js';
|
|
70
|
+
|
|
71
|
+
export default function ForecastCard({ options, title, name }: CardProps<typeof forecast>) {
|
|
72
|
+
const days = usePackageRpc<Day[]>('forecast');
|
|
73
|
+
|
|
74
|
+
return <Strip title={title ?? name} days={days.data?.slice(0, options.days)} />;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// vite.config.ts
|
|
80
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
81
|
+
import react from '@vitejs/plugin-react';
|
|
82
|
+
import { defineConfig } from 'vite';
|
|
83
|
+
|
|
84
|
+
import { nebula } from '@nebulahome/package-sdk/vite';
|
|
85
|
+
|
|
86
|
+
export default defineConfig({ plugins: [react(), tailwindcss(), nebula()] });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Entry points
|
|
90
|
+
|
|
91
|
+
| Import | What |
|
|
92
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `@nebulahome/package-sdk` | `definePackage`, `defineSettings`, `defineCard`, `defineEntityType`, `definePage`, `defineContainer`, `defineCompose`, `field`, entity id helpers, `SDK_VERSION`, the value types. |
|
|
94
|
+
| `/server` | `defineServer`, and the context `start` is given: settings (live or restart), devices and entities, typed custom entities, the rest of the house, methods for the interface, services between packages. |
|
|
95
|
+
| `/ui` | The props of a page, a card and an entity view; hooks for the package, its methods, its settings, the house, the viewer, the theme; navigation inside the package's page and to the platform's places. |
|
|
96
|
+
| `/components` | The part of the interface's design system a package may draw with. |
|
|
97
|
+
| `/vite` | `nebula()`, the Vite plugin that builds both halves: the interface in chunks with its CSS scoped to the package, the server bundled whole. |
|
|
98
|
+
| `/testing` | `createTestContext()`: run `start` in a unit test, every effect recorded. |
|
|
99
|
+
| `/styles.css` | The interface's Tailwind tokens, to `@reference`. |
|
|
100
|
+
|
|
101
|
+
The `nebulahome-package` command checks, builds and packs a package:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
nebulahome-package validate # the declaration holds together, as the platform checks it
|
|
105
|
+
nebulahome-package build # dist/: what an installation runs
|
|
106
|
+
nebulahome-package pack # <id>-<version>.tgz and its entry for the package index
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Compatibility
|
|
110
|
+
|
|
111
|
+
The SDK follows SemVer, and its types are the contract: the platform implements them, so the
|
|
112
|
+
package and the platform cannot drift apart unnoticed. A package's manifest records the SDK it was
|
|
113
|
+
built with (`sdk: ^<major>.<minor>.0`), and the platform refuses to install or start a release it
|
|
114
|
+
does not implement. A minor of the SDK is published only together with a platform release that
|
|
115
|
+
implements it. React's major is part of the contract.
|
|
116
|
+
|
|
117
|
+
## Releasing the SDK
|
|
118
|
+
|
|
119
|
+
For the platform's maintainers. The packet-manager reads the version it implements from this
|
|
120
|
+
`package.json`, so the version is raised by hand:
|
|
121
|
+
|
|
122
|
+
- **patch** - a fix, or a change to the command, the build, the tests' helpers or the
|
|
123
|
+
documentation that leaves what a package can use as it is. Packages keep declaring
|
|
124
|
+
`^<major>.<minor>.0`, so it is released whenever it is ready.
|
|
125
|
+
- **minor** - the API grows. Raise it in the first commit that grows it, so a package using the
|
|
126
|
+
new part is never installed on a platform without it; release it with the platform that
|
|
127
|
+
implements it.
|
|
128
|
+
- **major** - the API breaks: packages built before are refused until they are built again.
|
|
129
|
+
|
|
130
|
+
Every change of the release goes into `CHANGELOG.md`. Then, from the commit released:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
git tag package-sdk@<version> && git push origin package-sdk@<version>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`.github/workflows/release-package-sdk.yml` checks the tag says the package's version, runs the
|
|
137
|
+
SDK's gates and publishes a GitHub release with the archive npm will receive. The maintainer
|
|
138
|
+
publishes that archive, signed in to npm:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
npm publish nebulahome-package-sdk-<version>.tgz
|
|
142
|
+
```
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { G as CapabilityState, V as CapabilityCommand, X as EntityStateClass, Y as DeclaredCapabilities, a as SettingsOf, ct as EntityDomain, d as EntityTypeDefinition, et as ReportedCapabilityState, f as ValuesOf, i as SettingsDefinition, lt as EntityId, r as JsonValue, s as ArgsOf, t as JsonObject, u as CommandsOf } from "./json.js";
|
|
2
|
+
//#region src/schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* What checks a value that crosses into a package - a method's input, a service's answer: any
|
|
5
|
+
* [Standard Schema](https://standardschema.dev) - zod 4, valibot, arktype - or an object with
|
|
6
|
+
* zod's `safeParse`. The SDK carries no validation library of its own.
|
|
7
|
+
*
|
|
8
|
+
* The Standard Schema interface is written out here, as its authors ask libraries to do, rather
|
|
9
|
+
* than taken as a dependency.
|
|
10
|
+
*/
|
|
11
|
+
/** One problem a Standard Schema found. */
|
|
12
|
+
type SchemaIssue = {
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly path?: readonly (PropertyKey | {
|
|
15
|
+
readonly key: PropertyKey;
|
|
16
|
+
})[] | undefined;
|
|
17
|
+
};
|
|
18
|
+
/** What a Standard Schema's `validate` gives back. */
|
|
19
|
+
type StandardResult<TOutput> = {
|
|
20
|
+
readonly value: TOutput;
|
|
21
|
+
readonly issues?: undefined;
|
|
22
|
+
} | {
|
|
23
|
+
readonly issues: readonly SchemaIssue[];
|
|
24
|
+
};
|
|
25
|
+
/** The Standard Schema interface, version 1. */
|
|
26
|
+
type StandardSchemaV1<TInput = unknown, TOutput = TInput> = {
|
|
27
|
+
readonly '~standard': {
|
|
28
|
+
readonly version: 1;
|
|
29
|
+
readonly vendor: string;
|
|
30
|
+
readonly validate: (value: unknown) => StandardResult<TOutput> | Promise<StandardResult<TOutput>>;
|
|
31
|
+
readonly types?: {
|
|
32
|
+
readonly input: TInput;
|
|
33
|
+
readonly output: TOutput;
|
|
34
|
+
} | undefined;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/** An object that checks a value the way zod's `safeParse` does. */
|
|
38
|
+
type SafeParser<TOutput> = {
|
|
39
|
+
readonly safeParse: (value: unknown) => {
|
|
40
|
+
readonly success: true;
|
|
41
|
+
readonly data: TOutput;
|
|
42
|
+
} | {
|
|
43
|
+
readonly success: false;
|
|
44
|
+
readonly error: unknown;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/** What the SDK takes to check a value: a Standard Schema, or a `safeParse`. */
|
|
48
|
+
type Schema<TOutput> = StandardSchemaV1<unknown, TOutput> | SafeParser<TOutput>;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/server/context.d.ts
|
|
51
|
+
/** A value, or a promise of one. */
|
|
52
|
+
type Awaitable<TValue> = TValue | Promise<TValue>;
|
|
53
|
+
/** Stops what a registration started: a handler, a subscription. */
|
|
54
|
+
type Unsubscribe = () => void;
|
|
55
|
+
/** One log method: `(fields, message?)` or `(message)`. */
|
|
56
|
+
type LogMethod = {
|
|
57
|
+
(fields: object, message?: string): void;
|
|
58
|
+
(message: string): void;
|
|
59
|
+
};
|
|
60
|
+
/** The package's logger: its lines are the package's log on its page. */
|
|
61
|
+
type Logger = {
|
|
62
|
+
readonly trace: LogMethod;
|
|
63
|
+
readonly debug: LogMethod;
|
|
64
|
+
readonly info: LogMethod;
|
|
65
|
+
readonly warn: LogMethod;
|
|
66
|
+
readonly error: LogMethod;
|
|
67
|
+
readonly fatal: LogMethod;
|
|
68
|
+
/**
|
|
69
|
+
* A logger whose every line carries these fields too.
|
|
70
|
+
*
|
|
71
|
+
* @param fields - What each line adds.
|
|
72
|
+
* @returns The logger.
|
|
73
|
+
*/
|
|
74
|
+
readonly child: (fields: Record<string, unknown>) => Logger;
|
|
75
|
+
};
|
|
76
|
+
/** One entity of a platform capability, as `devices.sync` takes it. */
|
|
77
|
+
type CapabilityEntityInput = {
|
|
78
|
+
/** What follows the package's prefix in its id: `temperature` → `sensor.weather_temperature`. */
|
|
79
|
+
readonly objectId: string;
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly domain: EntityDomain;
|
|
82
|
+
readonly capabilities: DeclaredCapabilities;
|
|
83
|
+
readonly icon?: string;
|
|
84
|
+
/**
|
|
85
|
+
* The unit the device gives its numbers in. For a sensor of a class the platform knows, the
|
|
86
|
+
* platform registers the class's own unit and converts every number reported - kW to W, °F to
|
|
87
|
+
* °C - so report what the device says, as it says it.
|
|
88
|
+
*/
|
|
89
|
+
readonly unitOfMeasurement?: string;
|
|
90
|
+
/**
|
|
91
|
+
* What the number measures: `temperature`, `power`, `energy`. A unit the class cannot be
|
|
92
|
+
* converted from drops the class, and the number is kept as it comes.
|
|
93
|
+
*/
|
|
94
|
+
readonly deviceClass?: string;
|
|
95
|
+
/** How its numbers add up; the class's own when not said. */
|
|
96
|
+
readonly stateClass?: EntityStateClass;
|
|
97
|
+
readonly displayPrecision?: number;
|
|
98
|
+
};
|
|
99
|
+
/** One entity of one of the package's own entity types, as `devices.sync` takes it. */
|
|
100
|
+
type CustomEntityInput = {
|
|
101
|
+
readonly objectId: string;
|
|
102
|
+
readonly name: string;
|
|
103
|
+
/** The entity type: its domain is `custom`, its capability the type's. */
|
|
104
|
+
readonly type: EntityTypeDefinition;
|
|
105
|
+
readonly icon?: string;
|
|
106
|
+
};
|
|
107
|
+
/** One entity, as `devices.sync` takes it. */
|
|
108
|
+
type EntityInput = CapabilityEntityInput | CustomEntityInput;
|
|
109
|
+
/** One device, as `devices.sync` takes it. */
|
|
110
|
+
type DeviceInput = {
|
|
111
|
+
/** Its own id at the package: stable, what finds it again after a restart. */
|
|
112
|
+
readonly externalId: string;
|
|
113
|
+
/** What it starts as called; the owner's name, once given, outlives it. */
|
|
114
|
+
readonly name: string;
|
|
115
|
+
readonly icon?: string;
|
|
116
|
+
readonly manufacturer?: string;
|
|
117
|
+
readonly model?: string;
|
|
118
|
+
readonly swVersion?: string;
|
|
119
|
+
readonly entities: readonly EntityInput[];
|
|
120
|
+
};
|
|
121
|
+
/** What the platform did with the package's devices. */
|
|
122
|
+
type SyncResult = {
|
|
123
|
+
readonly devices: readonly {
|
|
124
|
+
readonly deviceId: string;
|
|
125
|
+
readonly externalId: string;
|
|
126
|
+
readonly entityIds: readonly EntityId[];
|
|
127
|
+
}[];
|
|
128
|
+
/** Entities written by no one, and why: log them. */
|
|
129
|
+
readonly refused: readonly {
|
|
130
|
+
readonly entityId: string;
|
|
131
|
+
readonly reason: string;
|
|
132
|
+
}[];
|
|
133
|
+
/** Devices gone from the list but kept, because an alias reads one of their entities. */
|
|
134
|
+
readonly kept: readonly string[];
|
|
135
|
+
};
|
|
136
|
+
/** What a report may say besides the state. */
|
|
137
|
+
type ReportOptions = {
|
|
138
|
+
/** What no capability covers, as JSON. */
|
|
139
|
+
readonly attributes?: JsonObject;
|
|
140
|
+
/** False while the thing the entity stands for cannot be reached. */
|
|
141
|
+
readonly isAvailable?: boolean;
|
|
142
|
+
};
|
|
143
|
+
/** One entity of the house, as it stands. */
|
|
144
|
+
type EntitySnapshot = {
|
|
145
|
+
readonly entityId: EntityId;
|
|
146
|
+
readonly state: CapabilityState;
|
|
147
|
+
readonly attributes: JsonObject;
|
|
148
|
+
readonly isAvailable: boolean;
|
|
149
|
+
/** When its state last differed, ISO 8601. */
|
|
150
|
+
readonly lastChanged: string;
|
|
151
|
+
/** When it was last reported, ISO 8601. */
|
|
152
|
+
readonly lastUpdated: string;
|
|
153
|
+
};
|
|
154
|
+
/** A change of an entity's state. */
|
|
155
|
+
type StateChange = {
|
|
156
|
+
readonly entityId: EntityId;
|
|
157
|
+
readonly current: EntitySnapshot;
|
|
158
|
+
/** What it was; absent on the first report after the platform started. */
|
|
159
|
+
readonly previous?: EntitySnapshot;
|
|
160
|
+
};
|
|
161
|
+
/** Which entities: of a domain, or by id. Neither is every entity. */
|
|
162
|
+
type EntityFilter = {
|
|
163
|
+
readonly domain?: EntityDomain;
|
|
164
|
+
readonly entityIds?: readonly EntityId[];
|
|
165
|
+
};
|
|
166
|
+
/** Who may call a method: whoever may read packages, or whoever may configure them. */
|
|
167
|
+
type RpcAccess = 'read' | 'configure';
|
|
168
|
+
/** Who called a method of the package's interface. */
|
|
169
|
+
type RpcCaller = {
|
|
170
|
+
readonly userId: string;
|
|
171
|
+
readonly username: string;
|
|
172
|
+
readonly role: string;
|
|
173
|
+
};
|
|
174
|
+
/** What a method's handler is told besides its input. */
|
|
175
|
+
type RpcCall = {
|
|
176
|
+
readonly caller: RpcCaller;
|
|
177
|
+
};
|
|
178
|
+
/** What a service's handler is told besides its input. */
|
|
179
|
+
type ServiceCall = {
|
|
180
|
+
/** The package or service that called, by its id. */
|
|
181
|
+
readonly caller: string;
|
|
182
|
+
};
|
|
183
|
+
/** A change of the package's settings that reached the running process. */
|
|
184
|
+
type SettingsChange<TValues> = {
|
|
185
|
+
/** Every setting, as it stands now. */
|
|
186
|
+
readonly values: TValues;
|
|
187
|
+
/** The keys that changed. */
|
|
188
|
+
readonly changed: readonly (keyof TValues & string)[];
|
|
189
|
+
};
|
|
190
|
+
/** The values of the settings a server declared, or plain JSON when it declared none. */
|
|
191
|
+
type ServerSettings<TSettings> = [TSettings] extends [SettingsDefinition] ? SettingsOf<TSettings> : JsonObject;
|
|
192
|
+
/** A command of one of the package's entity types, as its handler is given it. */
|
|
193
|
+
type CustomCommand<TArgs> = {
|
|
194
|
+
readonly entityId: EntityId;
|
|
195
|
+
readonly objectId: string;
|
|
196
|
+
readonly args: TArgs;
|
|
197
|
+
};
|
|
198
|
+
/** The entities of one of the package's entity types, typed by its declaration. */
|
|
199
|
+
type CustomEntities<TType extends EntityTypeDefinition> = {
|
|
200
|
+
/**
|
|
201
|
+
* The id of one of the package's entities of this type.
|
|
202
|
+
*
|
|
203
|
+
* @param objectId - What follows the package's prefix.
|
|
204
|
+
* @returns `custom.<prefix>_<objectId>`.
|
|
205
|
+
*/
|
|
206
|
+
readonly entityId: (objectId: string) => EntityId;
|
|
207
|
+
/**
|
|
208
|
+
* Reports some or all of an entity's values; an optional field may be null.
|
|
209
|
+
*
|
|
210
|
+
* @param entityId - The entity.
|
|
211
|
+
* @param values - The values that changed, or all of them.
|
|
212
|
+
* @param options - Its attributes and availability.
|
|
213
|
+
*/
|
|
214
|
+
readonly report: (entityId: EntityId, values: Partial<ValuesOf<TType>>, options?: ReportOptions) => void;
|
|
215
|
+
/**
|
|
216
|
+
* Handles one of the type's commands; a throw is the command's failure.
|
|
217
|
+
*
|
|
218
|
+
* @param command - The command's id.
|
|
219
|
+
* @param handler - What it does with the entity and the arguments.
|
|
220
|
+
* @returns What stops the handler.
|
|
221
|
+
* @throws {Error} When the command has a handler already.
|
|
222
|
+
*/
|
|
223
|
+
readonly onCommand: <TCommand extends CommandsOf<TType>>(command: TCommand, handler: (command: CustomCommand<ArgsOf<TType, TCommand>>) => Awaitable<void>) => Unsubscribe;
|
|
224
|
+
};
|
|
225
|
+
/** Registering a method of the package's interface. */
|
|
226
|
+
type RpcMethod = {
|
|
227
|
+
(name: string, options: {
|
|
228
|
+
readonly access: RpcAccess;
|
|
229
|
+
}, handler: (input: JsonValue | undefined, call: RpcCall) => Awaitable<JsonValue | undefined>): void;
|
|
230
|
+
<TInput>(name: string, options: {
|
|
231
|
+
readonly access: RpcAccess;
|
|
232
|
+
readonly input: Schema<TInput>;
|
|
233
|
+
}, handler: (input: TInput, call: RpcCall) => Awaitable<JsonValue | undefined>): void;
|
|
234
|
+
};
|
|
235
|
+
/** Offering a service. */
|
|
236
|
+
type ServiceProvide = {
|
|
237
|
+
(name: string, handler: (input: unknown, call: ServiceCall) => Awaitable<JsonValue | undefined>): void;
|
|
238
|
+
<TInput>(name: string, options: {
|
|
239
|
+
readonly input: Schema<TInput>;
|
|
240
|
+
}, handler: (input: TInput, call: ServiceCall) => Awaitable<JsonValue | undefined>): void;
|
|
241
|
+
};
|
|
242
|
+
/** Calling a service. */
|
|
243
|
+
type ServiceCallFunction = {
|
|
244
|
+
(target: string, name: string, input?: JsonValue, options?: {
|
|
245
|
+
readonly timeoutMs?: number;
|
|
246
|
+
}): Promise<unknown>;
|
|
247
|
+
<TOutput>(target: string, name: string, input: JsonValue, options: {
|
|
248
|
+
readonly timeoutMs?: number;
|
|
249
|
+
readonly output: Schema<TOutput>;
|
|
250
|
+
}): Promise<TOutput>;
|
|
251
|
+
};
|
|
252
|
+
/** What the platform hands `start`. */
|
|
253
|
+
type ServerContext<TSettings = undefined> = {
|
|
254
|
+
/** The package's id. */
|
|
255
|
+
readonly id: string;
|
|
256
|
+
/** The release running. */
|
|
257
|
+
readonly version: string;
|
|
258
|
+
readonly platform: {
|
|
259
|
+
readonly version: string;
|
|
260
|
+
};
|
|
261
|
+
/** The SDK version the platform implements. */
|
|
262
|
+
readonly sdk: {
|
|
263
|
+
readonly version: string;
|
|
264
|
+
};
|
|
265
|
+
/** What the owner saved, defaults applied, secrets open. Current after a live change. */
|
|
266
|
+
readonly settings: ServerSettings<TSettings>;
|
|
267
|
+
/**
|
|
268
|
+
* Hears a change of live settings, which reaches the process instead of restarting it.
|
|
269
|
+
*
|
|
270
|
+
* @param handler - What to do with the settings as they now stand.
|
|
271
|
+
* @returns What stops it hearing.
|
|
272
|
+
*/
|
|
273
|
+
readonly onSettingsChange: (handler: (change: SettingsChange<ServerSettings<TSettings>>) => Awaitable<void>) => Unsubscribe;
|
|
274
|
+
/** `packages/<id>/data/`: what it keeps, in every backup. */
|
|
275
|
+
readonly dataDir: string;
|
|
276
|
+
/** `cache/packages/<id>/`: what it can make again, in no backup. */
|
|
277
|
+
readonly cacheDir: string;
|
|
278
|
+
/** Aborted when the package is asked to stop. */
|
|
279
|
+
readonly signal: AbortSignal;
|
|
280
|
+
readonly logger: Logger;
|
|
281
|
+
/**
|
|
282
|
+
* One of its entities' ids.
|
|
283
|
+
*
|
|
284
|
+
* @param domain - The entity's domain.
|
|
285
|
+
* @param objectId - What follows the package's prefix.
|
|
286
|
+
* @returns `<domain>.<prefix>_<objectId>`.
|
|
287
|
+
*/
|
|
288
|
+
readonly entityId: (domain: EntityDomain, objectId: string) => EntityId;
|
|
289
|
+
/**
|
|
290
|
+
* The object id of one of its own entities.
|
|
291
|
+
*
|
|
292
|
+
* @param entityId - The entity.
|
|
293
|
+
* @returns What follows the package's prefix, or undefined for anyone else's entity.
|
|
294
|
+
*/
|
|
295
|
+
readonly objectIdOf: (entityId: string) => string | undefined;
|
|
296
|
+
readonly devices: {
|
|
297
|
+
/**
|
|
298
|
+
* Registers every device and entity the package has, whole: what is left out is removed.
|
|
299
|
+
*
|
|
300
|
+
* @param devices - Every device, with its entities.
|
|
301
|
+
* @param options - How long the platform has to answer.
|
|
302
|
+
* @param options.timeoutMs - In milliseconds; 15 seconds when not said.
|
|
303
|
+
* @returns What it wrote, and what it refused.
|
|
304
|
+
*/
|
|
305
|
+
readonly sync: (devices: readonly DeviceInput[], options?: {
|
|
306
|
+
readonly timeoutMs?: number;
|
|
307
|
+
}) => Promise<SyncResult>;
|
|
308
|
+
/**
|
|
309
|
+
* Reports an entity's state, through the pipeline every report goes through.
|
|
310
|
+
*
|
|
311
|
+
* @param entityId - The entity.
|
|
312
|
+
* @param state - One entry per capability.
|
|
313
|
+
* @param options - Its attributes and availability.
|
|
314
|
+
*/
|
|
315
|
+
readonly report: (entityId: EntityId, state: ReportedCapabilityState, options?: ReportOptions) => void;
|
|
316
|
+
/**
|
|
317
|
+
* Handles the commands routed to its entities; a throw is the command's failure.
|
|
318
|
+
*
|
|
319
|
+
* @param handler - What it does with one.
|
|
320
|
+
* @returns What stops the handler.
|
|
321
|
+
* @throws {Error} When there is a handler already.
|
|
322
|
+
*/
|
|
323
|
+
readonly onCommand: (handler: (command: CapabilityCommand) => Awaitable<void>) => Unsubscribe;
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* The entities of one of its entity types, typed.
|
|
327
|
+
*
|
|
328
|
+
* @param type - The entity type.
|
|
329
|
+
* @returns Its entities' ids, reports and commands.
|
|
330
|
+
*/
|
|
331
|
+
readonly custom: <TType extends EntityTypeDefinition>(type: TType) => CustomEntities<TType>;
|
|
332
|
+
/** The rest of the house, read only. */
|
|
333
|
+
readonly entities: {
|
|
334
|
+
/**
|
|
335
|
+
* One entity, as it stands.
|
|
336
|
+
*
|
|
337
|
+
* @param entityId - The entity.
|
|
338
|
+
* @returns It, or undefined when there is none.
|
|
339
|
+
*/
|
|
340
|
+
readonly get: (entityId: EntityId) => Promise<EntitySnapshot | undefined>;
|
|
341
|
+
/**
|
|
342
|
+
* Every entity, or those of a domain or by id.
|
|
343
|
+
*
|
|
344
|
+
* @param filter - Which.
|
|
345
|
+
* @returns Them.
|
|
346
|
+
*/
|
|
347
|
+
readonly list: (filter?: EntityFilter) => Promise<readonly EntitySnapshot[]>;
|
|
348
|
+
/**
|
|
349
|
+
* Hears every change of the entities a filter names.
|
|
350
|
+
*
|
|
351
|
+
* @param filter - Which.
|
|
352
|
+
* @param handler - What to do with a change.
|
|
353
|
+
* @returns What stops it hearing.
|
|
354
|
+
*/
|
|
355
|
+
readonly subscribe: (filter: EntityFilter, handler: (change: StateChange) => Awaitable<void>) => Unsubscribe;
|
|
356
|
+
};
|
|
357
|
+
readonly rpc: {
|
|
358
|
+
/** Registers a method its interface calls; a name registered twice throws. */
|
|
359
|
+
readonly method: RpcMethod;
|
|
360
|
+
};
|
|
361
|
+
readonly services: {
|
|
362
|
+
/** Answers a service other packages call; a name provided twice, or reserved, throws. */
|
|
363
|
+
readonly provide: ServiceProvide;
|
|
364
|
+
/** Calls another package's or the platform's service. */
|
|
365
|
+
readonly call: ServiceCallFunction;
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
//#endregion
|
|
369
|
+
export { StandardSchemaV1 as A, StateChange as C, Schema as D, SafeParser as E, SchemaIssue as O, SettingsChange as S, Unsubscribe as T, ServerContext as _, CustomEntityInput as a, ServiceCallFunction as b, EntityInput as c, Logger as d, ReportOptions as f, RpcMethod as g, RpcCaller as h, CustomEntities as i, StandardResult as k, EntitySnapshot as l, RpcCall as m, CapabilityEntityInput as n, DeviceInput as o, RpcAccess as p, CustomCommand as r, EntityFilter as s, Awaitable as t, LogMethod as u, ServerSettings as v, SyncResult as w, ServiceProvide as x, ServiceCall as y };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/host.ts
|
|
2
|
+
/** The key of the interface's implementation of `/ui`. */
|
|
3
|
+
const UI_MODULE = "@nebulahome/package-sdk/ui@1";
|
|
4
|
+
/** The key of the interface's implementation of `/components`. */
|
|
5
|
+
const COMPONENTS_MODULE = "@nebulahome/package-sdk/components@1";
|
|
6
|
+
/**
|
|
7
|
+
* Says whether a value is a record.
|
|
8
|
+
*
|
|
9
|
+
* @param value - The value.
|
|
10
|
+
* @returns True for a non-null object.
|
|
11
|
+
*/
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The SDK version the interface implements, as it says.
|
|
17
|
+
*
|
|
18
|
+
* @returns It, or undefined outside the interface.
|
|
19
|
+
*/
|
|
20
|
+
function hostSdkVersion() {
|
|
21
|
+
const nebula = Reflect.get(globalThis, "__nebula");
|
|
22
|
+
const version = isRecord(nebula) ? nebula["sdk"] : void 0;
|
|
23
|
+
return typeof version === "string" ? version : void 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One member of a module the interface lends.
|
|
27
|
+
*
|
|
28
|
+
* @param module - The module's key.
|
|
29
|
+
* @param name - The member.
|
|
30
|
+
* @param since - The SDK version that added it.
|
|
31
|
+
* @returns The interface's implementation of it.
|
|
32
|
+
* @throws {Error} When there is no interface, or it does not implement the member.
|
|
33
|
+
*/
|
|
34
|
+
function hostMember(module, name, since) {
|
|
35
|
+
const nebula = Reflect.get(globalThis, "__nebula");
|
|
36
|
+
const modules = isRecord(nebula) ? nebula["modules"] : void 0;
|
|
37
|
+
const lent = isRecord(modules) ? modules[module] : void 0;
|
|
38
|
+
const member = isRecord(lent) ? lent[name] : void 0;
|
|
39
|
+
/**
|
|
40
|
+
* Says whether the member is there: a function, or a component React wrapped. What it is
|
|
41
|
+
* beyond that the interface vouches for, its implementation being type-checked against the
|
|
42
|
+
* SDK's declaration.
|
|
43
|
+
*
|
|
44
|
+
* @param value - The member.
|
|
45
|
+
* @returns True when it can be called or drawn.
|
|
46
|
+
*/
|
|
47
|
+
const isLent = (value) => typeof value === "function" || isRecord(value) && "$$typeof" in value;
|
|
48
|
+
if (isLent(member)) return member;
|
|
49
|
+
const version = hostSdkVersion();
|
|
50
|
+
throw new Error(version === void 0 ? `${name} is lent by the NebulaHome interface, and there is none here` : `${name} needs package SDK ${since}; this interface implements ${version}`);
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { UI_MODULE as n, hostMember as r, COMPONENTS_MODULE as t };
|