@drift-beacon/plugin 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +368 -0
  3. package/bin/dbplugin.js +11 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +38 -0
  6. package/dist/dev.d.ts +38 -0
  7. package/dist/dev.js +8 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +9 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +8 -0
  12. package/dist/internal/actions.d.ts +35 -0
  13. package/dist/internal/actions.js +1 -0
  14. package/dist/internal/collection.d.ts +15 -0
  15. package/dist/internal/collection.js +52 -0
  16. package/dist/internal/compatibility.d.ts +26 -0
  17. package/dist/internal/compatibility.js +52 -0
  18. package/dist/internal/json-equal.d.ts +3 -0
  19. package/dist/internal/json-equal.js +20 -0
  20. package/dist/internal/manifest.d.ts +51 -0
  21. package/dist/internal/manifest.js +73 -0
  22. package/dist/internal/models.d.ts +62 -0
  23. package/dist/internal/models.js +387 -0
  24. package/dist/internal/package-layout.d.ts +21 -0
  25. package/dist/internal/package-layout.js +19 -0
  26. package/dist/internal/protocol.d.ts +96 -0
  27. package/dist/internal/protocol.js +8 -0
  28. package/dist/internal/rows.d.ts +41 -0
  29. package/dist/internal/rows.js +1 -0
  30. package/dist/internal.d.ts +17 -0
  31. package/dist/internal.js +13 -0
  32. package/dist/main.d.ts +92 -0
  33. package/dist/main.js +14 -0
  34. package/dist/pack.d.ts +24 -0
  35. package/dist/pack.js +131 -0
  36. package/dist/prepare.d.ts +28 -0
  37. package/dist/prepare.js +121 -0
  38. package/dist/types.d.ts +193 -0
  39. package/dist/types.js +10 -0
  40. package/dist/ui.d.ts +25 -0
  41. package/dist/ui.js +265 -0
  42. package/dist/version.d.ts +6 -0
  43. package/dist/version.js +5 -0
  44. package/dist/vite.d.ts +8 -0
  45. package/dist/vite.js +370 -0
  46. package/package.json +73 -0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The plugin package: `manifest.json`, `package.json`, `main/index.js` (plus chunks) and `ui/index.html`
