2 Commits
0.1.2 ... main

Author SHA1 Message Date
Morten Olsen
99248e98b5 prepare for client/server split 2024-12-11 12:32:23 +01:00
Morten Olsen
02614d216c feat: add build pipeline 2024-12-10 23:04:14 +01:00
47 changed files with 640 additions and 727 deletions

48
.github/release-drafter-config.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name-template: '$RESOLVED_VERSION 🌈'
tag-template: '$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
autolabeler:
- label: 'chore'
files:
- '*.md'
branch:
- '/docs{0,1}\/.+/'
- label: 'bug'
branch:
- '/fix\/.+/'
title:
- '/fix/i'
- label: 'enhancement'
branch:
- '/feature\/.+/'
- '/feat\/.+/'
title:
- '/feat:.+/'
template: |
## Changes
$CHANGES

21
.github/workflows/auto-labeler.yaml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Auto Labeler
on:
pull_request:
types: [opened, reopened, synchronize]
permissions:
contents: read
jobs:
auto-labeler:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter-config.yml
disable-releaser: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

133
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,133 @@
name: Build and release
on:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
env:
release_channel: latest
DO_NOT_TRACK: '1'
NODE_VERSION: '22.x'
NODE_REGISTRY: 'https://registry.npmjs.org'
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
permissions:
contents: read
packages: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '${{ env.NODE_VERSION }}'
registry-url: '${{ env.NODE_REGISTRY }}'
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.15.0
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build
run: pnpm build
- name: Run tests
run: pnpm test
- uses: actions/upload-artifact@v4
with:
name: lib
retention-days: 5
path: |
app/dist
packages/*/dist
package.json
README.md
update-release-draft:
name: Update release drafter
if: github.ref == 'refs/heads/main'
permissions:
contents: write
pull-requests: write
needs: build
environment: release
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
with:
config-name: release-drafter-config.yml
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release:
name: Release to NPM
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: update-release-draft
environment: release
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '${{ env.NODE_VERSION }}'
registry-url: '${{ env.NODE_REGISTRY }}'
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9.15.0
run_install: false
- name: Install dependencies
run: pnpm install
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- uses: actions/download-artifact@v4
with:
name: lib
path: ./
- run: |
git config user.name "Github Actions Bot"
git config user.email "<>"
node ./scripts/set-version.mjs $(git describe --tag --abbrev=0)
pnpm publish -r --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,6 +1,7 @@
{
"name": "plainidx",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.15.0",
"description": "",
"main": "index.js",
@@ -17,6 +18,8 @@
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.16.0",
"@pnpm/find-workspace-packages": "^6.0.9",
"@types/node": "^22.10.1",
"@vitest/coverage-v8": "2.1.8",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",

View File

@@ -1,5 +1,5 @@
{
"name": "@plainidx/react",
"name": "@plainidx/base",
"version": "1.0.0",
"type": "module",
"main": "dist/exports.js",
@@ -11,11 +11,10 @@
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"typescript": "^5.7.2"
},
"dependencies": {
"@plainidx/plainidx": "workspace:*"
"nanoid": "^5.0.9",
"zod": "^3.24.0"
}
}

View File

@@ -0,0 +1,8 @@
export { z } from 'zod';
export {
type Manifest,
type ManifestBackendAction,
type ManifestBackendActions,
type ManifestBackend,
type ManifestFrontend,
} from './manifest/manifest.js';

View File

@@ -0,0 +1,36 @@
import { ZodSchema } from 'zod';
type ManifestBackendAction = {
input: ZodSchema;
output: ZodSchema;
};
type ManifestBackendActions = Record<string, ManifestBackendAction>;
type ManifestBackend = {
main: string;
actions: ManifestBackendActions;
};
type ManifestFrontend = {
main: string;
};
type Manifest = {
id: string;
name: string;
version: string;
description?: string;
icon?: string;
config: ZodSchema;
frontend?: ManifestFrontend;
backend?: ManifestBackend;
};
export {
type Manifest,
type ManifestBackendAction,
type ManifestBackendActions,
type ManifestBackend,
type ManifestFrontend,
};

View File

@@ -1,8 +1,7 @@
{
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"jsx": "react"
"outDir": "dist"
},
"include": [
"src"

2
packages/client/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules/
/dist/

View File

@@ -0,0 +1,20 @@
{
"name": "@plainidx/client",
"version": "1.0.0",
"type": "module",
"main": "dist/exports.js",
"files": [
"dist"
],
"scripts": {
"build": "tsc --build"
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"typescript": "^5.7.2"
},
"dependencies": {
"@plainidx/base": "workspace:*",
"nanoid": "^5.0.9"
}
}

View File

@@ -0,0 +1,20 @@
import { Manifest } from '@plainidx/base';
import { Plugin } from '../plugin/plugin.js';
type ClientOptions = {
transport: unknown;
};
class Client {
#options: ClientOptions;
constructor(options: ClientOptions) {
this.#options = options;
}
public getPlugin = <TManifest extends Manifest>(manifest: TManifest) => {
return undefined as unknown as Plugin<TManifest>;
};
}
export { Client };

View File

@@ -0,0 +1,14 @@
import { Manifest, ManifestBackend, z } from '@plainidx/base';
type Plugin<TManifest extends Manifest> = {
manifest: TManifest;
actions: TManifest['backend'] extends ManifestBackend
? {
[TKey in keyof TManifest['backend']['actions']]: (
input: z.infer<TManifest['backend']['actions'][TKey]['input']>,
) => Promise<z.infer<TManifest['backend']['actions'][TKey]['output']>>;
}
: Record<string, never>;
};
export { Plugin };

View File

@@ -0,0 +1,9 @@
{
"extends": "@plainidx/configs/tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": [
"src"
]
}

View File

@@ -11,6 +11,7 @@
},
"devDependencies": {
"@plainidx/configs": "workspace:*",
"@plainidx/base": "workspace:*",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"typescript": "^5.7.2"

View File

@@ -1,39 +0,0 @@
import { EventEmitter } from '../utils/eventemitter.js';
type EditorPanel = {
name: string;
icon: string;
component: React.ComponentType;
};
type EditorPanelsEvents = {
change: () => void;
};
class EditorPanels extends EventEmitter<EditorPanelsEvents> {
#panels: Map<string, EditorPanel>;
constructor() {
super();
this.#panels = new Map();
}
public get panels() {
return this.#panels;
}
public add(id: string, panel: EditorPanel) {
this.#panels.set(id, panel);
this.emit('change');
return () => {
this.#panels.delete(id);
this.emit('change');
};
}
public get(id: string) {
return this.#panels.get(id);
}
}
export { EditorPanels };

View File

@@ -1,32 +0,0 @@
import { Document } from '../documents/documents.document.js';
import { EventEmitter } from '../utils/eventemitter.js';
type EditorRender = {
supports: (document: Document) => boolean;
name: string;
component: React.ComponentType;
};
type EditorRendersEvents = {
change: () => void;
};
class EditorRenders extends EventEmitter<EditorRendersEvents> {
#renders: EditorRender[] = [];
public add = (render: EditorRender) => {
this.#renders.push(render);
this.emit('change');
return () => {
this.#renders = this.#renders.filter((r) => r !== render);
this.emit('change');
};
};
public getByDocument = (document: Document) => {
return this.#renders.filter((r) => r.supports(document));
};
}
export { EditorRenders };

View File

@@ -1,33 +0,0 @@
import { EventEmitter } from '../utils/eventemitter.js';
import { EditorPanels } from './editor.panels.js';
import { EditorRenders } from './editor.renders.js';
import { EditorWorkspace } from './editor.workspace.js';
type EditorEvents = Record<string, never>;
class Editor extends EventEmitter<EditorEvents> {
#workspace: EditorWorkspace;
#renders: EditorRenders;
#panels: EditorPanels;
constructor() {
super();
this.#workspace = new EditorWorkspace();
this.#renders = new EditorRenders();
this.#panels = new EditorPanels();
}
public get workspace() {
return this.#workspace;
}
public get renders() {
return this.#renders;
}
public get panels() {
return this.#panels;
}
}
export { Editor };

View File

@@ -1,26 +0,0 @@
import { Document } from '../documents/documents.document.js';
import { EventEmitter } from '../utils/eventemitter.js';
type WorkspaceEvents = {
change: () => void;
};
class EditorWorkspace extends EventEmitter<WorkspaceEvents> {
#documents: Set<Document> = new Set<Document>();
public get documents() {
return [...this.#documents];
}
public openDocument(document: Document) {
this.#documents.add(document);
this.emit('change');
}
public closeDocument(document: Document) {
this.#documents.delete(document);
this.emit('change');
}
}
export { EditorWorkspace };

View File

@@ -2,11 +2,15 @@ export { PlainDB } from './plainidx/plainidx.js';
export { FileSystem } from './filesystem/filesystem.js';
export { Documents } from './documents/documents.js';
export { Document } from './documents/documents.document.js';
export { createActionApiRoute } from './plugins/plugin/plugin.api.js';
export { Plugins } from './plugins/plugins.js';
export { Plugin } from './plugins/plugin/plugin.js';
export { createPlugin } from './plugins/plugin/plugin.js';
export { Databases, DatabaseMigration } from './databases/databases.js';
export {
type Manifest,
type ManifestBackendAction,
type ManifestBackendActions,
type ManifestBackend,
type ManifestFrontend,
} from '@plainidx/base';
export * from 'zod';
export { type Knex as Database } from 'knex';
export { Editor } from './editor/editor.js';
export { EditorWorkspace } from './editor/editor.workspace.js';

View File

@@ -1,18 +0,0 @@
import { z, ZodSchema } from 'zod';
type PluginActionApi = Record<
string,
{
input?: ZodSchema;
output?: ZodSchema;
handle?: (input: any) => Promise<any>;
}
>;
const createActionApiRoute = <TInput extends ZodSchema = ZodSchema, TOutput extends ZodSchema = ZodSchema>(options: {
input?: TInput;
output?: TOutput;
handle?: (input: z.infer<TInput>) => Promise<z.infer<TOutput>>;
}) => options satisfies PluginActionApi[string];
export { type PluginActionApi, createActionApiRoute };

View File

@@ -1,110 +1,50 @@
import { z } from 'zod';
import { Document, Documents } from '../../documents/documents.js';
import { DatabaseMigration, Databases } from '../../databases/databases.js';
import { EventEmitter } from '../../utils/eventemitter.js';
import { Plugins } from '../plugins.js';
import { z, ZodSchema } from 'zod';
import { PluginActionApi } from './plugin.api.js';
import { Editor } from '../../editor/editor.js';
import { DatabaseMigration } from '../../databases/databases.js';
import { Knex } from 'knex';
import { Manifest, ManifestBackend } from '@plainidx/base';
type PluginOptions<TLocalConfig extends ZodSchema = ZodSchema, TSharedConfig extends ZodSchema = ZodSchema> = {
plugins: Plugins;
type Plugin<TManifest extends Manifest> = TManifest['backend'] extends ManifestBackend
? {
backend: true;
manifest: TManifest;
actions: {
[TKey in keyof TManifest['backend']['actions']]: (
input: z.infer<TManifest['backend']['actions'][TKey]['input']>,
) => Promise<z.infer<TManifest['backend']['actions'][TKey]['output']>>;
};
process?: (document: Document) => Promise<void>;
load?: () => Promise<void>;
unload?: () => Promise<void>;
}
: {
backend: false;
manifest: TManifest;
};
type PluginFactoryOptions<TManifest extends Manifest> = {
config: z.infer<TManifest['config']>;
documents: Documents;
databases: Databases;
configs: {
local?: TLocalConfig;
shared?: TSharedConfig;
};
getPlugin: <TManifest extends Manifest>(manifest: TManifest) => Promise<Plugin<TManifest>>;
getDb: (name: string, migrations: DatabaseMigration[]) => Promise<Knex>;
};
type PluginEvents = {
configChange: (config: unknown) => void;
};
type BackendPluginFactory<TManifest extends Manifest> = ((
options: PluginFactoryOptions<TManifest>,
) => Plugin<TManifest>) & { manifest: TManifest };
abstract class Plugin<
TLocalConfig extends ZodSchema = ZodSchema,
TSharedConfig extends ZodSchema = ZodSchema,
TActions extends PluginActionApi = PluginActionApi,
> extends EventEmitter<PluginEvents> {
#options: PluginOptions<TLocalConfig, TSharedConfig>;
const createPlugin = <TManifest extends Manifest>(
manifest: TManifest,
implementation: (options: PluginFactoryOptions<TManifest>) => Omit<Plugin<TManifest>, 'manifest' | 'backend'>,
): BackendPluginFactory<TManifest> =>
Object.assign(
(options: PluginFactoryOptions<TManifest>): Plugin<TManifest> =>
({
...implementation(options),
manifest,
backend: !!manifest.backend,
}) as Plugin<TManifest>,
{ manifest },
);
constructor(options: PluginOptions<TLocalConfig, TSharedConfig>) {
super();
this.#options = options;
}
public get documents(): Documents {
return this.#options.documents;
}
public readonly configSchemas?: {
local?: TLocalConfig;
shared?: TSharedConfig;
};
public getDB = async (name: string, migrations: DatabaseMigration[]) => {
const { databases } = this.#options;
const scopedName = `plugins:${this.name}:${name}`;
return databases.get({ name: scopedName, migrations });
};
public get configs(): {
local?: z.infer<TLocalConfig>;
shared?: z.infer<TSharedConfig>;
} {
return this.#options.configs;
}
public setConfigs = async (configs: { local?: z.infer<TLocalConfig>; shared?: z.infer<TSharedConfig> }) => {
this.#options.configs = configs;
await this.emit('configChange', configs);
};
public abstract readonly name: string;
public actions?: TActions;
public onLoad?: () => Promise<void>;
public onUnload?: () => Promise<void>;
public onLoaded?: () => Promise<void>;
public process?: (document: Document) => Promise<void>;
public setupUI?: (editor: Editor) => Promise<void>;
/*public getPlugin = async <T extends Plugin>(plugin: new (...args: any) => T): Promise<
T['api'] extends (...args: any[]) => infer R ? R : never
> => {
const { plugins } = this.#options;
const instance = await plugins.get(plugin);
return instance.api?.() as any;
}*/
public action = async <TPlugin extends Plugin<any, any, any>, TAction extends keyof TPlugin['actions']>(
plugin: new (...args: any[]) => TPlugin,
action: TAction,
input: Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['input'], undefined> extends ZodSchema
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['input'], undefined>>
: undefined,
): Promise<
Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined> extends ZodSchema
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined>>
: undefined
> => {
const { plugins } = this.#options;
const instance = await plugins.get(plugin);
const { actions } = instance;
if (!actions) {
throw new Error(`Plugin ${plugin.name} does not have actions`);
}
const actionDef = actions[action];
if (!actionDef) {
throw new Error(`Plugin ${plugin.name} does not have action ${String(action)}`);
}
actionDef.input?.parse(input);
return (await actionDef.handle?.(input)) as any;
};
}
type PluginConstructor<
TLocalConfig extends ZodSchema = ZodSchema,
TSharedConfig extends ZodSchema = ZodSchema,
T extends Plugin = Plugin,
> = new (options: PluginOptions<TLocalConfig, TSharedConfig>) => T;
export { Plugin, type PluginOptions, type PluginConstructor };
export { type Plugin, type BackendPluginFactory as PluginFactory, createPlugin };

