Skip to content

Commit 393eb83

Browse files
SHAcollisiontipogi
andauthored
feat: generate isomorphic code bundle/nodejs (#60)
* feat: generate isomorphic code bundle/nodejs * add sync load of wasm in ES modules * add/fix workflow * use text blocks for macro examples to avoid doctest CI failures * create the root pkg folder * prepare for publishing the rc * change JS binding workflow path * last changes: readme and package version --------- Co-authored-by: tipogi <tipogi@protonmail.com>
1 parent e4014c5 commit 393eb83

File tree

14 files changed

+791
-78
lines changed

14 files changed

+791
-78
lines changed

.github/workflows/js-binding.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: JS Binding Tests
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Install Rust
17+
uses: dtolnay/rust-toolchain@stable
18+
with:
19+
targets: wasm32-unknown-unknown
20+
21+
- name: Cache Rust build artifacts
22+
uses: Swatinem/rust-cache@v2
23+
with:
24+
cache-targets: "wasm32-unknown-unknown"
25+
26+
- name: Install wasm-pack
27+
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
28+
29+
- name: Install Node.js
30+
uses: actions/setup-node@v4
31+
with:
32+
node-version: '22.13.0'
33+
34+
- name: Build WASM package
35+
run: |
36+
# Change to the js directory and run build wasm
37+
cd ./pkg
38+
npm run build
39+
40+
- name: Run JS tests
41+
run: |
42+
cd ./pkg
43+
npm install
44+
echo "Running JavaScript tests..."
45+
npm run test
46+
47+
echo "All tests completed successfully!"

README.md

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Pubky.app Data Model Specification
22

3-
_Version 0.3.5_
3+
_Version 0.4.0_
44

55
> ⚠️ **Warning: Rapid Development Phase**
66
> This specification is in an **early development phase** and is evolving quickly. Expect frequent changes and updates as the system matures. Consider this a **v0 draft**.
@@ -9,41 +9,49 @@ _Version 0.3.5_
99
1010
### JS package
1111

12-
Build with
12+
The package is available as an npm module [pubky-app-specs](https://www.npmjs.com/package/pubky-app-specs). Alternatively, you can build from source using the provided build scripts:
1313

1414
```bash
15-
./build.sh
15+
cd pkg
16+
npm run build
1617
```
1718

18-
Test with
19+
Test with:
1920

2021
```bash
21-
wasm-pack test --headless --firefox
22+
cd pkg
23+
npm run install
24+
npm run test
2225
```
2326

27+
Examples with:
28+
29+
```bash
30+
cd pkg
31+
npm run example
32+
```
33+
34+
---
35+
2436
## Table of Contents
2537

26-
1. [Introduction](#introduction)
27-
2. [Quick Start](#quick-start)
28-
3. [Data Models](#data-models)
29-
- [PubkyAppUser](#pubkyappuser)
30-
- [PubkyAppFile](#pubkyappfile)
31-
- [PubkyAppPost](#pubkyapppost)
32-
- [PubkyAppTag](#pubkyapptag)
33-
- [PubkyAppBookmark](#pubkyappbookmark)
34-
- [PubkyAppFollow](#pubkyappfollow)
35-
- [PubkyAppMute](#pubkyappmute)
36-
- [PubkyAppFeed](#pubkyappfeed)
37-
- [PubkyAppLastRead](#pubkyapplastread)
38-
4. [Validation Rules](#validation-rules)
39-
- [Common Rules](#common-rules)
40-
- [ID Generation](#id-generation)
41-
5. [Glossary](#glossary)
42-
6. [Examples](#examples)
43-
- [PubkyAppUser](#example-pubkyappuser)
44-
- [PubkyAppPost](#example-pubkyapppost)
45-
- [PubkyAppTag](#example-pubkyapptag)
46-
7. [License](#license)
38+
- [Pubky.app Data Model Specification](#pubkyapp-data-model-specification)
39+
- [JS package](#js-package)
40+
- [Table of Contents](#table-of-contents)
41+
- [Introduction](#introduction)
42+
- [Quick Start](#quick-start)
43+
- [Concepts:](#concepts)
44+
- [Data Models](#data-models)
45+
- [PubkyAppUser](#pubkyappuser)
46+
- [PubkyAppFile](#pubkyappfile)
47+
- [PubkyAppPost](#pubkyapppost)
48+
- [PubkyAppTag](#pubkyapptag)
49+
- [PubkyAppBookmark](#pubkyappbookmark)
50+
- [PubkyAppFollow](#pubkyappfollow)
51+
- [PubkyAppFeed](#pubkyappfeed)
52+
- [Validation Rules](#validation-rules)
53+
- [Common Rules](#common-rules)
54+
- [License](#license)
4755

4856
---
4957

build.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
#!/bin/bash
2-
3-
42
echo "🦀 Testing WebAssembly package..."
53
wasm-pack test --headless --firefox
64

75
echo "🦀 Building WebAssembly package..."
8-
wasm-pack build --target web --out-dir dist
6+
cargo run --bin bundle_specs_npm
97

108
echo "📋 Copying package.json and Readme files to /dist..."
11-
cp pkg/* dist/
9+
cp bindings/js/* dist/
1210

1311
echo "✨ Building and testing completed!"

pkg/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
pubky*
2+
index.js
3+
index.cjs
4+
index.d.ts
5+
index_bg.wasm
6+
nodejs*
7+
node_modules*
8+
package-lock.json

pkg/README.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 🦄 Pubky App Specs (WASM) · `pubky-app-specs`
1+
# Pubky App Specs · `pubky-app-specs`
22

33
A WASM library for building and validating structured JSON models compatible with Pubky.App social powered by [`@synonymdev/pubky`](https://www.npmjs.com/package/@synonymdev/pubky). It handles domain objects like **Users**, **Posts**, **Feeds**, **Bookmarks**, **Tags**, and more. Each object is:
44

@@ -24,27 +24,28 @@ npm install pubky-app-specs
2424
yarn add pubky-app-specs
2525
```
2626

27-
> **Note**: This package uses WASM. Ensure your bundler or environment supports loading WASM modules (e.g. Next.js, Vite, etc.).
27+
> **Note**: This package uses WASM with embedded bytes for automatic initialization. No manual WASM loading required - just import and use!
2828
2929
---
3030

3131
## 🚀 Quick Start
3232

33-
1. **Initialize** the WASM module.
33+
1. **Import** the library.
3434
2. **Construct** a `PubkySpecsBuilder(pubkyId)` object.
3535
3. **Create** validated domain objects (User, Post, Tag, etc.).
3636
4. **Store** them on the [PubKy homeserver](https://github.com/synonymdev/pubky) or any distributed storage solution you prefer.
3737

38-
### Import & Initialize
38+
### Import & Usage
3939

4040
```js
41-
import init, { PubkySpecsBuilder } from "pubky-app-specs";
41+
// ES Modules
42+
import { PubkySpecsBuilder } from "pubky-app-specs";
4243

43-
async function loadSpecs(pubkyId) {
44-
// 1. Initialize WASM
45-
await init();
44+
// OR CommonJS
45+
const { PubkySpecsBuilder } = require("pubky-app-specs/index.cjs");
4646

47-
// 2. Create a specs builder instance
47+
function loadSpecs(pubkyId) {
48+
// Create a specs builder instance - WASM is already initialized
4849
const specs = new PubkySpecsBuilder(pubkyId);
4950
return specs;
5051
}
@@ -67,19 +68,19 @@ async function createUser(pubkyId) {
6768
const specs = new PubkySpecsBuilder(pubkyId);
6869

6970
// Create user object with minimal fields
70-
const userResult = specs.createUser(
71+
const {user, meta} = specs.createUser(
7172
"Alice", // Name
7273
"Hello from WASM", // Bio
7374
null, // Image URL or File
7475
null, // Links
7576
"active" // Status
7677
);
7778

78-
// userResult.meta contains { id, path, url }.
79-
// userResult.user is the Rust "PubkyAppUser" object.
79+
// meta contains { id, path, url }.
80+
// user is the Rust "PubkyAppUser" object.
8081

8182
// We bring the Rust object to JS using the .toJson() method.
82-
const userJson = userResult.user.toJson();
83+
const userJson = user.toJson();
8384

8485
// Store in homeserver via pubky
8586
const response = await client.fetch(userResult.meta.url, {
@@ -109,7 +110,7 @@ async function createPost(pubkyId, content) {
109110
const specs = new PubkySpecsBuilder(pubkyId);
110111

111112
// Create the Post object referencing your (optional) attachment
112-
const postResult = specs.createPost(
113+
const {post, meta} = specs.createPost(
113114
content,
114115
PubkyAppPostKind.Short,
115116
null, // parent post
@@ -118,14 +119,14 @@ async function createPost(pubkyId, content) {
118119
);
119120

120121
// Store the post
121-
const postJson = postResult.post.toJson();
122-
await client.fetch(postResult.meta.url, {
122+
const postJson = post.toJson();
123+
await client.fetch(meta.url, {
123124
method: "PUT",
124125
body: JSON.stringify(postJson),
125126
});
126127

127-
console.log("Post stored at:", postResult.meta.url);
128-
return postResult;
128+
console.log("Post stored at:", meta.url);
129+
return {post, meta};
129130
}
130131
```
131132

@@ -139,12 +140,12 @@ async function followUser(myPubkyId, userToFollow) {
139140
const client = new Client();
140141
const specs = new PubkySpecsBuilder(myPubkyId);
141142

142-
const followResult = specs.createFollow(userToFollow);
143+
const {follow, meta} = specs.createFollow(userToFollow);
143144

144145
// We only need to store the JSON in the homeserver
145-
await client.fetch(followResult.meta.url, {
146+
await client.fetch(meta.url, {
146147
method: "PUT",
147-
body: JSON.stringify(followResult.follow.toJson()),
148+
body: JSON.stringify(follow.toJson()),
148149
});
149150

150151
console.log(`Successfully followed: ${userToFollow}`);

pkg/example.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { PubkyAppPostKind, PubkySpecsBuilder, PubkyAppPostEmbed } from "./index.js";
2+
3+
const OTTO = "8kkppkmiubfq4pxn6f73nqrhhhgkb5xyfprntc9si3np9ydbotto";
4+
const RIO = "dzswkfy7ek3bqnoc89jxuqqfbzhjrj6mi8qthgbxxcqkdugm3rio";
5+
6+
// 👤 Create a user profile
7+
console.log("👤 Creating User Profile...");
8+
const specsBuilder = new PubkySpecsBuilder(OTTO);
9+
const { user, meta: userMeta } =
10+
specsBuilder.createUser("Alice Smith", "Software Developer", null, null, "active");
11+
console.log("User Profile URL:", userMeta.url);
12+
console.log("User Data:", JSON.stringify(user.toJson(), null, 2));
13+
console.log("-".repeat(60));
14+
15+
// 📝 Create different posts
16+
console.log("📝 Creating First Post...");
17+
const { post, meta } = specsBuilder.createPost("Hello, Pubky world! This is my first post.", PubkyAppPostKind.Short, null, null, null);
18+
console.log("Post ID:", meta.id);
19+
console.log("Post URL:", meta.url);
20+
console.log("Post Data:", JSON.stringify(post.toJson(), null, 2));
21+
console.log("-".repeat(60));
22+
23+
console.log("💬 Creating Reply Post...");
24+
const { post: replyPost, meta: replyMeta } = specsBuilder.createPost("This is a reply to the first post!", PubkyAppPostKind.Short, userMeta.url, null, null);
25+
console.log("Reply Post ID:", replyMeta.id);
26+
console.log("Reply Post URL:", replyMeta.url);
27+
console.log("Reply Data:", JSON.stringify(replyPost.toJson(), null, 2));
28+
console.log("-".repeat(60));
29+
30+
console.log("🔄 Creating Repost with Embed...");
31+
let embeed = new PubkyAppPostEmbed(`pubky://${RIO}/pub/pubky.app/posts/0033SREKPC4N0`, PubkyAppPostKind.Video);
32+
const { post: repost, meta: repostMeta } = specsBuilder.createPost("This is a repost to random post!", PubkyAppPostKind.Short, null, embeed, null);
33+
console.log("Repost Post ID:", repostMeta.id);
34+
console.log("Repost Post URL:", repostMeta.url);
35+
console.log("Repost Data:", JSON.stringify(repost.toJson(), null, 2));
36+
console.log("-".repeat(60));
37+
38+
console.log("🔖 Creating Bookmark...");
39+
let { bookmark, meta: bookmarkMeta } = specsBuilder.createBookmark(`pubky://${RIO}/pub/pubky.app/posts/0033SREKPC4N0`);
40+
console.log("Bookmark ID:", bookmarkMeta.id);
41+
console.log("Bookmark URL:", bookmarkMeta.url);
42+
console.log("Bookmark Data:", JSON.stringify(bookmark.toJson(), null, 2));
43+
console.log("-".repeat(60));
44+
45+
console.log("👥 Creating Follow...");
46+
let {follow, meta: followMeta} = specsBuilder.createFollow(RIO);
47+
console.log("Follow ID:", followMeta.id);
48+
console.log("Follow URL:", followMeta.url);
49+
console.log("Follow Data:", JSON.stringify(follow.toJson(), null, 2));
50+
console.log("-".repeat(60));
51+
52+
console.log("🏷️ Creating Tag...");
53+
let {tag, meta: tagMeta} = specsBuilder.createTag(`pubky://${OTTO}/pub/pubky.app/profile.json`, "otto");
54+
console.log("Tag ID:", tagMeta.id);
55+
console.log("Tag URL:", tagMeta.url);
56+
console.log("Tag Data:", JSON.stringify(tag.toJson(), null, 2));
57+
console.log("-".repeat(60));
58+
59+
console.log("🔇 Creating Mute...");
60+
let {mute, meta: muteMeta} = specsBuilder.createMute(RIO);
61+
console.log("Mute ID:", muteMeta.id);
62+
console.log("Mute URL:", muteMeta.url);
63+
console.log("Mute Data:", JSON.stringify(mute.toJson(), null, 2));
64+
console.log("-".repeat(60));
65+
66+
console.log("📖 Creating Last Read...");
67+
let {last_read, meta: lastReadMeta} = specsBuilder.createLastRead(RIO);
68+
console.log("LastRead Timestamp:", lastReadMeta.url);
69+
console.log("LastRead Data:", JSON.stringify(last_read.toJson(), null, 2));
70+
console.log("-".repeat(60));
71+
72+
console.log("💾 Creating Blob...");
73+
let { blob, meta: blobMeta } = specsBuilder.createBlob(Array.from({length: 8}, () => Math.floor(Math.random() * 256)));
74+
console.log("Blob ID:", blobMeta.id);
75+
console.log("Blob URL:", blobMeta.url);
76+
console.log("Blob Data:", JSON.stringify(blob.toJson(), null, 2));
77+
console.log("-".repeat(60));
78+
79+
console.log("📄 Creating File...");
80+
let { file, meta: fileMeta } = specsBuilder.createFile("My adventures", blobMeta.url, "application/pdf", 88);
81+
console.log("File ID:", fileMeta.id);
82+
console.log("File URL:", fileMeta.url);
83+
console.log("File Data:", JSON.stringify(file.toJson(), null, 2));
84+
console.log("-".repeat(60));
85+
86+
console.log("📰 Creating Feed...");
87+
let { feed, meta: feedMeta } = specsBuilder.createFeed(["mountain","hike"], "all", "columns", "recent", "image", "nature");
88+
console.log("Feed ID:", feedMeta.id);
89+
console.log("Feed URL:", feedMeta.url);
90+
console.log("Feed Data:", JSON.stringify(feed.toJson(), null, 2));
91+
console.log("=".repeat(60));
92+
console.log("🎉 All Pubky App Specs examples completed successfully!");
93+
console.log("=".repeat(60));

0 commit comments

Comments
 (0)