@buildwithharbor/pier 0.1.5
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/dist/index.js +71874 -0
- package/dist/local-worker-runtime-child.mjs +377 -0
- package/package.json +44 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { watchFile, unwatchFile } from "node:fs";
|
|
2
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import Cloudflare from "cloudflare";
|
|
7
|
+
import { Miniflare } from "miniflare";
|
|
8
|
+
import {
|
|
9
|
+
maybeStartOrUpdateRemoteProxySession,
|
|
10
|
+
unstable_convertConfigBindingsToStartWorkerBindings,
|
|
11
|
+
unstable_getMiniflareWorkerOptions,
|
|
12
|
+
} from "wrangler";
|
|
13
|
+
|
|
14
|
+
const runtimeConfig = await readRuntimeConfig();
|
|
15
|
+
const adapterConfig = await readJson(runtimeConfig.adapterConfigPath);
|
|
16
|
+
const port = runtimeConfig.port ?? 8787;
|
|
17
|
+
const remoteBindings = convertRemoteBindings(adapterConfig);
|
|
18
|
+
|
|
19
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Worker dev port must be a positive integer. Received: ${String(port)}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cloudflareCredentials =
|
|
26
|
+
remoteBindings.length > 0
|
|
27
|
+
? {
|
|
28
|
+
accountId: requiredEnv("CLOUDFLARE_ACCOUNT_ID"),
|
|
29
|
+
apiToken: requiredEnv("CLOUDFLARE_API_TOKEN"),
|
|
30
|
+
}
|
|
31
|
+
: undefined;
|
|
32
|
+
|
|
33
|
+
if (cloudflareCredentials) {
|
|
34
|
+
await ensureRemoteResources({ ...cloudflareCredentials, adapterConfig });
|
|
35
|
+
}
|
|
36
|
+
const proxySession =
|
|
37
|
+
remoteBindings.length > 0 && cloudflareCredentials
|
|
38
|
+
? await maybeStartOrUpdateRemoteProxySession(
|
|
39
|
+
{
|
|
40
|
+
account_id: cloudflareCredentials.accountId,
|
|
41
|
+
bindings: remoteBindings,
|
|
42
|
+
name: runtimeConfig.workerName,
|
|
43
|
+
},
|
|
44
|
+
null
|
|
45
|
+
)
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
48
|
+
if (remoteBindings.length > 0 && !proxySession) {
|
|
49
|
+
throw new Error("Pier could not start the Cloudflare remote binding proxy.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const remoteProxyConnectionString =
|
|
53
|
+
proxySession?.session.remoteProxyConnectionString;
|
|
54
|
+
const miniflare = new Miniflare(miniflareOptions(remoteProxyConnectionString));
|
|
55
|
+
await miniflare.ready;
|
|
56
|
+
|
|
57
|
+
const server = createServer(async (request, response) => {
|
|
58
|
+
try {
|
|
59
|
+
const miniflareResponse = await dispatchToMiniflare(miniflare, request);
|
|
60
|
+
response.writeHead(
|
|
61
|
+
miniflareResponse.status,
|
|
62
|
+
Object.fromEntries(miniflareResponse.headers.entries())
|
|
63
|
+
);
|
|
64
|
+
response.end(Buffer.from(await miniflareResponse.arrayBuffer()));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
67
|
+
response.end(`${errorMessage(error)}\n`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
72
|
+
server.once("error", rejectListen);
|
|
73
|
+
server.listen(port, "127.0.0.1", () => {
|
|
74
|
+
server.off("error", rejectListen);
|
|
75
|
+
resolveListen();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
console.log(`Pier Worker runtime: http://127.0.0.1:${port}`);
|
|
80
|
+
console.log(`Project: ${runtimeConfig.projectName}`);
|
|
81
|
+
console.log(`App: ${runtimeConfig.appName}`);
|
|
82
|
+
console.log(`Worker: ${runtimeConfig.workerName}`);
|
|
83
|
+
printRemoteBindingSummary(adapterConfig);
|
|
84
|
+
for (const serviceWorker of runtimeConfig.serviceWorkers) {
|
|
85
|
+
console.log(
|
|
86
|
+
`Local service binding worker: ${serviceWorker.appName} -> ${serviceWorker.workerName}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
console.log(`Watching Worker bundle: ${runtimeConfig.workerPath}`);
|
|
90
|
+
|
|
91
|
+
watchFile(runtimeConfig.workerPath, { interval: 250 }, () => {
|
|
92
|
+
void reloadMiniflare();
|
|
93
|
+
});
|
|
94
|
+
for (const serviceWorker of runtimeConfig.serviceWorkers) {
|
|
95
|
+
watchFile(serviceWorker.workerPath, { interval: 250 }, () => {
|
|
96
|
+
void reloadMiniflare();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let disposed = false;
|
|
101
|
+
const dispose = async () => {
|
|
102
|
+
if (disposed) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
disposed = true;
|
|
106
|
+
unwatchFile(runtimeConfig.workerPath);
|
|
107
|
+
for (const serviceWorker of runtimeConfig.serviceWorkers) {
|
|
108
|
+
unwatchFile(serviceWorker.workerPath);
|
|
109
|
+
}
|
|
110
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
111
|
+
await miniflare.dispose();
|
|
112
|
+
await proxySession?.session.dispose();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
process.once("SIGINT", () => {
|
|
116
|
+
dispose()
|
|
117
|
+
.catch((error) => {
|
|
118
|
+
console.error(errorMessage(error));
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
})
|
|
121
|
+
.finally(() => process.exit(process.exitCode ?? 0));
|
|
122
|
+
});
|
|
123
|
+
process.once("SIGTERM", () => {
|
|
124
|
+
dispose()
|
|
125
|
+
.catch((error) => {
|
|
126
|
+
console.error(errorMessage(error));
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
})
|
|
129
|
+
.finally(() => process.exit(process.exitCode ?? 0));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await new Promise(() => {});
|
|
133
|
+
|
|
134
|
+
async function dispatchToMiniflare(miniflareInstance, request) {
|
|
135
|
+
const url = new URL(
|
|
136
|
+
request.url ?? "/",
|
|
137
|
+
`http://${request.headers.host ?? `127.0.0.1:${port}`}`
|
|
138
|
+
);
|
|
139
|
+
const init = {
|
|
140
|
+
headers: request.headers,
|
|
141
|
+
method: request.method,
|
|
142
|
+
redirect: "manual",
|
|
143
|
+
};
|
|
144
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
145
|
+
init.body = await readRequestBody(request);
|
|
146
|
+
}
|
|
147
|
+
return miniflareInstance.dispatchFetch(url, init);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function reloadMiniflare() {
|
|
151
|
+
const startedAt = Date.now();
|
|
152
|
+
try {
|
|
153
|
+
await miniflare.setOptions(miniflareOptions(remoteProxyConnectionString));
|
|
154
|
+
logRuntime("pier.worker.reloaded", "Worker runtime reloaded", {
|
|
155
|
+
durationMs: Date.now() - startedAt,
|
|
156
|
+
workerPath: runtimeConfig.workerPath,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logRuntime(
|
|
160
|
+
"pier.worker.reload_failed",
|
|
161
|
+
"Worker runtime reload failed",
|
|
162
|
+
{
|
|
163
|
+
error: errorMessage(error),
|
|
164
|
+
workerPath: runtimeConfig.workerPath,
|
|
165
|
+
},
|
|
166
|
+
"error"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function logRuntime(name, message, metadata = {}, level = "info") {
|
|
172
|
+
const event = {
|
|
173
|
+
app: runtimeConfig.appName,
|
|
174
|
+
environment: "dev",
|
|
175
|
+
level,
|
|
176
|
+
message,
|
|
177
|
+
metadata,
|
|
178
|
+
name,
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
worker: runtimeConfig.workerName,
|
|
181
|
+
};
|
|
182
|
+
const line = JSON.stringify(event);
|
|
183
|
+
if (level === "error") {
|
|
184
|
+
console.error(line);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
console.log(line);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function miniflareOptions(remoteProxyConnectionString) {
|
|
191
|
+
const converted = unstable_getMiniflareWorkerOptions(
|
|
192
|
+
runtimeConfig.adapterConfigPath,
|
|
193
|
+
undefined,
|
|
194
|
+
remoteProxyConnectionString ? { remoteProxyConnectionString } : undefined
|
|
195
|
+
);
|
|
196
|
+
const workerOptions = {
|
|
197
|
+
...converted.workerOptions,
|
|
198
|
+
modules: true,
|
|
199
|
+
name: runtimeConfig.workerName,
|
|
200
|
+
scriptPath: runtimeConfig.workerPath,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
defaultPersistRoot: runtimeConfig.persistRoot,
|
|
205
|
+
workers: [
|
|
206
|
+
workerOptions,
|
|
207
|
+
...runtimeConfig.serviceWorkers.flatMap((serviceWorker) =>
|
|
208
|
+
serviceWorkerOptions(serviceWorker, remoteProxyConnectionString)
|
|
209
|
+
),
|
|
210
|
+
...converted.externalWorkers,
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function serviceWorkerOptions(serviceWorker, remoteProxyConnectionString) {
|
|
216
|
+
const converted = unstable_getMiniflareWorkerOptions(
|
|
217
|
+
serviceWorker.adapterConfigPath,
|
|
218
|
+
undefined,
|
|
219
|
+
remoteProxyConnectionString ? { remoteProxyConnectionString } : undefined
|
|
220
|
+
);
|
|
221
|
+
return [
|
|
222
|
+
{
|
|
223
|
+
...converted.workerOptions,
|
|
224
|
+
modules: true,
|
|
225
|
+
name: serviceWorker.workerName,
|
|
226
|
+
scriptPath: serviceWorker.workerPath,
|
|
227
|
+
},
|
|
228
|
+
...converted.externalWorkers,
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function convertRemoteBindings(config) {
|
|
233
|
+
const bindings = unstable_convertConfigBindingsToStartWorkerBindings({
|
|
234
|
+
d1_databases: remoteOnly(config.d1_databases),
|
|
235
|
+
kv_namespaces: remoteOnly(config.kv_namespaces),
|
|
236
|
+
r2_buckets: remoteOnly(config.r2_buckets),
|
|
237
|
+
});
|
|
238
|
+
if (!bindings || bindings.length === 0) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
return bindings;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function ensureRemoteResources(input) {
|
|
245
|
+
const client = new Cloudflare({ apiToken: input.apiToken });
|
|
246
|
+
await ensureRemoteR2Buckets({ ...input, client });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function ensureRemoteR2Buckets(input) {
|
|
250
|
+
const buckets = remoteOnly(input.adapterConfig.r2_buckets);
|
|
251
|
+
if (buckets.length === 0) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await mkdir(resolve(".pier", "state"), { recursive: true });
|
|
256
|
+
const existing = await input.client.r2.buckets.list({
|
|
257
|
+
account_id: input.accountId,
|
|
258
|
+
});
|
|
259
|
+
const existingNames = new Set(
|
|
260
|
+
(existing.buckets ?? []).map((bucket) => bucket.name)
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
for (const bucket of buckets) {
|
|
264
|
+
if (existingNames.has(bucket.bucket_name)) {
|
|
265
|
+
console.log(`Remote R2 bucket exists: ${bucket.bucket_name}`);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let created = true;
|
|
270
|
+
await input.client.r2.buckets
|
|
271
|
+
.create({
|
|
272
|
+
account_id: input.accountId,
|
|
273
|
+
name: bucket.bucket_name,
|
|
274
|
+
})
|
|
275
|
+
.catch((error) => {
|
|
276
|
+
if (isAlreadyOwnedBucketError(error)) {
|
|
277
|
+
created = false;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
});
|
|
282
|
+
console.log(
|
|
283
|
+
`${created ? "Remote R2 bucket created" : "Remote R2 bucket exists"}: ${bucket.bucket_name}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function printRemoteBindingSummary(config) {
|
|
289
|
+
for (const bucket of remoteOnly(config.r2_buckets)) {
|
|
290
|
+
console.log(
|
|
291
|
+
`Remote binding: ${bucket.binding} -> R2 bucket ${bucket.bucket_name}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
for (const namespace of remoteOnly(config.kv_namespaces)) {
|
|
295
|
+
console.log(
|
|
296
|
+
`Remote binding: ${namespace.binding} -> KV namespace ${namespace.id}`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
for (const database of remoteOnly(config.d1_databases)) {
|
|
300
|
+
console.log(
|
|
301
|
+
`Remote binding: ${database.binding} -> D1 database ${database.database_name}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function remoteOnly(values) {
|
|
307
|
+
return Array.isArray(values)
|
|
308
|
+
? values.filter((value) => value?.remote === true)
|
|
309
|
+
: [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function readRuntimeConfig() {
|
|
313
|
+
const configPath = process.argv[2];
|
|
314
|
+
if (!configPath) {
|
|
315
|
+
throw new Error("Missing runtime config path.");
|
|
316
|
+
}
|
|
317
|
+
const config = await readJson(resolve(configPath));
|
|
318
|
+
return {
|
|
319
|
+
appName: stringValue(config.appName, "worker"),
|
|
320
|
+
persistRoot: stringValue(
|
|
321
|
+
config.persistRoot,
|
|
322
|
+
resolve(".pier/state/miniflare/worker")
|
|
323
|
+
),
|
|
324
|
+
port: typeof config.port === "number" ? config.port : undefined,
|
|
325
|
+
projectName: stringValue(config.projectName, "pier"),
|
|
326
|
+
serviceWorkers: Array.isArray(config.serviceWorkers)
|
|
327
|
+
? config.serviceWorkers.map((serviceWorker) => ({
|
|
328
|
+
appName: stringValue(serviceWorker.appName, "service"),
|
|
329
|
+
workerName: stringValue(serviceWorker.workerName, "pier-service"),
|
|
330
|
+
workerPath: stringValue(serviceWorker.workerPath, ""),
|
|
331
|
+
adapterConfigPath: stringValue(serviceWorker.adapterConfigPath, ""),
|
|
332
|
+
}))
|
|
333
|
+
: [],
|
|
334
|
+
workerName: stringValue(config.workerName, "pier-worker"),
|
|
335
|
+
workerPath: stringValue(config.workerPath, ""),
|
|
336
|
+
adapterConfigPath: stringValue(config.adapterConfigPath, ""),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function readJson(path) {
|
|
341
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function readRequestBody(request) {
|
|
345
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
346
|
+
const chunks = [];
|
|
347
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
348
|
+
request.on("end", () => resolveBody(Buffer.concat(chunks)));
|
|
349
|
+
request.on("error", rejectBody);
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function requiredEnv(name) {
|
|
354
|
+
const value = process.env[name];
|
|
355
|
+
if (!value) {
|
|
356
|
+
throw new Error(`${name} is required for Pier remote Worker dev.`);
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function errorMessage(error) {
|
|
362
|
+
return error instanceof Error ? error.message : String(error);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isAlreadyOwnedBucketError(error) {
|
|
366
|
+
return (
|
|
367
|
+
typeof error === "object" &&
|
|
368
|
+
error !== null &&
|
|
369
|
+
"status" in error &&
|
|
370
|
+
error.status === 409 &&
|
|
371
|
+
errorMessage(error).includes("you own it")
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function stringValue(value, fallback) {
|
|
376
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
377
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@buildwithharbor/pier",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"bin": {
|
|
5
|
+
"pier": "dist/index.js"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build:npm": "bun build src/index.ts --target bun --outfile dist/index.js && cp src/local-worker-runtime-child.mjs dist/local-worker-runtime-child.mjs",
|
|
19
|
+
"check-types": "tsgo -p tsconfig.json"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@pier/app-build": "0.1.15",
|
|
23
|
+
"@pier/backend": "0.1.15",
|
|
24
|
+
"@pier/cloudflare": "0.1.18",
|
|
25
|
+
"@pier/core": "0.1.16",
|
|
26
|
+
"@pier/credentials": "0.1.15",
|
|
27
|
+
"@pier/db": "0.1.15",
|
|
28
|
+
"@pier/log": "0.1.15",
|
|
29
|
+
"@pier/sync": "0.1.21",
|
|
30
|
+
"@pier/worker-bundler": "0.1.15",
|
|
31
|
+
"@repo/api-contract": "workspace:*",
|
|
32
|
+
"@repo/api-sync-contract": "workspace:*",
|
|
33
|
+
"@repo/api-sync-schema": "workspace:*",
|
|
34
|
+
"@repo/local-daemon-contract": "workspace:*",
|
|
35
|
+
"@rocicorp/zero": "^1.6.1",
|
|
36
|
+
"clipanion": "4.0.0-rc.4",
|
|
37
|
+
"cloudflare": "6.4.0",
|
|
38
|
+
"miniflare": "4.20260603.0",
|
|
39
|
+
"postgres": "^3.4.9",
|
|
40
|
+
"wrangler": "4.98.0",
|
|
41
|
+
"@types/bun": "^1.3.4",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
}
|
|
44
|
+
}
|