View File

@@ -1,9 +1,8 @@
import { Document } from '../documents/documents.document.js';
import { Documents } from '../documents/documents.js';
import { Databases } from '../databases/databases.js';
import { Plugin, PluginConstructor } from './plugin/plugin.js';
import { z, ZodSchema } from 'zod';
import { Editor } from '../editor/editor.js';
import { Plugin, PluginFactory } from './plugin/plugin.js';
import { Manifest } from '@plainidx/base';
type PluginsOptions = {
documents: Documents;
@@ -12,7 +11,7 @@ type PluginsOptions = {
class Plugins {
#options: PluginsOptions;
#plugins: Map<PluginConstructor, Plugin>;
#plugins: Map<string, Plugin<any>>;
constructor(options: PluginsOptions) {
this.#options = options;
@@ -22,35 +21,20 @@ class Plugins {
#onSave = async (document: Document) => {
for (const plugin of this.#plugins.values()) {
await plugin.process?.(document);
if (plugin.backend) {
await plugin?.process?.(document);
}
}
};
#load = async (plugins: Plugin[]) => {
await Promise.all(plugins.map((plugin) => plugin.onLoad?.()));
plugins.forEach((plugin) => plugin.onLoaded?.());
};
#saveConfig = async (plugin: Plugin) => {
const document = await this.#options.documents.get(`.db/plugins/${plugin.name}/config.json`);
document.data = Buffer.from(JSON.stringify(plugin.configs));
await document.save();
};
public setupUI = (editor: Editor) => {
for (const plugin of this.#plugins.values()) {
plugin.setupUI?.(editor);
public get = async <TManifest extends Manifest>(manifest: TManifest): Promise<Plugin<TManifest>> => {
if (!this.#plugins.has(manifest.id)) {
throw new Error(`Plugin ${manifest.id} is not loaded`);
}
return this.#plugins.get(manifest.id) as Plugin<TManifest>;
};
public get = async <T extends Plugin>(plugin: PluginConstructor<any, any, T>): Promise<T> => {
if (!this.#plugins.has(plugin)) {
await this.add([plugin]);
}
return this.#plugins.get(plugin) as T;
};
public add = async (plugins: PluginConstructor[]) => {
public add = async (plugins: PluginFactory<any>[]) => {
const { documents, databases } = this.#options;
const configs = await Promise.all(
plugins.map(async (plugin) => {
@@ -58,58 +42,52 @@ class Plugins {
return JSON.parse(document.data.toString() || '{}');
}),
);
const instances = plugins.map(
(Plugin, i) =>
new Plugin({
plugins: this,
const instances = await Promise.all(
plugins.map(async (plugin, i) => {
const instance = plugin({
config: configs[i],
documents,
databases,
configs: configs[i],
getPlugin: this.get,
getDb: async (name, migrations) => {
return databases.get({
name: `plugins/${plugin.manifest.id}/${name}`,
migrations,
});
},
});
return instance as Plugin<any>;
}),
);
await this.#load(instances);
for (let i = 0; i < plugins.length; i++) {
const instance = instances[i];
const plugin = plugins[i];
instance.on('configChange', this.#saveConfig.bind(null, instance));
this.#plugins.set(plugin, instance);
for (const instance of instances) {
this.#plugins.set(instance.manifest.id, instance);
}
await Promise.all(
instances.map(async (instance) => {
if ('load' in instance) {
await instance.load?.();
}
}),
);
};
public process = async (document: Document) => {
for (const plugin of this.#plugins.values()) {
await plugin.process?.(document);
if (plugin.backend) {
await plugin?.process?.(document);
}
}
};
public unload = async () => {
await Promise.all(this.#plugins.values().map((plugin) => plugin.onUnload?.()));
await Promise.all(
this.#plugins.values().map((plugin) => {
if (plugin.backend) {
return plugin.unload?.();
}
}),
);
this.#plugins = new Map();
};
public action = async <TPlugin extends Plugin<any, any, any>, TAction extends keyof TPlugin['actions']>(
plugin: new (...args: any[]) => TPlugin,
action: TAction,
input: Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['input'], undefined> extends ZodSchema
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['input'], undefined>>
: undefined,
): Promise<
Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined> extends ZodSchema
? z.infer<Exclude<Exclude<TPlugin['actions'], undefined>[TAction]['output'], undefined>>
: undefined
> => {
const instance = await this.get(plugin);
const { actions } = instance;
if (!actions) {
throw new Error(`Plugin ${plugin.name} does not have actions`);
}
const actionDef = actions[action];
if (!actionDef) {
throw new Error(`Plugin ${plugin.name} does not have action ${String(action)}`);
}
actionDef.input?.parse(input);
return actionDef.handle?.(input) as any;
};
}
export { Plugins };

View File

@@ -1,10 +0,0 @@
import { Document } from '@plainidx/plainidx';
import React from 'react';
type DocumentContextType = {
document: Document;
};
const DocumentContext = React.createContext<DocumentContextType | undefined>(undefined);
export { DocumentContext };

View File

@@ -1,57 +0,0 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { DocumentContext } from './document.context.js';
import { usePlainDB } from '../plaindb/plaindb.hooks.js';
const useDocument = () => {
const context = useContext(DocumentContext);
if (!context) {
throw new Error('useDocument must be used within a DocumentProvider');
}
return context.document;
};
const useDocumentRenders = () => {
const { editor } = usePlainDB();
const document = useDocument();
const [current, setCurrent] = useState(editor.renders.getByDocument(document));
useEffect(() => {
const listen = () => {
setCurrent(editor.renders.getByDocument(document));
};
editor.renders.on('change', listen);
return () => {
editor.renders.off('change', listen);
};
}, [editor.renders, document, setCurrent]);
return current;
};
const useDocumentValue = () => {
const document = useDocument();
const [current, setCurrent] = useState(document.data);
const setValue = useCallback(
(newValue: Buffer) => {
document.data = newValue;
},
[document],
);
useEffect(() => {
const listen = () => {
setCurrent(document.data);
};
document.on('change', listen);
return () => {
document.off('change', listen);
};
}, [document, setCurrent]);
return [current, setValue] as const;
};
export { useDocument, useDocumentValue, useDocumentRenders };

View File

@@ -1,14 +0,0 @@
import React from 'react';
import { Document } from '@plainidx/plainidx';
import { DocumentContext } from './document.context.js';
type DocumentProviderProps = {
document: Document;
children: React.ReactNode;
};
const DocumentProvider = ({ document, children }: DocumentProviderProps) => {
return <DocumentContext.Provider value={{ document }}>{children}</DocumentContext.Provider>;
};
export { DocumentProvider };

View File

@@ -1,21 +0,0 @@
import { useEffect, useState } from 'react';
import { usePlainDB } from '../plaindb/plaindb.hooks.js';
const usePanel = (id: string) => {
const { editor } = usePlainDB();
const [panel, setPanel] = useState(editor.panels.get(id));
useEffect(() => {
const update = () => {
setPanel(editor.panels.get(id));
};
editor.panels.on('change', update);
return () => {
editor.panels.off('change', update);
};
}, [editor, id]);
return panel;
};
export { usePanel };

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { usePanel } from './panels.hooks.js';
type PanelProps = {
id: string;
};
const Panel = ({ id }: PanelProps) => {
const panel = usePanel(id);
if (!panel) {
return null;
}
const Component = panel.component;
return <Component />;
};
export { Panel };

View File

@@ -1,11 +0,0 @@
import { Editor, PlainDB } from '@plainidx/plainidx';
import React from 'react';
type PlainDBContextType = {
db: PlainDB;
editor: Editor;
};
const PlainDBContext = React.createContext<PlainDBContextType | undefined>(undefined);
export { PlainDBContext };

View File

@@ -1,12 +0,0 @@
import { useContext } from 'react';
import { PlainDBContext } from './plaindb.context.js';
const usePlainDB = () => {
const context = useContext(PlainDBContext);
if (context === undefined) {
throw new Error('usePlainDB must be used within a PlainDBProvider');
}
return context;
};
export { usePlainDB };

View File

@@ -1,19 +0,0 @@
import React, { useMemo } from 'react';
import { Editor, PlainDB } from '@plainidx/plainidx';
import { PlainDBContext } from './plaindb.context.js';
type PlainDBProviderProps = {
children: React.ReactNode;
db: PlainDB;
};
const PlainDBProvider = ({ children, db }: PlainDBProviderProps) => {
const editor = useMemo(() => {
const next = new Editor();
db.plugins.setupUI(editor);
return next;
}, [db]);
return <PlainDBContext.Provider value={{ db, editor }}>{children}</PlainDBContext.Provider>;
};
export { PlainDBProvider };

View File

@@ -1,35 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { usePlainDB } from '../plaindb/plaindb.hooks.js';
const useOpenDocument = () => {
const { editor, db } = usePlainDB();
const open = useCallback(
async (location: string) => {
const document = await db.documents.get(location);
editor.workspace.openDocument(document);
},
[editor, db],
);
return open;
};
const useDocuments = () => {
const { editor } = usePlainDB();
const [documents, setDocuments] = useState(editor.workspace.documents);
useEffect(() => {
const update = () => {
setDocuments(editor.workspace.documents);
};
editor.workspace.on('change', update);
return () => {
editor.workspace.off('change', update);
};
});
return documents;
};
export { useOpenDocument, useDocuments };

View File

@@ -6,6 +6,17 @@
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/exports.js"
},
"./manifest": {
"import": "./dist/manifest.js"
},
"./plugin": {
"import": "./dist/plugin.js"
}
},
"scripts": {
"build": "tsc --build"
},

View File

@@ -1,89 +0,0 @@
import { createActionApiRoute, type Database, Plugin, z } from '@plainidx/plainidx';
import { migrations } from './migrations/migrations.js';
class CorePlugin extends Plugin {
#db?: Promise<Database>;
public readonly name = '@builtin/core';
public actions = {
setTags: createActionApiRoute({
input: z.object({
document: z.string(),
tags: z.array(z.string()),
}),
output: z.undefined(),
handle: async ({ document, tags }) => {
await this.setTags(document, tags);
return undefined;
},
}),
getTags: createActionApiRoute({
output: z.array(
z.object({
name: z.string(),
count: z.number(),
}),
),
handle: async () => {
return this.getTags();
},
}),
setTitle: createActionApiRoute({
input: z.object({
document: z.string(),
title: z.string(),
}),
output: z.undefined(),
handle: async ({ document, title }) => {
await this.setTitle(document, title);
return undefined;
},
}),
getTitles: createActionApiRoute({
output: z.array(
z.object({
document: z.string(),
title: z.string(),
}),
),
handle: async () => {
return this.getTitles();
},
}),
};
#getDatabase = async () => {
if (!this.#db) {
this.#db = this.getDB('data', migrations);
}
return this.#db;
};
public getTags = async () => {
const db = await this.#getDatabase();
return db('tags')
.select([db.raw('tag as name'), db.raw('count(document) as count')])
.groupBy('tag');
};
public setTags = async (document: string, tags: string[]) => {
const db = await this.#getDatabase();
await db('tags').where({ document }).delete();
if (tags.length) {
await db('tags').insert(tags.map((tag) => ({ tag, document })));
}
};
public setTitle = async (document: string, title: string) => {
const db = await this.#getDatabase();
await db('titles').where({ document }).insert({ document, title }).onConflict('document').merge();
};
public getTitles = async () => {
const db = await this.#getDatabase();
return await db('titles').select('*');
};
}
export { CorePlugin };

View File

@@ -1 +1,2 @@
export { CorePlugin } from './core.js';
export { core } from './plugin.js';
export { CorePlugin } from './manifest.js';

View File

@@ -0,0 +1,31 @@
import { Manifest, z } from '@plainidx/plainidx';
const CorePlugin = {
id: 'buildin-core',
name: 'Core Plugin',
version: '0.0.1',
config: z.any(),
backend: {
main: './dist/plugin.js',
actions: {
getTitles: {
input: z.object({}),
output: z.array(z.object({ location: z.string(), title: z.string() })),
},
getTags: {
input: z.object({}),
output: z.array(z.string()),
},
setData: {
input: z.object({
location: z.string(),
title: z.string(),
tags: z.array(z.string()),
}),
output: z.any(),
},
},
},
} satisfies Manifest;
export { CorePlugin };

View File

@@ -5,15 +5,15 @@ const migrations: DatabaseMigration[] = [
name: 'init',
up: async (db) => {
await db.schema.createTable('tags', (table) => {
table.string('tag').notNullable();
table.string('document').notNullable();
table.primary(['tag', 'document']);
table.index('document');
table.index('tag');
table.string('name').notNullable();
table.string('location').notNullable();
table.primary(['name', 'location']);
table.index('location');
table.index('name');
});
await db.schema.createTable('titles', (table) => {
table.string('document').primary().notNullable();
table.string('location').primary().notNullable();
table.string('title').notNullable();
});
},

View File

@@ -0,0 +1,39 @@
import { createPlugin } from '@plainidx/plainidx';
import { CorePlugin } from './manifest.js';
import { migrations } from './migrations/migrations.js';
const core = createPlugin(CorePlugin, ({ getDb }) => {
const dbPromise = getDb('data', migrations);
return {
backend: true,
actions: {
getTitles: async () => {
const db = await dbPromise;
const results = await db('titles').select(['location', 'title']);
return results;
},
getTags: async () => {
const db = await dbPromise;
const results = await db('tags')
.select(['name', db.raw('count(*) as count')])
.groupBy('name');
return results;
},
setData: async ({ location, title, tags }) => {
const db = await dbPromise;
const currentTitle = await db('titles').select('title').where({ location }).first();
await db.transaction(async (trx) => {
await trx('tags').delete().where({ location });
await trx('tags').insert(tags.map((tag) => ({ name: tag, location })));
if (currentTitle) {
await trx('titles').update({ title }).where({ location });
} else {
await trx('titles').insert({ location, title });
}
});
},
},
};
});
export { core };

View File

@@ -1 +1,2 @@
export { MarkdownPlugin } from './markdown.js';
export { markdownPlugin } from './markdown.js';
export { MarkdownPlugin } from './manifest.js';

View File

@@ -0,0 +1,14 @@
import { Manifest, z } from '@plainidx/plainidx';
const MarkdownPlugin = {
id: 'markdown',
name: 'Markdown Plugin',
version: '0.0.1',
config: z.any(),
backend: {
main: './dist/plugin.js',
actions: {},
},
} satisfies Manifest;
export { MarkdownPlugin };

View File

@@ -1,34 +1,24 @@
import { createActionApiRoute, Document, Plugin, z } from '@plainidx/plainidx';
import { MarkdownAst } from './utils/markdown-ast.js';
import { CorePlugin } from '@plainidx/plugin-core';
import { MarkdownPlugin } from './manifest.js';
import { MarkdownAst } from './utils/markdown-ast.js';
import { createPlugin } from '@plainidx/plainidx';
type MarkdownSubPlugin = {
process: (ast: MarkdownAst) => Promise<void>;
};
class MarkdownPlugin extends Plugin {
#subPlugins = new Set<MarkdownSubPlugin>();
public readonly name = '@builtin/markdown';
public actions = {
register: createActionApiRoute({
input: z.object({
plugin: z.custom<MarkdownSubPlugin>(),
}),
handle: async ({ plugin }) => {
this.#subPlugins.add(plugin);
},
}),
};
public process = async (document: Document) => {
const markdownPlugin = createPlugin(MarkdownPlugin, ({ getPlugin }) => {
const plugins: MarkdownSubPlugin[] = [];
return {
actions: {},
process: async (document) => {
const core = await getPlugin(CorePlugin);
if (!document.location.endsWith('.md')) {
return;
}
const ast = new MarkdownAst(document.data);
for (const plugin of this.#subPlugins) {
for (const plugin of plugins) {
await plugin.process(ast);
}
@@ -49,11 +39,6 @@ class MarkdownPlugin extends Plugin {
}
});
await this.action(CorePlugin, 'setTitle', {
title,
document: document.location,
});
ast.visit((node) => {
if (node.type === 'textDirective' && node.name === 'tag') {
const body = ast.nodeToString(node.children);
@@ -61,11 +46,14 @@ class MarkdownPlugin extends Plugin {
}
});
document.replace(ast.toBuffer());
await this.action(CorePlugin, 'setTags', {
tags,
document: document.location,
});
};
}
export { MarkdownPlugin };
await core.actions.setData({
location: document.location,
title,
tags,
});
},
};
});
export { markdownPlugin };

View File

@@ -1,8 +1,8 @@
import { describe, it } from 'vitest';
import { CorePlugin } from '@plainidx/plugin-core';
import { MarkdownPlugin } from '@plainidx/plugin-markdown';
import { core, CorePlugin } from '@plainidx/plugin-core';
import { PlainDB } from '../../plainidx/dist/exports.js';
import { MemoryFileSystem } from '@plainidx/fs-memory';
import { markdownPlugin } from '@plainidx/plugin-markdown';
describe('documents', () => {
it('should be able to create a document', async () => {
@@ -19,34 +19,24 @@ describe('documents', () => {
}),
});
await plugins.add([MarkdownPlugin]);
await plugins.add([core, markdownPlugin]);
const tags1 = await plugins.action(CorePlugin, 'getTags', undefined);
const plugin = await plugins.get(CorePlugin);
console.log('Done', tags1);
{
const tags = await plugin.actions.getTags({});
console.log(tags);
}
const demoDocument = await documents.get('hello/world.md');
const document = await documents.get('foo/bar.md');
demoDocument.data = Buffer.from(`
# Hello World
document.data = Buffer.from(['# Hello World', '', ':tag[test]'].join('\n'));
:tag[hello]
`);
await demoDocument.save();
const tags2 = await plugins.action(CorePlugin, 'getTags', undefined);
console.log('Done', tags2);
demoDocument.data = Buffer.from(`
# Hello World
:tag[world]
`);
await demoDocument.save();
const tags3 = await plugins.action(CorePlugin, 'getTags', undefined);
console.log('Done', tags3);
await document.save();
{
const tags = await plugin.actions.getTags({});
console.log(tags);
}
await close();
});

58
pnpm-lock.yaml generated
View File

@@ -14,6 +14,12 @@ importers:
'@eslint/js':
specifier: ^9.16.0
version: 9.16.0
'@pnpm/find-workspace-packages':
specifier: ^6.0.9
version: 6.0.9(@pnpm/logger@5.2.0)
'@types/node':
specifier: ^22.10.1
version: 22.10.1
'@vitest/coverage-v8':
specifier: 2.1.8
version: 2.1.8(vitest@2.1.8(@types/node@22.10.1))
@@ -42,6 +48,38 @@ importers:
specifier: ^2.1.8
version: 2.1.8(@types/node@22.10.1)
packages/base:
dependencies:
nanoid:
specifier: ^5.0.9
version: 5.0.9
zod:
specifier: ^3.24.0
version: 3.24.0
devDependencies:
'@plainidx/configs':
specifier: workspace:*
version: link:../configs
typescript:
specifier: ^5.7.2
version: 5.7.2
packages/client:
dependencies:
'@plainidx/base':
specifier: workspace:*
version: link:../base
nanoid:
specifier: ^5.0.9
version: 5.0.9
devDependencies:
'@plainidx/configs':
specifier: workspace:*
version: link:../configs
typescript:
specifier: ^5.7.2
version: 5.7.2
packages/configs: {}
packages/fs-memory:
@@ -91,25 +129,9 @@ importers:
specifier: ^3.24.0
version: 3.24.0
devDependencies:
'@plainidx/configs':
'@plainidx/base':
specifier: workspace:*
version: link:../configs
'@types/node':
specifier: ^22.10.1
version: 22.10.1
'@types/react':
specifier: ^19.0.1
version: 19.0.1
typescript:
specifier: ^5.7.2
version: 5.7.2
packages/platform-react:
dependencies:
'@plainidx/plainidx':
specifier: workspace:*
version: link:../plainidx
devDependencies:
version: link:../base
'@plainidx/configs':
specifier: workspace:*
version: link:../configs

View File

@@ -1,2 +1,3 @@
packages:
- app
- packages/*

15
scripts/set-version.mjs Normal file
View File

@@ -0,0 +1,15 @@
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { findWorkspacePackages } from '@pnpm/find-workspace-packages';
const packages = await findWorkspacePackages(process.cwd());
for (const pkg of packages) {
const pkgPath = join(pkg.dir, 'package.json');
const pkgJson = JSON.parse(await readFile(pkgPath, 'utf-8'));
pkgJson.version = process.argv[2];
await writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
}

View File

@@ -1,6 +1,9 @@
{
"include": [],
"references": [
{
"path": "./packages/base"
},
{
"path": "./packages/plainidx"
},
@@ -18,9 +21,6 @@
},
{
"path": "./packages/plugin-markdown"
},
{
"path": "./packages/platform-react"
}
]
}