Skip to content

Commit db20f36

Browse files
authored
Merge pull request #14 from BeastByteAI/js-release
prepared packages for npm release
2 parents 25e7626 + c52047f commit db20f36

30 files changed

Lines changed: 3189 additions & 741 deletions

.github/workflows/npm_publish.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Build and Publish JS Packages
2+
3+
on:
4+
release:
5+
types: [created]
6+
workflow_dispatch:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
if: |
12+
github.event_name == 'workflow_dispatch' ||
13+
(github.event_name == 'release' &&
14+
startsWith(github.ref, 'refs/tags/') &&
15+
contains(github.ref, 'js_v'))
16+
strategy:
17+
matrix:
18+
node-version: ["22", "24"]
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup pnpm
23+
uses: pnpm/action-setup@v4
24+
with:
25+
version: latest
26+
27+
- name: Set up Node ${{ matrix.node-version }}
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: ${{ matrix.node-version }}
31+
cache: pnpm
32+
cache-dependency-path: src/js/pnpm-lock.yaml
33+
34+
- name: Install dependencies
35+
working-directory: src/js
36+
run: pnpm install --frozen-lockfile
37+
38+
- name: Build
39+
working-directory: src/js
40+
run: pnpm build
41+
42+
- name: Run tests
43+
working-directory: src/js
44+
run: pnpm test
45+
46+
publish:
47+
runs-on: ubuntu-latest
48+
needs: [test]
49+
environment: npm
50+
permissions:
51+
id-token: write
52+
if: |
53+
github.event_name == 'workflow_dispatch' ||
54+
(github.event_name == 'release' &&
55+
startsWith(github.ref, 'refs/tags/') &&
56+
contains(github.ref, 'js_v'))
57+
steps:
58+
- uses: actions/checkout@v4
59+
60+
- name: Setup pnpm
61+
uses: pnpm/action-setup@v4
62+
with:
63+
version: latest
64+
65+
- name: Set up Node
66+
uses: actions/setup-node@v4
67+
with:
68+
node-version: "24"
69+
registry-url: https://registry.npmjs.org
70+
71+
- name: Install dependencies
72+
working-directory: src/js
73+
run: pnpm install --frozen-lockfile
74+
75+
- name: Build
76+
working-directory: src/js
77+
run: pnpm build
78+
79+
- name: Publish
80+
working-directory: src/js
81+
run: pnpm -r publish --access public --no-git-checks
82+
env:
83+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-9.52 KB
Binary file not shown.
-5.33 KB
Binary file not shown.
-5.36 KB
Binary file not shown.

