@primate/go 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/private/Default.d.ts +1 -1
- package/lib/private/Default.js +57 -89
- package/lib/private/to-request.d.ts +2 -2
- package/lib/private/to-request.js +10 -13
- package/lib/private/to-response.d.ts +1 -6
- package/lib/private/to-response.js +25 -17
- package/lib/private/wrapper.d.ts +12 -0
- package/lib/private/wrapper.js +52 -0
- package/lib/public/wrapper.d.ts +2 -0
- package/lib/public/wrapper.js +2 -0
- package/package.json +5 -5
package/lib/private/Default.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import Runtime from "#Runtime";
|
|
|
2
2
|
import type BuildApp from "@primate/core/BuildApp";
|
|
3
3
|
import type NextBuild from "@primate/core/NextBuild";
|
|
4
4
|
export default class Default extends Runtime {
|
|
5
|
-
build(app: BuildApp, next: NextBuild):
|
|
5
|
+
build(app: BuildApp, next: NextBuild): Promise<BuildApp>;
|
|
6
6
|
}
|
|
7
7
|
//# sourceMappingURL=Default.d.ts.map
|
package/lib/private/Default.js
CHANGED
|
@@ -1,113 +1,81 @@
|
|
|
1
1
|
import Runtime from "#Runtime";
|
|
2
2
|
import AppError from "@primate/core/AppError";
|
|
3
|
+
import TAG from "@primate/core/backend/TAG";
|
|
4
|
+
import fail from "@primate/core/fail";
|
|
3
5
|
import log from "@primate/core/log";
|
|
4
|
-
import wrap from "@primate/core/route/wrap";
|
|
5
6
|
import assert from "@rcompat/assert";
|
|
6
7
|
import user from "@rcompat/env/user";
|
|
7
|
-
import runtime from "@rcompat/runtime";
|
|
8
8
|
import execute from "@rcompat/stdio/execute";
|
|
9
9
|
import which from "@rcompat/stdio/which";
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const COMMAND = await which("go");
|
|
11
|
+
const ENV = {
|
|
12
12
|
GOARCH: "wasm",
|
|
13
|
-
GOCACHE: (await execute(`${
|
|
13
|
+
GOCACHE: (await execute(`${COMMAND} env GOCACHE`, {})).replaceAll("\n", ""),
|
|
14
14
|
GOOS: "js",
|
|
15
15
|
HOME: user.HOME,
|
|
16
16
|
};
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import
|
|
23
|
-
import route from "primate/route";
|
|
24
|
-
|
|
25
|
-
globalThis.PRMT_SESSION = {
|
|
26
|
-
get exists() {
|
|
27
|
-
return session.exists;
|
|
28
|
-
},
|
|
29
|
-
get id() {
|
|
30
|
-
return session.id;
|
|
31
|
-
},
|
|
32
|
-
get data() {
|
|
33
|
-
return JSON.stringify(session.try());
|
|
34
|
-
},
|
|
35
|
-
create(data) {
|
|
36
|
-
session.create(JSON.parse(data));
|
|
37
|
-
},
|
|
38
|
-
get() {
|
|
39
|
-
return JSON.stringify(session.get());
|
|
40
|
-
},
|
|
41
|
-
try() {
|
|
42
|
-
return JSON.stringify(session.try());
|
|
43
|
-
},
|
|
44
|
-
set(data) {
|
|
45
|
-
session.set(JSON.parse(data));
|
|
46
|
-
},
|
|
47
|
-
destroy() {
|
|
48
|
-
session.destroy();
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
${runtime === "bun"
|
|
53
|
-
? `import route_path from "${path}" with { type: "file" };
|
|
54
|
-
const go_route = await Bun.file(route_path).arrayBuffer();`
|
|
55
|
-
:
|
|
56
|
-
`import FileRef from "primate/runtime/FileRef";
|
|
57
|
-
|
|
58
|
-
const buffer = await FileRef.arrayBuffer(import.meta.url+"/../${path}");
|
|
59
|
-
const go_route = new Uint8Array(buffer);`}
|
|
60
|
-
|
|
61
|
-
env();
|
|
62
|
-
|
|
63
|
-
// Run Go once to register routes and get available verbs
|
|
64
|
-
const go = new globalThis.Go();
|
|
65
|
-
const result = await WebAssembly.instantiate(go_route, go.importObject);
|
|
66
|
-
go.run(result.instance);
|
|
67
|
-
|
|
68
|
-
const verbs = globalThis.__primate_verbs || [];
|
|
69
|
-
|
|
70
|
-
for (const verb of verbs) {
|
|
71
|
-
route[verb.toLowerCase()](async request => {
|
|
72
|
-
const requested = await toRequest(request);
|
|
73
|
-
const freshGo = new globalThis.Go();
|
|
74
|
-
return WebAssembly.instantiate(go_route, {...freshGo.importObject}).then(freshResult => {
|
|
75
|
-
freshGo.run(freshResult.instance);
|
|
76
|
-
return to_response(globalThis.__primate_handle(verb, requested));
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
`;
|
|
81
|
-
const go_wrapper = (code) => {
|
|
82
|
-
if (!code.includes("github.com/primate-run/go/route")) {
|
|
83
|
-
console.warn("Go file does not import \"github.com/primate-run/go/route\" - skipping route registration");
|
|
17
|
+
const REPO = "github.com/primate-run/go";
|
|
18
|
+
const [MAJOR, MINOR] = TAG.split(".").map(Number);
|
|
19
|
+
const run = (wasm, go) => `${COMMAND} build -o ${wasm.name} ${go.name}`;
|
|
20
|
+
function postlude(code, route_id) {
|
|
21
|
+
if (!code.includes(`${REPO}/route`)) {
|
|
22
|
+
log.warn("Go file does not import {0} - skipping route registration", REPO);
|
|
84
23
|
return code;
|
|
85
24
|
}
|
|
86
25
|
return `${code}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
26
|
+
func main() {
|
|
27
|
+
route.Commit("${route_id}")
|
|
28
|
+
select{}
|
|
29
|
+
}`;
|
|
30
|
+
}
|
|
31
|
+
async function check_version() {
|
|
32
|
+
try {
|
|
33
|
+
const version = await execute(`go list -m -f '{{.Version}}' ${REPO}`);
|
|
34
|
+
const trimmed = version.trim();
|
|
35
|
+
const version_match = trimmed.match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
36
|
+
if (!version_match)
|
|
37
|
+
throw fail("invalid version format: {0}", trimmed);
|
|
38
|
+
const [, major, minor] = version_match.map(Number);
|
|
39
|
+
if (major !== MAJOR || minor !== MINOR) {
|
|
40
|
+
throw fail("installed version {0} not in range {1}", trimmed, TAG);
|
|
41
|
+
}
|
|
42
|
+
log.info("using {0} package v{1}.{2}.x", REPO, major, minor);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof AppError)
|
|
46
|
+
throw error;
|
|
47
|
+
throw fail("{0} dependency not found - run 'go get {0}@v{1}.0'", REPO, TAG);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
93
50
|
export default class Default extends Runtime {
|
|
94
|
-
build(app, next) {
|
|
95
|
-
|
|
51
|
+
async build(app, next) {
|
|
52
|
+
await check_version();
|
|
53
|
+
app.bind(this.fileExtension, async (route, { context }) => {
|
|
96
54
|
assert(context === "routes", "go: only route files are supported");
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
await
|
|
55
|
+
const relative = route.debase(app.path.routes);
|
|
56
|
+
const route_id = relative.path.slice(1, -relative.extension.length);
|
|
57
|
+
// create a temporary .go file in the build directory for compilation
|
|
58
|
+
// this is necessary because `go build` requires an actual file on disk
|
|
59
|
+
const build_go_file = app.runpath("wasm", `${route_id}.go`);
|
|
60
|
+
await build_go_file.directory.create({ recursive: true });
|
|
61
|
+
await build_go_file.write(postlude(await route.text(), route_id));
|
|
62
|
+
const wasm = app.runpath("wasm", `${route_id}.wasm`);
|
|
103
63
|
try {
|
|
104
64
|
log.info("compiling {0} to WebAssembly", route);
|
|
105
|
-
|
|
106
|
-
|
|
65
|
+
await execute(run(wasm, build_go_file), {
|
|
66
|
+
cwd: build_go_file.directory.path,
|
|
67
|
+
env: ENV,
|
|
68
|
+
});
|
|
107
69
|
}
|
|
108
70
|
catch (error) {
|
|
109
|
-
throw
|
|
71
|
+
throw fail("error in module {0}\n{1}", route, error);
|
|
110
72
|
}
|
|
73
|
+
await build_go_file.remove();
|
|
74
|
+
return `
|
|
75
|
+
import wrapper from "@primate/go/wrapper";
|
|
76
|
+
import bytes from "app:wasm/${route_id}.wasm" with { type: "bytes" };
|
|
77
|
+
await wrapper(bytes, ${JSON.stringify(route_id)});
|
|
78
|
+
`;
|
|
111
79
|
});
|
|
112
80
|
return next(app);
|
|
113
81
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type RequestFacade from "@primate/core/request/RequestFacade";
|
|
2
2
|
export default function toRequest(request: RequestFacade): Promise<{
|
|
3
3
|
body: {
|
|
4
|
-
|
|
4
|
+
formSync: () => string;
|
|
5
5
|
filesSync: () => {
|
|
6
6
|
bytes: Uint8Array;
|
|
7
7
|
field: string;
|
|
@@ -9,7 +9,7 @@ export default function toRequest(request: RequestFacade): Promise<{
|
|
|
9
9
|
size: number;
|
|
10
10
|
type: string;
|
|
11
11
|
}[];
|
|
12
|
-
type: "
|
|
12
|
+
type: "form";
|
|
13
13
|
} | {
|
|
14
14
|
textSync: () => string;
|
|
15
15
|
type: "text";
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
async function
|
|
2
|
-
const fields = body.fields();
|
|
1
|
+
async function bridge_form(body) {
|
|
3
2
|
const meta = Object.create(null);
|
|
4
3
|
const files = [];
|
|
5
4
|
const pending = [];
|
|
6
|
-
for (const [k, v] of Object.entries(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
// v is File
|
|
5
|
+
for (const [k, v] of Object.entries(body.form())) {
|
|
6
|
+
meta[k] = v;
|
|
7
|
+
}
|
|
8
|
+
for (const [k, v] of Object.entries(body.files())) {
|
|
12
9
|
const name = v.name;
|
|
13
10
|
const type = v.type;
|
|
14
11
|
const size = v.size;
|
|
15
12
|
meta[k] = { name, size, type };
|
|
16
|
-
//
|
|
13
|
+
// precompute bytes so Go can call a SYNC getter
|
|
17
14
|
pending.push(v.arrayBuffer().then(buffer => {
|
|
18
15
|
files.push({
|
|
19
16
|
bytes: new Uint8Array(buffer),
|
|
@@ -27,9 +24,9 @@ async function bridgeFields(body) {
|
|
|
27
24
|
await Promise.all(pending);
|
|
28
25
|
const jsonStr = JSON.stringify(meta);
|
|
29
26
|
return {
|
|
30
|
-
|
|
27
|
+
formSync: () => jsonStr,
|
|
31
28
|
filesSync: () => files,
|
|
32
|
-
type: "
|
|
29
|
+
type: "form",
|
|
33
30
|
};
|
|
34
31
|
}
|
|
35
32
|
async function bridgeBody(body) {
|
|
@@ -44,8 +41,8 @@ async function bridgeBody(body) {
|
|
|
44
41
|
const jsonStr = JSON.stringify(val);
|
|
45
42
|
return { jsonSync: () => jsonStr, type };
|
|
46
43
|
}
|
|
47
|
-
case "
|
|
48
|
-
return await
|
|
44
|
+
case "form": {
|
|
45
|
+
return await bridge_form(body);
|
|
49
46
|
}
|
|
50
47
|
case "binary": {
|
|
51
48
|
const blob = body.binary();
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type ResponseLike from "@primate/core/response/ResponseLike";
|
|
2
|
-
|
|
3
|
-
type Handler = "error" | "redirect" | "view";
|
|
4
|
-
type ResponseType = (() => {
|
|
5
|
-
handler: Handler;
|
|
6
|
-
} & Dict) | string;
|
|
7
|
-
declare const _default: (response: ResponseType) => ResponseLike;
|
|
2
|
+
declare const _default: (response: any) => ResponseLike;
|
|
8
3
|
export default _default;
|
|
9
4
|
//# sourceMappingURL=to-response.d.ts.map
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import error from "@primate/core/response/error";
|
|
2
2
|
import redirect from "@primate/core/response/redirect";
|
|
3
3
|
import view from "@primate/core/response/view";
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
if (handler === "view") {
|
|
7
|
-
const { component, options, props } = response;
|
|
8
|
-
return view(component, parse(props), parse(options));
|
|
9
|
-
}
|
|
10
|
-
if (handler === "redirect") {
|
|
11
|
-
const { location, status } = response;
|
|
12
|
-
return redirect(location, status === null ? undefined : status);
|
|
13
|
-
}
|
|
14
|
-
const { options } = response;
|
|
15
|
-
return error(parse(options));
|
|
16
|
-
};
|
|
4
|
+
const handlers = { error, redirect, view };
|
|
5
|
+
const is_handler = (handler) => typeof handler === "string" && Object.keys(handlers).includes(handler);
|
|
17
6
|
export default (response) => {
|
|
18
|
-
if (typeof response === "
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
if (typeof response === "string") {
|
|
8
|
+
try {
|
|
9
|
+
response = JSON.parse(response);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return response;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (response && typeof response === "object") {
|
|
16
|
+
const handler = response.handler;
|
|
17
|
+
if (is_handler(handler)) {
|
|
18
|
+
if (handler === "view") {
|
|
19
|
+
const { component, props, options } = response;
|
|
20
|
+
return view(component, props ? JSON.parse(props) : {}, options ? JSON.parse(options) : {});
|
|
21
|
+
}
|
|
22
|
+
if (handler === "redirect") {
|
|
23
|
+
const { location, status } = response;
|
|
24
|
+
return redirect(location, status);
|
|
25
|
+
}
|
|
26
|
+
const { options } = response;
|
|
27
|
+
return error(options ? JSON.parse(options) : {});
|
|
28
|
+
}
|
|
21
29
|
}
|
|
22
|
-
return
|
|
30
|
+
return response;
|
|
23
31
|
};
|
|
24
32
|
//# sourceMappingURL=to-response.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
var Go: {
|
|
3
|
+
new (): {
|
|
4
|
+
importObject: WebAssembly.Imports;
|
|
5
|
+
run(instance: WebAssembly.Instance): Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
var PRMT_SESSION: any;
|
|
9
|
+
var __primate_go_initialized: Set<string> | undefined;
|
|
10
|
+
}
|
|
11
|
+
export default function wrapper(bytes: Uint8Array, routeId: string): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=wrapper.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import env from "@primate/go/env";
|
|
2
|
+
import toRequest from "@primate/go/to-request";
|
|
3
|
+
import to_response from "@primate/go/to-response";
|
|
4
|
+
import session from "primate/config/session";
|
|
5
|
+
import route from "primate/route";
|
|
6
|
+
export default async function wrapper(bytes, routeId) {
|
|
7
|
+
if (!globalThis.__primate_go_initialized) {
|
|
8
|
+
globalThis.__primate_go_initialized = new Set();
|
|
9
|
+
}
|
|
10
|
+
globalThis.PRMT_SESSION = {
|
|
11
|
+
get exists() { return session().exists; },
|
|
12
|
+
get id() { return session().id; },
|
|
13
|
+
get data() { return JSON.stringify(session().try()); },
|
|
14
|
+
create(data) { session().create(JSON.parse(data)); },
|
|
15
|
+
get() { return JSON.stringify(session().get()); },
|
|
16
|
+
try() { return JSON.stringify(session().try()); },
|
|
17
|
+
set(data) { session().set(JSON.parse(data)); },
|
|
18
|
+
destroy() { session().destroy(); },
|
|
19
|
+
};
|
|
20
|
+
env();
|
|
21
|
+
const safe_route_id = routeId.replace(/\//g, "_");
|
|
22
|
+
const registry_name = `__primate_go_registry_${safe_route_id}`;
|
|
23
|
+
const call_go = `__primate_call_go_${safe_route_id}`;
|
|
24
|
+
delete globalThis[registry_name];
|
|
25
|
+
delete globalThis[call_go];
|
|
26
|
+
await new Promise((resolve) => {
|
|
27
|
+
globalThis[`__primate_go_ready_${safe_route_id}`] = () => {
|
|
28
|
+
resolve();
|
|
29
|
+
};
|
|
30
|
+
const go = new globalThis.Go();
|
|
31
|
+
WebAssembly.instantiate(bytes, go.importObject).then(result => {
|
|
32
|
+
go.run(result.instance).catch(err => {
|
|
33
|
+
console.error("Go runtime error:", routeId, err);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
const registry_fn = globalThis[registry_name];
|
|
38
|
+
const registry = registry_fn();
|
|
39
|
+
const verbs = registry?.length
|
|
40
|
+
? Array.from({ length: registry.length }, (_, i) => registry[i])
|
|
41
|
+
: [];
|
|
42
|
+
for (const verb of verbs) {
|
|
43
|
+
route[verb.toLowerCase()](async (request) => {
|
|
44
|
+
const requested = await toRequest(request);
|
|
45
|
+
const callFn = globalThis[call_go];
|
|
46
|
+
const response = callFn(verb, requested);
|
|
47
|
+
return to_response(response);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
globalThis.__primate_go_initialized.add(routeId);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=wrapper.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primate/go",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Primate Go backend",
|
|
5
5
|
"homepage": "https://primate.run/docs/backend/go",
|
|
6
6
|
"bugs": "https://github.com/primate-run/primate/issues",
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@rcompat/assert": "^0.3.1",
|
|
20
20
|
"@rcompat/cli": "^0.11.3",
|
|
21
|
-
"@rcompat/env": "^0.9.
|
|
22
|
-
"@rcompat/fs": "^0.
|
|
21
|
+
"@rcompat/env": "^0.9.2",
|
|
22
|
+
"@rcompat/fs": "^0.22.3",
|
|
23
23
|
"@rcompat/runtime": "^0.6.0",
|
|
24
24
|
"@rcompat/stdio": "^0.12.2",
|
|
25
25
|
"@rcompat/string": "^0.10.0",
|
|
26
|
-
"@primate/core": "^0.
|
|
26
|
+
"@primate/core": "^0.4.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"primate": "^0.
|
|
29
|
+
"primate": "^0.35.0"
|
|
30
30
|
},
|
|
31
31
|
"type": "module",
|
|
32
32
|
"imports": {
|