Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/sweet-glasses-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@anywidget/deno": patch
---

Publish package to [`jsr:@anywidget/deno`](https://jsr.io/@anywidget/deno)

Deprecates https://deno.land/x/anywidget distribution for the preferred
[JSR](https://jsr.io).
21 changes: 21 additions & 0 deletions .github/workflows/jsr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Publish @anywidget/deno to jsr

on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v4
- name: Publish package
run: |
cd packages/deno
npx jsr publish
22 changes: 11 additions & 11 deletions packages/deno/src/install.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import * as path from "@std/path";
import * as fs from "@std/fs";
import * as cli from "@std/cli";
import * as unzipit from "unzipit";
import * as z from "zod";
import {
find_data_dir,
system_data_dirs,
user_data_dir,
} from "./jupyter_paths.ts";

/**
* @module
* Install the front-end anywidget assets for JupyterLab.
Expand All @@ -20,6 +9,17 @@ import {
* ```
*/

import * as path from "@std/path";
import * as fs from "@std/fs";
import * as cli from "@std/cli";
import * as unzipit from "unzipit";
import * as z from "zod";
import {
find_data_dir,
system_data_dirs,
user_data_dir,
} from "./jupyter_paths.ts";

let ReleaseSchema = z.object({
packagetype: z.string(),
url: z.string(),
Expand Down
82 changes: 53 additions & 29 deletions packages/deno/src/mod.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as path from "@std/path";
import { find_data_dir } from "./jupyter_paths.ts";

/**
* Jupyter widgets for the Deno Jupyter kernel.
* @module
*/

import * as path from "@std/path";
import { find_data_dir } from "./jupyter_paths.ts";

let COMMS = new WeakMap<object, Comm>();
// TODO: We need to get this version from somewhere. Needs to match packages/anywidget/package.json#version
let DEFAULT_VERSION: string = "0.9.3";
Expand Down Expand Up @@ -62,7 +62,7 @@ interface TestingInternals {
/** Get the comm for a model */
get_comm(model: object): Comm;
/** Get the init promise for a model */
get_init_promise(model: _Model<unknown>): Promise<void> | undefined;
get_init_promise(model: Model<unknown>): Promise<void> | undefined;
/** The version of anywidget used. */
version: string;
}
Expand All @@ -77,8 +77,8 @@ export const _internals: TestingInternals = {
}
return comm;
},
get_init_promise(model: _Model<unknown>): Promise<void> | undefined {
// @ts-expect-error - we hide the symbol from the user
get_init_promise(model: Model<unknown>): Promise<void> | undefined {
// @ts-expect-error - We have tagged this symbol onto the model privately
return model[init_promise_symbol];
},
get version() {
Expand All @@ -99,10 +99,12 @@ class Comm {
this.#protocol_version_minor = 1;
}

/** The id of the comm. */
get id(): string {
return this.#id;
}

/** Send a message to the front end to initialize the widget. */
init(): Promise<void> {
return _internals.jupyter_broadcast(
"comm_open",
Expand Down Expand Up @@ -130,13 +132,15 @@ class Comm {
);
}

/** Send a state update to the front end. */
send_state(state: object): Promise<void> {
return _internals.jupyter_broadcast("comm_msg", {
comm_id: this.id,
data: { method: "update", state },
});
}

/** The Jupyter "mimebundle" for displaying the underlying widget. */
mimebundle(): Mimebundle {
return {
"application/vnd.jupyter.widget-view+json": {
Expand All @@ -152,23 +156,46 @@ type ChangeEvents<State> = {
[K in string & keyof State as `change:${K}`]: State[K];
};

class _Model<State> {
/** A BackboneJS-like model for the anywidget. */
export class Model<State> {
private _state: State;
private _target: EventTarget;

constructor(state: State) {
this._state = state;
this._target = new EventTarget();
}

/**
* Get a property of the state object.
*
* @param key - The property to get.
*/
get<K extends keyof State>(key: K): State[K] {
return this._state[key];
}

/**
* Set a property of the state object.
*
* @param key - The property to set.
* @param value - The new value.
*/
set<K extends keyof State>(key: K, value: State[K]): void {
this._state[key] = value;
this._target.dispatchEvent(
new CustomEvent(`change:${key as string}`, { detail: value }),
);
}

/**
* Subscribe to changes in the state object.
*
* Note: Only `change:${key}` events are supported.
*
* @param name - The event name to subscribe to.
* @param callback - The callback to call when the event is dispatched.
*/
on<Event extends keyof ChangeEvents<State>>(
name: Event,
callback: () => void,
Expand All @@ -177,18 +204,27 @@ class _Model<State> {
}
}

export type Model = typeof _Model;

export type FrontEndModel<State> = _Model<State> & {
/** The front end variant of the model. */
export type FrontEndModel<State> = Model<State> & {
/** Sync changes with the Deno kernel. */
save_changes(): void;
};

// Requires mod user to include lib DOM in their compiler options if they want to use this type.
type HTMLElement = typeof globalThis extends { HTMLElement: infer T } ? T
: unknown;

export type WidgetProps<State> = {
/** The initial state of the widget. */
// TODO: more robust serialization of render function (with context?)
function to_esm<State>({
imports = "",
render,
}: Pick<WidgetOptions<State>, "imports" | "render">) {
return `${imports}\nexport default { render: ${render.toString()} }`;
}

/** The options bag to pass to the {@link widget} method. */
export type WidgetOptions<State> = {
/** The initial widget state. */
state: State;
/** A function that renders the widget. This function is serialized and sent to the front end. */
render: (context: {
Expand All @@ -197,18 +233,10 @@ export type WidgetProps<State> = {
}) => unknown;
/** The imports required for the front-end function. */
imports?: string;
/** The version of anywidget to use. */
/** The version of the anywidget front end to use. */
version?: string;
};

// TODO: more robust serialization of render function (with context?)
function to_esm<State>({
imports = "",
render,
}: Pick<WidgetProps<State>, "imports" | "render">) {
return `${imports}\nexport default { render: ${render.toString()} }`;
}

/**
* Creates an anywidget for the Deno Jupyter kernel.
*
Expand All @@ -234,21 +262,17 @@ function to_esm<State>({
* counter; // displays the widget
* ```
*
* @param props - The properties of the widget.
* @param props.state - The initial state of the widget (must be an object)
* @param props.render - A function that renders the widget in the front end. This function is serialized and sent to the front end.
* @param props.imports - The CDN ESM imports required for the front-end function.
* @param props.version - The version of anywidget to use.
* @param options - The options for the widget {@link WidgetOptions}.
*/
export function widget<State>(props: WidgetProps<State>): _Model<State> {
let { state, render, imports, version } = props;
export function widget<State>(options: WidgetOptions<State>): Model<State> {
let { state, render, imports, version } = options;
let comm = new Comm({ anywidget_version: version });
let init_promise = comm
.init()
.then(() =>
comm.send_state({ ...state, _esm: to_esm({ imports, render }) })
);
let model = new _Model(state);
let model = new Model(state);
for (let key in state) {
model.on(`change:${key}`, () => {
comm.send_state({ [key]: model.get(key) });
Expand Down
6 changes: 3 additions & 3 deletions packages/deno/src/uninstall.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import * as path from "@std/path";
import { find_data_dir } from "./jupyter_paths.ts";

/**
* @module
*
Expand All @@ -11,6 +8,9 @@ import { find_data_dir } from "./jupyter_paths.ts";
* ```
*/

import * as path from "@std/path";
import { find_data_dir } from "./jupyter_paths.ts";

let data_dir = await find_data_dir();

await Deno.readTextFile(
Expand Down