src/js/pack.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
set -e
3+
4+
cd "$(dirname "$0")"
5+
6+
pnpm build
7+
8+
mkdir -p local-packages
9+
10+
for pkg in packages/*/; do
11+
(cd "$pkg" && pnpm pack)
12+
mv "$pkg"/*.tgz local-packages/
13+
done

src/js/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"build": "pnpm -r build",
77
"dev": "pnpm -r --parallel run dev",
8-
"test": "pnpm -r test",
8+
"test": "pnpm --filter @fnnx/node --filter @fnnx/web test",
99
"lint": "pnpm -r lint",
1010
"clean": "pnpm -r clean",
1111
"play": "node --loader ts-node/esm pg_run.ts"
@@ -16,11 +16,11 @@
1616
"prettier": "^3.4.2",
1717
"ts-node": "^10.9.2",
1818
"typescript": "^5.7.2",
19-
"vitest": "^1.6.0"
19+
"vitest": "^4.0.18"
2020
},
2121
"dependencies": {
2222
"@fnnx/common": "workspace:*",
23-
"@fnnx/web": "workspace:*",
24-
"@fnnx/node": "workspace:*"
23+
"@fnnx/node": "workspace:*",
24+
"@fnnx/web": "workspace:*"
2525
}
2626
}

src/js/packages/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fnnx/common",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"type": "module",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/js/packages/common/src/handler.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,15 @@ export class LocalHandler {
113113

114114
if (spec.content_type === 'NDJSON') {
115115
if (spec.dtype.startsWith('NDContainer[')) {
116-
throw new Error('NDContainer support is not implemented yet');
116+
if (input instanceof NDContainer) {
117+
preparedInputs[name] = input;
118+
} else {
119+
preparedInputs[name] = new NDContainer(
120+
input,
121+
spec.dtype,
122+
this.dtypesManager
123+
);
124+
}
117125
} else if (spec.dtype.startsWith('Array[')) {
118126
if (!(input instanceof NDArray)) {
119127
throw new Error(`Input ${name} must be an NDArray`);
@@ -128,9 +136,13 @@ export class LocalHandler {
128136
throw new Error(`Invalid NDJSON dtype: ${spec.dtype}. Must be Array[...] or NDContainer[...]`);
129137
}
130138
} else if (spec.content_type === 'JSON') {
131-
throw new Error('JSON input support is not implemented yet');
132-
}
133-
else {
139+
if (this.variant === 'pipeline') {
140+
throw new Error('Pipeline variant does not support JSON inputs');
141+
}
142+
const dtype = spec.dtype;
143+
this.dtypesManager.validateJsonSchema(dtype, input);
144+
preparedInputs[name] = input;
145+
} else {
134146
throw new Error(`Unknown content type: ${spec["content_type"]}`);
135147
}
136148
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { NDArray, ArrayDType, DtypesManager } from "./ndarray";
1+
import { NDArray, ArrayDType, DtypesManager, NDContainer } from "./ndarray";
22
import Registry from "./registry";
33
import { BaseOp } from "./ops/base";
44
import { LocalHandler } from "./handler";
55
import { Inputs, Outputs, DynamicAttributes } from "./handler";
6+
import { applyPatches } from "./jsonpatcher";
67

78
export * as interfaces from './interfaces';
8-
export { NDArray, ArrayDType, DtypesManager };
9+
export { NDArray, ArrayDType, DtypesManager, NDContainer };
910
export { Registry };
1011
export { LocalHandler };
1112
export { BaseOp };
13+
export { applyPatches };
1214

1315
export type { Inputs, Outputs, DynamicAttributes };
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[];
2+
type JsonObject = { [key: string]: JsonValue };
3+
type JsonPatchOp = { op: string; path: string; value?: JsonValue };
4+
type JsonPatch = JsonPatchOp[];
5+
6+
function decodePointerToken(token: string): string {
7+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
8+
}
9+
10+
function splitPointer(path: string): string[] {
11+
if (path === "") {
12+
throw new Error("Empty JSON Pointer path is not supported");
13+
}
14+
if (!path.startsWith("/")) {
15+
throw new Error(
16+
`Only absolute JSON Pointer paths are supported, got: '${path}'`
17+
);
18+
}
19+
const rawTokens = path.slice(1).split("/");
20+
return rawTokens.filter((t) => t !== "").map(decodePointerToken);
21+
}
22+
23+
function parseIndex(token: string, maxLen: number): number {
24+
const idx = parseInt(token, 10);
25+
if (isNaN(idx)) {
26+
throw new Error(`Array index must be an integer, got '${token}'`);
27+
}
28+
if (idx < 0 || idx >= maxLen) {
29+
throw new RangeError(
30+
`Array index ${idx} out of range (len=${maxLen})`
31+
);
32+
}
33+
return idx;
34+
}
35+
36+
function parseIndexForAdd(token: string, maxLen: number): number {
37+
const idx = parseInt(token, 10);
38+
if (isNaN(idx)) {
39+
throw new Error(`Array index must be an integer, got '${token}'`);
40+
}
41+
if (idx < 0 || idx > maxLen) {
42+
throw new RangeError(
43+
`Array index ${idx} out of range for add (len=${maxLen})`
44+
);
45+
}
46+
return idx;
47+
}
48+
49+
function traverseToParent(doc: JsonValue, path: string): [JsonValue, string] {
50+
const tokens = splitPointer(path);
51+
if (tokens.length === 0) {
52+
throw new Error(
53+
`Path '${path}' does not point to a child of the root`
54+
);
55+
}
56+
57+
let parent: JsonValue = doc;
58+
for (let i = 0; i < tokens.length - 1; i++) {
59+
const token = tokens[i];
60+
if (Array.isArray(parent)) {
61+
const idx = parseIndex(token, parent.length);
62+
parent = parent[idx];
63+
} else if (parent !== null && typeof parent === "object") {
64+
if (!(token in parent)) {
65+
throw new Error(
66+
`Path segment '${token}' not found while traversing '${path}'`
67+
);
68+
}
69+
parent = (parent as JsonObject)[token];
70+
} else {
71+
throw new TypeError(
72+
`Cannot traverse into non-container type at segment '${token}'`
73+
);
74+
}
75+
}
76+
return [parent, tokens[tokens.length - 1]];
77+
}
78+
79+
function opAdd(doc: JsonValue, path: string, value: JsonValue): void {
80+
const [parent, token] = traverseToParent(doc, path);
81+
82+
if (Array.isArray(parent)) {
83+
if (token === "-") {
84+
parent.push(value);
85+
return;
86+
}
87+
const idx = parseIndexForAdd(token, parent.length);
88+
parent.splice(idx, 0, value);
89+
return;
90+
}
91+
92+
if (parent !== null && typeof parent === "object") {
93+
(parent as JsonObject)[token] = value;
94+
return;
95+
}
96+
97+
throw new TypeError(
98+
`Cannot apply 'add' at '${path}': parent is not a container`
99+
);
100+
}
101+
102+
function opReplace(doc: JsonValue, path: string, value: JsonValue): void {
103+
const [parent, token] = traverseToParent(doc, path);
104+
105+
if (Array.isArray(parent)) {
106+
const idx = parseIndex(token, parent.length);
107+
parent[idx] = value;
108+
return;
109+
}
110+
111+
if (parent !== null && typeof parent === "object") {
112+
if (!(token in (parent as JsonObject))) {
113+
throw new Error(
114+
`Cannot 'replace' non-existent member '${token}' at '${path}'`
115+
);
116+
}
117+
(parent as JsonObject)[token] = value;
118+
return;
119+
}
120+
121+
throw new TypeError(
122+
`Cannot apply 'replace' at '${path}': parent is not a container`
123+
);
124+
}
125+
126+
function applyPatch(document: JsonValue, patch: JsonPatch): void {
127+
for (const op of patch) {
128+
const opType = op.op;
129+
const path = op.path;
130+
if (opType === undefined || path === undefined) {
131+
throw new Error(`Invalid JSON Patch operation: ${JSON.stringify(op)}`);
132+
}
133+
134+
if (opType === "add") {
135+
opAdd(document, path, op.value as JsonValue);
136+
} else if (opType === "replace") {
137+
opReplace(document, path, op.value as JsonValue);
138+
} else {
139+
throw new Error(
140+
`Unsupported JSON Patch op: '${opType}' (only 'add' and 'replace' are allowed)`
141+
);
142+
}
143+
}
144+
}
145+
146+
export function applyPatches(
147+
document: JsonObject,
148+
patchDocuments: JsonPatch[]
149+
): JsonObject {
150+
const result: JsonObject = JSON.parse(JSON.stringify(document));
151+
for (const patch of patchDocuments) {
152+
applyPatch(result, patch);
153+
}
154+
return result;
155+
}

0 commit comments

Comments
 (0)