This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Nix 85.9%
Shell 14.1%
4 1 0

Clone this repository

https://tangled.org/7778777.online/impro-flake https://tangled.org/did:plc:buboojb7inxdddgtwkb3ikdp
git@tangled.org:7778777.online/impro-flake git@tangled.org:did:plc:buboojb7inxdddgtwkb3ikdp

For self-hosted knots, clone URLs may differ based on your setup.



README.md

impro_flake#

Nix flake + direnv workspace for developing Impro and its plugins. Contains three independent git repositories side by side:

Path Purpose Remote
impro/ Impro web client (Eleventy SPA). The plugin host. tangled-7778777:7778777.online/impro
impro-sample-plugin/ Reference plugin. Starter template for new plugins. tangled-7778777:7778777.online/impro-sample-plugin
. (this repo) The flake + direnv configuration that wraps both projects. local-only at present

The flake repo deliberately ignores both project subdirectories (see .gitignore). This is what stops nix build / nix develop from copying their node_modules/, build/, and plugin output into the nix store on every rebuild — flake evaluation only sees flake.nix, flake.lock, .envrc, and this README.md.


Prerequisites#

  • Nix 2.20+ with flakes enabled (experimental-features = flakes nix-command)
  • direnv, hooked into your shell (eval "$(direnv hook zsh)" in ~/.zshrc)

Both are already configured on this machine.


First-time setup#

cd ~/impro_flake
direnv allow                       # loads the dev shell from flake.nix
cd impro                && npm ci  # install host deps
cd ../impro-sample-plugin && npm ci # install plugin SDK + esbuild

If npm ci complains about un-reviewed install scripts, run npm approve-scripts --all once in each project. This is npm 11+ policy.


What the dev shell provides#

flake.nix exposes a single devShells.default containing:

Tool Why
nodejs_22 (Node 22.x, npm) Runtime + package manager for both projects
typescript-language-server LSP for TypeScript / JavaScript (editor-agnostic)
typescript (tsc) Backing tsserver for the LSP
vscode-langservers-extracted HTML, CSS, JSON, ESLint language servers
prettier Code formatter (impro also has a local copy via npm)
playwright-driver End-to-end browser binaries for npm run test:e2e
git Version control

PLAYWRIGHT_BROWSERS_PATH is exported to the nix-installed browser bundle, so Playwright won't try to re-download Chromium/Firefox/WebKit into ~/.cache/ms-playwright.

Build, run, test, debug#

All commands run inside the relevant project subdirectory.

impro/ (host)#

Task Command
Dev server npm run start (live-reload on :8080)
Production build npm run build (outputs to build/)
Serve built bundle npm run serve:static
Unit tests npm run test:unit
Unit tests + coverage npm run test:unit:coverage
E2E tests npm run test:e2e
Bundle client libs npm run bundle:capacitor, npm run bundle:lit-html
Debug node process node --inspect-brk <script>

Pre-commit hook (.husky/pre-commit) runs prettier --staged + unit tests.

impro-sample-plugin/ (and any plugin you fork from it)#

Task Command
One-shot build npm run build (esbuild → main.js)
Watch mode npm run watch / npm start
Debug esbuild node --inspect-brk node_modules/.bin/esbuild src/main.js --bundle --format=cjs --outfile=main.js

Editor debug: use node --inspect-brk and attach via chrome://inspect or VS Code's "Attach to Node Process" command.


Plugin development#

The Impro plugin system runs each plugin in a sandboxed Web Worker. Plugins talk to the host via the @impro.social/impro-plugin SDK, which ships its own RPC layer (see impro/impro-plugin/main.js for the host-side source).

  1. Develop against the local SDK. The sample plugin's package.json pulls @impro.social/impro-plugin from a public registry, but the up-to-date SDK source lives at impro/impro-plugin/main.js in this workspace. To develop against the local copy, point npm at it:

    cd impro-sample-plugin
    npm install --save ../impro/impro-plugin
    

    Then npm run build will bundle the latest local SDK into your plugin. (Revert this before publishing.)

  2. Symlink your plugin into the host. Already done for the sample plugin:

    ln -s ../../impro-sample-plugin impro/plugins-local/impro-sample-plugin
    

    The target is relative to the symlink's location (impro/plugins-local/), so it takes two .. to climb out of impro/ before entering impro-sample-plugin/. A single .. will silently resolve to a non-existent path and break the build with an ENOENT from fs.realpathSync in eleventy.config.js.

    While the dev server is running, Impro copies any symlinked plugin from impro/plugins-local/ into build/plugins-local/, appending __LOCAL to its id, and regenerates index.json. The plugin shows up at http://localhost:8080/settings/plugins/community as impro-sample-plugin__LOCAL.

  3. Iterate. Run two terminals:

    # terminal 1: rebuild plugin on save
    cd ~/impro_flake/impro-sample-plugin && npm run watch
    
    # terminal 2: impro dev server (auto-reloads on plugin main.js change)
    cd ~/impro_flake/impro && npm run start
    

    Toggle the plugin on in http://localhost:8080/settings/plugins/community, then exercise its features (sidebar lightning-bolt icon, post context-menu item, settings tab).