3
+ * (plus assets and the manifest icon). Shared by the pack tool and the server. Pure: the web app bundles
4
+ * `/internal`, so no `node:` imports.
5
+ */
6
+ export const PACKAGE_LIMITS = {
7
+ entries: 2000,
8
+ fileBytes: 32 * 2 ** 20,
9
+ totalBytes: 128 * 2 ** 20,
10
+ archiveBytes: 32 * 2 ** 20,
11
+ manifestBytes: 256 * 2 ** 10,
12
+ };
13
+ /** The `package.json` every package gets: main is ESM. */
14
+ export const PACKAGE_JSON = { type: "module", private: true };
15
+ export const REQUIRED_FILES = ["main/index.js", "ui/index.html"];
16
+ /** manifest.json, package.json, main/** and ui/** (directories: main, ui and below). `name` has no trailing slash. */
17
+ export const isPackagePath = (name, directory) => name.startsWith("main/") ||
18
+ name.startsWith("ui/") ||
19
+ (directory ? name === "main" || name === "ui" : name === "manifest.json" || name === "package.json");
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Messages between a plugin UI (iframe) and the web app hosting it, over `postMessage`.
3
+ *
4
+ * The UI client ships inside every published UI bundle, so this protocol is frozen into
5
+ * them: change it only additively within an API version. The UI opens with `hello`; nothing
6
+ * else flows until the host answers `welcome` or `reject`.
7
+ */
8
+ import type { PluginErrorCode, PluginInfo, UserInfo, WorkspaceInfo } from "../types.ts";
9
+ import type { WorkspaceRows } from "./rows.ts";
10
+ /** Marks our messages so both sides ignore unrelated `postMessage` traffic (dev tools, HMR). */
11
+ export declare const UI_CHANNEL = "drift-beacon-plugin";
12
+ /** Workspace data sent to a UI, as rows. */
13
+ export type UiData = WorkspaceRows;
14
+ export type UiRequest = {
15
+ readonly method: "sessions.start";
16
+ readonly params: {
17
+ readonly activityId: string;
18
+ };
19
+ } | {
20
+ readonly method: "sessions.end";
21
+ readonly params: {
22
+ readonly sessionId: string;
23
+ };
24
+ } | {
25
+ readonly method: "sessions.mark";
26
+ readonly params: {
27
+ readonly activityId: string;
28
+ };
29
+ } | {
30
+ readonly method: "sessions.discard";
31
+ readonly params: {
32
+ readonly sessionId: string;
33
+ };
34
+ } | {
35
+ readonly method: "storage.set";
36
+ readonly params: {
37
+ readonly key: string;
38
+ readonly value: unknown;
39
+ };
40
+ } | {
41
+ readonly method: "storage.remove";
42
+ readonly params: {
43
+ readonly key: string;
44
+ };
45
+ };
46
+ export type UiToHostMessage = {
47
+ readonly channel: typeof UI_CHANNEL;
48
+ readonly type: "hello";
49
+ readonly apiVersion: string;
50
+ } | ({
51
+ readonly channel: typeof UI_CHANNEL;
52
+ readonly type: "request";
53
+ readonly id: string;
54
+ } & UiRequest);
55
+ export interface UiError {
56
+ readonly code: PluginErrorCode;
57
+ readonly message: string;
58
+ }
59
+ export type HostToUiMessage = {
60
+ readonly channel: typeof UI_CHANNEL;
61
+ readonly type: "welcome";
62
+ readonly apiVersion: string;
63
+ readonly plugin: PluginInfo;
64
+ readonly user: UserInfo;
65
+ readonly workspace: WorkspaceInfo;
66
+ readonly config: Readonly<Record<string, unknown>>;
67
+ readonly data: UiData;
68
+ readonly storage: Readonly<Record<string, unknown>>;
69
+ } | {
70
+ readonly channel: typeof UI_CHANNEL;
71
+ readonly type: "reject";
72
+ readonly reason: string;
73
+ }
74
+ /** The app restarted its side of the connection; the UI answers with `hello` and gets a new `welcome`. */
75
+ | {
76
+ readonly channel: typeof UI_CHANNEL;
77
+ readonly type: "reconnect";
78
+ } | {
79
+ /** Replaces whichever parts are present. */
80
+ readonly channel: typeof UI_CHANNEL;
81
+ readonly type: "state";
82
+ readonly config?: Readonly<Record<string, unknown>>;
83
+ readonly data?: UiData;
84
+ readonly storage?: Readonly<Record<string, unknown>>;
85
+ } | {
86
+ readonly channel: typeof UI_CHANNEL;
87
+ readonly type: "response";
88
+ readonly id: string;
89
+ readonly result?: unknown;
90
+ readonly error?: UiError;
91
+ };
92
+ export declare function isUiMessage(value: unknown): value is {
93
+ channel: typeof UI_CHANNEL;
94
+ type: string;
95
+ };
96
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1,8 @@
1
+ /** Marks our messages so both sides ignore unrelated `postMessage` traffic (dev tools, HMR). */
2
+ export const UI_CHANNEL = "drift-beacon-plugin";
3
+ export function isUiMessage(value) {
4
+ return (typeof value === "object" &&
5
+ value !== null &&
6
+ value.channel === UI_CHANNEL &&
7
+ typeof value.type === "string");
8
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Workspace data as it travels from the app to plugins: to plugin hosts over IPC and to plugin
3
+ * UIs over `postMessage`. Plugins never see rows; the model layer (`models.ts`) wraps them.
4
+ */
5
+ import type { ActivityData, CategoryData } from "../types.ts";
6
+ export type ActivityRow = ActivityData;
7
+ export type CategoryRow = CategoryData;
8
+ export interface SessionRow {
9
+ readonly id: string;
10
+ readonly activityId: string;
11
+ readonly type: "span" | "point";
12
+ readonly status: "live" | "completed";
13
+ readonly memberIds: readonly string[];
14
+ /** ISO timestamp: when a span started, or when a point was marked. */
15
+ readonly startedAt: string;
16
+ /** ISO timestamp when a span ended; null while live and for points. */
17
+ readonly endedAt: string | null;
18
+ }
19
+ export interface WorkspaceRows {
20
+ readonly activities: readonly ActivityRow[];
21
+ readonly categories: readonly CategoryRow[];
22
+ readonly sessions: readonly SessionRow[];
23
+ }
24
+ /** One change to a collection of rows. */
25
+ export type RowChange<T> = {
26
+ readonly type: "added";
27
+ readonly item: T;
28
+ } | {
29
+ readonly type: "updated";
30
+ readonly item: T;
31
+ readonly previous: T;
32
+ } | {
33
+ readonly type: "removed";
34
+ readonly item: T;
35
+ };
36
+ export interface WorkspaceRowChanges {
37
+ readonly activities: readonly RowChange<ActivityRow>[];
38
+ readonly categories: readonly RowChange<CategoryRow>[];
39
+ readonly sessions: readonly RowChange<SessionRow>[];
40
+ }
41
+ //# sourceMappingURL=rows.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Platform-only entry used by the plugin host, the web app and the server. Not part of the plugin API.
3
+ * Public entries never import this file; SDK modules may import `internal/*` directly. Publishing drops
4
+ * only its export key.
5
+ */
6
+ export * from "./dev.ts";
7
+ export type { PluginAction } from "./internal/actions.ts";
8
+ export { Collection } from "./internal/collection.ts";
9
+ export { type ApiCompatibility, type ApiVersion, checkApiCompatibility, parseApiVersion, SUPPORTED_API_VERSIONS, } from "./internal/compatibility.ts";
10
+ export { jsonEqual } from "./internal/json-equal.ts";
11
+ export { type BooleanConfigurationItem, type ConfigurationItem, type DropdownConfigurationItem, isStableVersion, type NumberConfigurationItem, type PluginManifest, type StringConfigurationItem, validateManifest, } from "./internal/manifest.ts";
12
+ export { type ModelApis, type ModelChanges, type SessionActions, type WorkspaceCollections, WorkspaceModels, } from "./internal/models.ts";
13
+ export { isPackagePath, PACKAGE_JSON, PACKAGE_LIMITS, REQUIRED_FILES } from "./internal/package-layout.ts";
14
+ export * from "./internal/protocol.ts";
15
+ export type { ActivityRow, CategoryRow, RowChange, SessionRow, WorkspaceRowChanges, WorkspaceRows, } from "./internal/rows.ts";
16
+ export type { PluginErrorCode } from "./types.ts";
17
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Platform-only entry used by the plugin host, the web app and the server. Not part of the plugin API.
3
+ * Public entries never import this file; SDK modules may import `internal/*` directly. Publishing drops
4
+ * only its export key.
5
+ */
6
+ export * from "./dev.js";
7
+ export { Collection } from "./internal/collection.js";
8
+ export { checkApiCompatibility, parseApiVersion, SUPPORTED_API_VERSIONS, } from "./internal/compatibility.js";
9
+ export { jsonEqual } from "./internal/json-equal.js";
10
+ export { isStableVersion, validateManifest, } from "./internal/manifest.js";
11
+ export { WorkspaceModels, } from "./internal/models.js";
12
+ export { isPackagePath, PACKAGE_JSON, PACKAGE_LIMITS, REQUIRED_FILES } from "./internal/package-layout.js";
13
+ export * from "./internal/protocol.js";
package/dist/main.d.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { ActivitiesApi, CategoriesApi, PluginConfig, PluginInfo, Session, SessionsApi, StorageApi, Unsubscribe, UserInfo, WorkspaceInfo } from "./types.ts";
2
+ export type SessionEndReason = "completed" | "discarded";
3
+ export interface MainSessionsApi extends SessionsApi {
4
+ /**
5
+ * A user just started a span session. Live events fire for actions happening now, never for
6
+ * sessions that arrive later from a client's offline sync.
7
+ */
8
+ onStarted(callback: (session: Session) => void): Unsubscribe;
9
+ /** A span session just ended or was discarded. */
10
+ onEnded(callback: (session: Session, reason: SessionEndReason) => void): Unsubscribe;
11
+ /** A point activity was just marked. */
12
+ onMarked(callback: (session: Session) => void): Unsubscribe;
13
+ }
14
+ export type MqttStatus = "connecting" | "connected" | "disconnected" | "error" | "unavailable";
15
+ export interface MqttMessage {
16
+ readonly topic: string;
17
+ readonly payload: string;
18
+ }
19
+ export interface MqttApi {
20
+ /** Subscribe to a topic filter. `+` matches one level and `#` the remaining levels. */
21
+ subscribe(topic: string, callback: (message: MqttMessage) => void): Unsubscribe;
22
+ /** Rejects when no broker is configured or the broker refuses the message. */
23
+ publish(topic: string, payload: string): Promise<void>;
24
+ /** `unavailable` when no broker is configured for the workspace. */
25
+ readonly status: MqttStatus;
26
+ onStatusChange(callback: (status: MqttStatus) => void): Unsubscribe;
27
+ }
28
+ export interface RouteRequest {
29
+ readonly method: "GET" | "POST";
30
+ /** Parsed JSON body for POST requests. */
31
+ readonly body: unknown;
32
+ readonly query: Readonly<Record<string, string>>;
33
+ /** Request headers, without credentials such as `authorization` and `cookie`. */
34
+ readonly headers: Readonly<Record<string, string>>;
35
+ }
36
+ export interface RouteResponse {
37
+ /** Defaults to 200. */
38
+ readonly status?: number;
39
+ /** Sent as JSON. */
40
+ readonly body?: unknown;
41
+ }
42
+ export type RouteHandler = (request: RouteRequest) => RouteResponse | Promise<RouteResponse>;
43
+ export interface RoutesApi {
44
+ /** Serve `GET <apiPath>/<name>`. Callers authenticate with a workspace API key. */
45
+ get(name: string, handler: RouteHandler): void;
46
+ /** Serve `POST <apiPath>/<name>`. Callers authenticate with a workspace API key. */
47
+ post(name: string, handler: RouteHandler): void;
48
+ }
49
+ export interface LogApi {
50
+ info(...args: unknown[]): void;
51
+ warn(...args: unknown[]): void;
52
+ error(...args: unknown[]): void;
53
+ }
54
+ /** Everything a plugin's main code can use. One `ctx` per instance: a plugin for one user in one workspace. */
55
+ export interface MainContext {
56
+ readonly plugin: PluginInfo;
57
+ readonly user: UserInfo;
58
+ readonly workspace: WorkspaceInfo;
59
+ /** The plugin's settings, validated, with defaults applied. A change restarts the instance. */
60
+ readonly config: Readonly<PluginConfig>;
61
+ readonly activities: ActivitiesApi;
62
+ readonly categories: CategoriesApi;
63
+ readonly sessions: MainSessionsApi;
64
+ /** Called once after each update to workspace data. */
65
+ onDataChange(callback: () => void): Unsubscribe;
66
+ readonly storage: StorageApi;
67
+ readonly mqtt: MqttApi;
68
+ readonly routes: RoutesApi;
69
+ readonly log: LogApi;
70
+ /**
71
+ * Run when the instance stops: disabled, reconfigured, updated or shut down. `ctx` still works
72
+ * while stop callbacks run; afterwards every subscription made through it is released.
73
+ */
74
+ onStop(callback: () => void | Promise<void>): void;
75
+ }
76
+ export interface PluginDefinition {
77
+ /** Called once per instance. Register everything through `ctx`; it is released when the instance stops. */
78
+ onStart(ctx: MainContext): void | Promise<void>;
79
+ }
80
+ /**
81
+ * Define a plugin's main code. Export the result as the module's default export:
82
+ *
83
+ * ```ts
84
+ * export default definePlugin({
85
+ * onStart(ctx) {
86
+ * ctx.mqtt.subscribe(ctx.config.mqttTopic, (message) => ctx.log.info(message.payload));
87
+ * },
88
+ * });
89
+ * ```
90
+ */
91
+ export declare function definePlugin(definition: PluginDefinition): PluginDefinition;
92
+ //# sourceMappingURL=main.d.ts.map
package/dist/main.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Define a plugin's main code. Export the result as the module's default export:
3
+ *
4
+ * ```ts
5
+ * export default definePlugin({
6
+ * onStart(ctx) {
7
+ * ctx.mqtt.subscribe(ctx.config.mqttTopic, (message) => ctx.log.info(message.payload));
8
+ * },
9
+ * });
10
+ * ```
11
+ */
12
+ export function definePlugin(definition) {
13
+ return definition;
14
+ }
package/dist/pack.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface PackResult {
2
+ readonly id: string;
3
+ readonly version: string;
4
+ readonly tag: string;
5
+ readonly asset: string;
6
+ readonly archivePath: string;
7
+ }
8
+ /**
9
+ * Zip the package in `dir` into `<outDir>/<id>.zip`: its manifest, a generated `package.json`, and `main/**`
10
+ * and `ui/**`. Anything else in `dir` (the dev marker, its `package.json`) is left out. Never runs plugin code.
11
+ */
12
+ export declare function packDirectory(dir: string, outDir: string): Promise<PackResult>;
13
+ export interface PackOptions {
14
+ /** Where the zip goes. Default `<root>/releases`. */
15
+ readonly out?: string;
16
+ }
17
+ /**
18
+ * Build a release of the plugin package at `root`: prepare, typecheck main and UI with the package's own
19
+ * TypeScript, `vite build` into a fresh temp folder with the package's own Vite, then `packDirectory`. The
20
+ * package's `dist/` is never read or written. Everything printed meanwhile goes to stderr, so a caller's
21
+ * stdout carries only the result.
22
+ */
23
+ export declare function pack(root: string, options?: PackOptions): Promise<PackResult>;
24
+ //# sourceMappingURL=pack.d.ts.map
package/dist/pack.js ADDED
@@ -0,0 +1,131 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /**
10
+ * Release packaging. `packDirectory` zips a finished package folder (`dbplugin pack` and the server's
11
+ * `plugin:install`); `pack` typechecks a plugin package, builds it fresh into a temp folder and packs that.
12
+ */
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync } from "node:fs";
15
+ import { lstat, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
16
+ import { createRequire } from "node:module";
17
+ import { tmpdir } from "node:os";
18
+ import path from "node:path";
19
+ import { pathToFileURL } from "node:url";
20
+ import { strToU8, zipSync } from "fflate";
21
+ import { PACKAGE_JSON, PACKAGE_LIMITS, REQUIRED_FILES } from "./internal/package-layout.js";
22
+ import { loadManifest, prepare } from "./prepare.js";
23
+ /**
24
+ * Zip the package in `dir` into `<outDir>/<id>.zip`: its manifest, a generated `package.json`, and `main/**`
25
+ * and `ui/**`. Anything else in `dir` (the dev marker, its `package.json`) is left out. Never runs plugin code.
26
+ */
27
+ export async function packDirectory(dir, outDir) {
28
+ const source = path.resolve(dir);
29
+ const { manifest, bytes: manifestBytes } = loadManifest(source);
30
+ const files = Object.create(null);
31
+ files["manifest.json"] = manifestBytes;
32
+ const packageBytes = strToU8(`${JSON.stringify(PACKAGE_JSON, null, 2)}\n`);
33
+ files["package.json"] = packageBytes;
34
+ let totalBytes = manifestBytes.length + packageBytes.length;
35
+ let entries = 2;
36
+ const names = new Set(["manifest.json", "package.json"]);
37
+ const collect = async (relativePath) => {
38
+ if (relativePath.length > 512 ||
39
+ /[\\:]/.test(relativePath) ||
40
+ [...relativePath].some((char) => char.charCodeAt(0) < 32))
41
+ throw new Error(`Unsupported package path: ${relativePath}`);
42
+ const absolute = path.join(source, relativePath);
43
+ const stat = await lstat(absolute);
44
+ if (stat.isSymbolicLink())
45
+ throw new Error(`Package cannot contain symlinks: ${relativePath}`);
46
+ if (stat.isDirectory()) {
47
+ for (const name of (await readdir(absolute)).sort()) {
48
+ if (name === "node_modules")
49
+ throw new Error("Bundle dependencies instead of packaging node_modules");
50
+ await collect(`${relativePath}/${name}`);
51
+ }
52
+ }
53
+ else if (stat.isFile()) {
54
+ if (names.has(relativePath.toLowerCase()))
55
+ throw new Error(`Duplicate package path: ${relativePath}`);
56
+ names.add(relativePath.toLowerCase());
57
+ totalBytes += stat.size;
58
+ entries += 1;
59
+ if (stat.size > PACKAGE_LIMITS.fileBytes)
60
+ throw new Error(`File exceeds 32 MiB: ${relativePath}`);
61
+ if (totalBytes > PACKAGE_LIMITS.totalBytes || entries > PACKAGE_LIMITS.entries) {
62
+ throw new Error("Plugin exceeds package size/entry limits");
63
+ }
64
+ files[relativePath] = await readFile(absolute);
65
+ }
66
+ else {
67
+ throw new Error(`Package contains an unsupported file: ${relativePath}`);
68
+ }
69
+ };
70
+ const top = new Set(await readdir(source));
71
+ for (const part of ["main", "ui"])
72
+ if (top.has(part))
73
+ await collect(part);
74
+ for (const required of REQUIRED_FILES) {
75
+ if (!files[required])
76
+ throw new Error(`Missing ${required}`);
77
+ }
78
+ if (manifest.icon !== "" && !files[`ui/${manifest.icon}`]) {
79
+ throw new Error(`manifest.json names icon ${manifest.icon}, but ui/public/${manifest.icon} is missing`);
80
+ }
81
+ const asset = `${manifest.id}.zip`;
82
+ const archivePath = path.join(path.resolve(outDir), asset);
83
+ const archive = zipSync(files, { level: 6 });
84
+ if (archive.length > PACKAGE_LIMITS.archiveBytes)
85
+ throw new Error("Compressed package exceeds 32 MiB");
86
+ await mkdir(path.dirname(archivePath), { recursive: true });
87
+ await writeFile(archivePath, archive);
88
+ return { id: manifest.id, version: manifest.version, tag: `${manifest.id}-${manifest.version}`, asset, archivePath };
89
+ }
90
+ /**
91
+ * Build a release of the plugin package at `root`: prepare, typecheck main and UI with the package's own
92
+ * TypeScript, `vite build` into a fresh temp folder with the package's own Vite, then `packDirectory`. The
93
+ * package's `dist/` is never read or written. Everything printed meanwhile goes to stderr, so a caller's
94
+ * stdout carries only the result.
95
+ */
96
+ export async function pack(root, options = {}) {
97
+ const pkg = path.resolve(root);
98
+ const write = process.stdout.write;
99
+ process.stdout.write = process.stderr.write.bind(process.stderr);
100
+ let temp;
101
+ try {
102
+ prepare(pkg, { strict: true });
103
+ const require = createRequire(path.join(pkg, "package.json"));
104
+ // Found through its package.json `bin`: TypeScript 7 doesn't export `typescript/bin/tsc`.
105
+ const typescript = require.resolve("typescript/package.json");
106
+ const { bin } = JSON.parse(await readFile(typescript, "utf8"));
107
+ const tsc = path.join(path.dirname(typescript), bin.tsc);
108
+ for (const project of ["tsconfig.main.json", "tsconfig.ui.json"]) {
109
+ const result = spawnSync(process.execPath, [tsc, "-p", path.join(".drift-beacon", project)], {
110
+ cwd: pkg,
111
+ stdio: ["ignore", 2, 2],
112
+ });
113
+ if (result.error)
114
+ throw result.error;
115
+ if (result.status !== 0)
116
+ throw new Error(`Type errors (.drift-beacon/${project}); nothing was packed`);
117
+ }
118
+ temp = await mkdtemp(path.join(tmpdir(), "dbplugin-pack-"));
119
+ const vite = await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("vite")).href));
120
+ await vite.build({ root: pkg, mode: "production", logLevel: "warn", build: { outDir: temp } });
121
+ if (!existsSync(path.join(temp, "main/index.js"))) {
122
+ throw new Error("vite build produced no main/index.js: add driftBeacon() to vite.config.ts");
123
+ }
124
+ return await packDirectory(temp, options.out ?? path.join(pkg, "releases"));
125
+ }
126
+ finally {
127
+ process.stdout.write = write;
128
+ if (temp)
129
+ await rm(temp, { recursive: true, force: true });
130
+ }
131
+ }
@@ -0,0 +1,28 @@
1
+ import { type PluginManifest } from "./internal/manifest.ts";
2
+ export interface PrepareOptions {
3
+ /**
4
+ * Throw on manifest problems (dev, build and pack). When false (`dbplugin prepare`, run on install), warn
5
+ * and leave `.drift-beacon/` as it was, so a manifest typo never fails `pnpm install`.
6
+ */
7
+ readonly strict: boolean;
8
+ }
9
+ /** A package's `manifest.json`: parsed and checked, with its bytes. */
10
+ export interface LoadedManifest {
11
+ readonly manifest: PluginManifest;
12
+ readonly bytes: Buffer;
13
+ }
14
+ /**
15
+ * Generate `<root>/.drift-beacon/` from the manifest, and return the manifest (`loadManifest`). Files are
16
+ * rewritten only when their content changes.
17
+ */
18
+ export declare function prepare(root: string, options: {
19
+ readonly strict: true;
20
+ }): LoadedManifest;
21
+ export declare function prepare(root: string, options: PrepareOptions): LoadedManifest | null;
22
+ /**
23
+ * `<root>/manifest.json`, the one read of a manifest in the SDK (dev, build, prepare and pack): a regular
24
+ * file within the package limit, valid, and for an `apiVersion` this SDK runs (the server's check). The
25
+ * error names the file.
26
+ */
27
+ export declare function loadManifest(root: string): LoadedManifest;
28
+ //# sourceMappingURL=prepare.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Writes a plugin package's generated `.drift-beacon/` folder: `config.d.ts` (the `ctx.config` types from
3
+ * manifest.json), the main and UI tsconfigs, and a `.gitignore` that ignores the folder itself. Also reads
4
+ * and checks manifests for `driftBeacon()` and `dbplugin pack` (`loadManifest`).
5
+ */
6
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { checkApiCompatibility } from "./internal/compatibility.js";
9
+ import { validateManifest } from "./internal/manifest.js";
10
+ import { PACKAGE_LIMITS } from "./internal/package-layout.js";
11
+ import { API_VERSION } from "./version.js";
12
+ const COMPILER_OPTIONS = {
13
+ target: "ES2023",
14
+ module: "Preserve",
15
+ moduleResolution: "Bundler",
16
+ moduleDetection: "force",
17
+ strict: true,
18
+ skipLibCheck: true,
19
+ noEmit: true,
20
+ verbatimModuleSyntax: true,
21
+ allowImportingTsExtensions: true,
22
+ noFallthroughCasesInSwitch: true,
23
+ noUncheckedSideEffectImports: true,
24
+ };
25
+ // Explicit `types` keep UI typings (React, three.js) out of main; `vite/client` allows CSS and asset imports in the UI.
26
+ const TSCONFIG_MAIN = {
27
+ compilerOptions: { ...COMPILER_OPTIONS, lib: ["ES2023"], types: ["node"] },
28
+ include: ["../main/src", "../vite.config.ts", "./config.d.ts"],
29
+ };
30
+ const TSCONFIG_UI = {
31
+ compilerOptions: {
32
+ ...COMPILER_OPTIONS,
33
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
34
+ jsx: "react-jsx",
35
+ useDefineForClassFields: true,
36
+ types: ["vite/client"],
37
+ },
38
+ include: ["../ui/src", "./config.d.ts"],
39
+ };
40
+ export function prepare(root, { strict }) {
41
+ let loaded;
42
+ try {
43
+ loaded = loadManifest(root);
44
+ }
45
+ catch (error) {
46
+ if (strict)
47
+ throw error;
48
+ console.warn(`${error.message}\nLeft .drift-beacon/ unchanged; fix manifest.json and run dbplugin prepare.`);
49
+ return null;
50
+ }
51
+ const { manifest } = loaded;
52
+ const out = path.join(root, ".drift-beacon");
53
+ mkdirSync(out, { recursive: true });
54
+ writeIfChanged(path.join(out, ".gitignore"), "*\n");
55
+ writeIfChanged(path.join(out, "config.d.ts"), configTypes(manifest.configuration));
56
+ writeIfChanged(path.join(out, "tsconfig.main.json"), `${JSON.stringify(TSCONFIG_MAIN, null, 2)}\n`);
57
+ writeIfChanged(path.join(out, "tsconfig.ui.json"), `${JSON.stringify(TSCONFIG_UI, null, 2)}\n`);
58
+ return loaded;
59
+ }
60
+ /**
61
+ * `<root>/manifest.json`, the one read of a manifest in the SDK (dev, build, prepare and pack): a regular
62
+ * file within the package limit, valid, and for an `apiVersion` this SDK runs (the server's check). The
63
+ * error names the file.
64
+ */
65
+ export function loadManifest(root) {
66
+ const file = path.join(root, "manifest.json");
67
+ let manifest;
68
+ let bytes;
69
+ try {
70
+ const stat = lstatSync(file);
71
+ if (!stat.isFile())
72
+ throw new Error("must be a regular file");
73
+ if (stat.size > PACKAGE_LIMITS.manifestBytes)
74
+ throw new Error("exceeds 256 KiB");
75
+ bytes = readFileSync(file);
76
+ manifest = JSON.parse(bytes.toString("utf8"));
77
+ validateManifest(manifest);
78
+ }
79
+ catch (error) {
80
+ const reason = error.code === "ENOENT" ? "not found" : error.message;
81
+ throw new Error(`${file}: ${reason}`, { cause: error });
82
+ }
83
+ const compatibility = checkApiCompatibility(manifest.apiVersion, [API_VERSION]);
84
+ if (!compatibility.compatible)
85
+ throw new Error(`${file}: ${compatibility.message}`);
86
+ return { manifest, bytes };
87
+ }
88
+ /**
89
+ * Augments the SDK's `PluginConfig`. Mirrors the server's config validation: a setting can be missing
90
+ * unless it is required or has a default. The type-only import makes this a module, so `declare module`
91
+ * augments instead of declaring an ambient module, and it loads the SDK entry into every program: the UI
92
+ * imports only `@drift-beacon/plugin/ui`, and an augmentation of a module the program never loads has no effect.
93
+ */
94
+ function configTypes(configuration) {
95
+ const lines = [
96
+ "// Generated by dbplugin prepare from manifest.json. Do not edit.",
97
+ 'import type {} from "@drift-beacon/plugin";',
98
+ 'declare module "@drift-beacon/plugin" {',
99
+ " interface PluginConfig {",
100
+ ];
101
+ for (const item of configuration) {
102
+ const doc = [item.title, item.description].filter(Boolean).join(": ").replaceAll("*/", "*\\/");
103
+ if (doc)
104
+ lines.push(` /** ${doc} */`);
105
+ const optional = item.required === true || item.default !== undefined ? "" : "?";
106
+ lines.push(` ${JSON.stringify(item.name)}${optional}: ${valueType(item)};`);
107
+ }
108
+ lines.push(" }", "}", "");
109
+ return lines.join("\n");
110
+ }
111
+ const valueType = (item) => item.type !== "dropdown"
112
+ ? item.type
113
+ : item.data.length === 0
114
+ ? "string"
115
+ : [...new Set(item.data.map((option) => JSON.stringify(option.value)))].join(" | ");
116
+ /** Leaves the file (and its mtime) alone when it already has `content`, so editors and watchers stay quiet. */
117
+ function writeIfChanged(file, content) {
118
+ if (existsSync(file) && readFileSync(file, "utf8") === content)
119
+ return;
120
+ writeFileSync(file, content);
121
+ }