Publishing a plugin version#

Per Impro convention (impro/plugins.md):

  1. Tag a commit with the bare version number (e.g. 0.2.0, no v prefix) and push it to your public git remote.
  2. To get into the Community Plugins listing, open a PR to improsocial/impro-releases with the plugin info.

Plugin API reference (cheat-sheet)#

All exports below come from @impro.social/impro-plugin. Source of truth: impro/impro-plugin/main.js.

Lifecycle#

import { Plugin } from "@impro.social/impro-plugin";

export default class MyPlugin extends Plugin {
  async onload() {
    this.settings = { ...(await this.loadData()) }; // persisted per-user
    // register UI, listeners, settings, etc. here
  }
  onunload() { /* cleanup */ }
}

Plugin.register() is the entrypoint called by the host; subclasses must set themselves as the default export.

UI registration methods (on Plugin)#

Method Effect
addSidebarItem(icon, title, cb) Adds a left-sidebar entry; cb invoked on click.
addSettingTab(tab) Registers a PluginSettingTab instance.
addFeedFilter(cb(uri, post)) Filters posts in feeds; cb returns truthy to keep.
registerRichTextTransform(cb, opts) Rewrites the rich-text token stream per post.
registerSlot(name, cb) Renders custom HTML into a named host slot.
app.on(event, listener) Subscribes to host events (see below).

Host events (app.on)#

  • post-context-menu(menu) => {} — add items to post menu
  • profile-context-menu(menu) => {} — add items to profile menu
  • post-composer-open(composer) => {} — mutate the composer

menu.addItem(item => item.setTitle(...).setIcon(...).onClick(...)). composer.setText/appendText/prependText(text) / .setCursor(index).

Host bridge (read-only data access)#

const post    = await this.app.data.getPost(uri);
const profile = await this.app.data.getProfile(did);
const record  = await this.app.data.getRecord(repo, collection, rkey);
const me      = this.app.currentUser;        // populated before onload()

For network access, use the SDK's fetch() (whitelist-gated, not the global one). Returns a PluginResponse with .status / .ok / .headers / .text() / .json().

UI primitives#

  • VirtualEl — tag/attrs/text/children builder; use createEl, createDiv, createSpan, setText, addClass, setAttr, onClick/onChange/onInput. Specialised constructors: createProfilesList, createPostsFeed, createIcon.
  • Modal — subclass and override onOpen/onClose; build into this.contentEl / this.titleEl; call .open() / .close().
  • Notice — toast: new Notice("hi", 5000).setMessage(...).hide().
  • StyleSnippet — injected CSS: new StyleSnippet(".x { ... }"), .ready promise, .remove().
  • Setting — setting row builder: new Setting(containerEl).setName(...).setDesc(...).addText/addToggle/addDropdown/addButton(...).
  • PluginSettingTab — subclass, override display(), call this.refresh({ reset: true }) to re-render after data changes.
  • FlattenedTokens / flattenForScan(tokens) — helpers for rich-text transforms that need to pattern-match across token boundaries.

Constraints#

Plugins cannot make arbitrary network requests or read/modify page HTML directly. Everything happens through the SDK's RPC bridge. See impro/plugins.md for the canonical "can / cannot" list.


Git identity#

All commits across all three repositories in this workspace are authored as:

Nameless 7778777 <7778777@7778777.online>

This is enforced three ways (defence in depth):

  1. Per-repo local configgit config --local user.{name,email} is set in .git/config for impro/, impro-sample-plugin/, and the outer flake repo.
  2. Direnv env vars.envrc exports GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL / GIT_COMMITTER_NAME / GIT_COMMITTER_EMAIL. These shadow the shell's own GIT_* exports (which point at a different identity) whenever you are inside ~/impro_flake/, including subdirectories.
  3. No cryptographic signing — per setup choice "identity only"; no GPG/SSH signing is configured.

To verify before committing:

git config user.name && git config user.email
echo "$GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL>"

Troubleshooting#

  • direnv: error / nix-direnv: ... — run direnv allow after any change to .envrc or flake.nix. The cache lives in .direnv/ (gitignored).
  • nix flake lock: bad optional access — usually a Nix-evaluation error masquerading as a C++ exception. Run nix flake lock --show-trace to see the real cause.
  • nodePackages has been removed — current nixpkgs moved most node tools to top-level (pkgs.typescript-language-server etc.). The flake already uses the new names.
  • package-lock.json shows a different version than package.json — the lockfiles in both upstream repos are out of sync with the bumped package.json versions. Running npm install regenerates them; commit or discard as you see fit.
  • npm approve-scripts — npm 11+ blocks dependency install scripts until you approve them. Run npm approve-scripts --all (writes an allowScripts map into package.json).
  • Playwright tries to download browsers — the dev shell sets PLAYWRIGHT_BROWSERS_PATH; if it is unset, direnv hasn't loaded. Re-run direnv allow and verify with echo $PLAYWRIGHT_BROWSERS_PATH.