rwsdk 0.1.19 → 0.1.20

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.
@@ -1,6 +1,6 @@
1
1
  import { DODialect } from "kysely-do";
2
2
  import { DurableObject } from "cloudflare:workers";
3
- import { Kysely } from "kysely";
3
+ import { Kysely, ParseJSONResultsPlugin, } from "kysely";
4
4
  import { createMigrator } from "./index.js";
5
5
  import debug from "../debug.js";
6
6
  const log = debug("sdk:do-db");
@@ -13,6 +13,7 @@ export class SqliteDurableObject extends DurableObject {
13
13
  this.migrationTableName = migrationTableName;
14
14
  this.kysely = new Kysely({
15
15
  dialect: new DODialect({ ctx }),
16
+ plugins: [new ParseJSONResultsPlugin()],
16
17
  });
17
18
  }
18
19
  async initialize() {
@@ -22,7 +23,13 @@ export class SqliteDurableObject extends DurableObject {
22
23
  }
23
24
  log("Initializing Durable Object database");
24
25
  const migrator = createMigrator(this.kysely, this.migrations, this.migrationTableName);
25
- await migrator.migrateToLatest();
26
+ const result = await migrator.migrateToLatest();
27
+ if (result.error) {
28
+ console.log("rwsdk/db: Migrations failed, rolling back and throwing with the migration error: %O", result.results);
29
+ await migrator.migrateDown();
30
+ throw result.error;
31
+ }
32
+ log("Migrations results", result.results);
26
33
  this.initialized = true;
27
34
  log("Database initialization complete");
28
35
  }
@@ -1,5 +1,5 @@
1
1
  import { Kysely } from "kysely";
2
- import { requestInfo } from "../../requestInfo/worker.js";
2
+ import { requestInfo, waitForRequestInfo } from "../../requestInfo/worker.js";
3
3
  import { DOWorkerDialect } from "./DOWorkerDialect.js";
4
4
  const createDurableObjectDb = (durableObjectBinding, name = "main") => {
5
5
  const durableObjectId = durableObjectBinding.idFromName(name);
@@ -12,6 +12,13 @@ const createDurableObjectDb = (durableObjectBinding, name = "main") => {
12
12
  export function createDb(durableObjectBinding, name = "main") {
13
13
  const cacheKey = `${durableObjectBinding}_${name}`;
14
14
  const doCreateDb = () => {
15
+ if (!requestInfo.rw) {
16
+ throw new Error(`
17
+ rwsdk: A database created using createDb() was accessed before requestInfo was available.
18
+
19
+ Please make sure database access is happening in a request handler or action handler.
20
+ `);
21
+ }
15
22
  let db = requestInfo.rw.databases.get(cacheKey);
16
23
  if (!db) {
17
24
  db = createDurableObjectDb(durableObjectBinding, name);
@@ -19,7 +26,7 @@ export function createDb(durableObjectBinding, name = "main") {
19
26
  }
20
27
  return db;
21
28
  };
22
- doCreateDb();
29
+ waitForRequestInfo().then(() => doCreateDb());
23
30
  return new Proxy({}, {
24
31
  get(target, prop, receiver) {
25
32
  const db = doCreateDb();
@@ -1,5 +1,8 @@
1
1
  import { RequestInfo, DefaultAppContext } from "./types";
2
- export declare const requestInfo: RequestInfo<DefaultAppContext>;
2
+ type DefaultRequestInfo = RequestInfo<DefaultAppContext>;
3
+ export declare const requestInfo: DefaultRequestInfo;
3
4
  export declare function getRequestInfo(): RequestInfo;
4
- export declare function runWithRequestInfo<Result>(context: Record<string, any>, fn: () => Result): Result;
5
- export declare function runWithRequestInfoOverrides<Result>(overrides: Record<string, any>, fn: () => Result): Result;
5
+ export declare function waitForRequestInfo(): Promise<DefaultRequestInfo>;
6
+ export declare function runWithRequestInfo<Result>(nextRequestInfo: DefaultRequestInfo, fn: () => Result): Result;
7
+ export declare function runWithRequestInfoOverrides<Result>(overrides: Partial<DefaultRequestInfo>, fn: () => Result): Result;
8
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "async_hooks";
2
+ const requestInfoDeferred = Promise.withResolvers();
2
3
  const requestInfoStore = new AsyncLocalStorage();
3
4
  const requestInfoBase = {};
4
5
  const REQUEST_INFO_KEYS = ["request", "params", "ctx", "headers", "rw", "cf"];
@@ -20,8 +21,15 @@ export function getRequestInfo() {
20
21
  }
21
22
  return store;
22
23
  }
23
- export function runWithRequestInfo(context, fn) {
24
- return requestInfoStore.run(context, fn);
24
+ export function waitForRequestInfo() {
25
+ return requestInfoDeferred.promise;
26
+ }
27
+ export function runWithRequestInfo(nextRequestInfo, fn) {
28
+ const runWithRequestInfoFn = () => {
29
+ requestInfoDeferred.resolve(nextRequestInfo);
30
+ return fn();
31
+ };
32
+ return requestInfoStore.run(nextRequestInfo, runWithRequestInfoFn);
25
33
  }
26
34
  export function runWithRequestInfoOverrides(overrides, fn) {
27
35
  const requestInfo = requestInfoStore.getStore();
@@ -29,5 +37,5 @@ export function runWithRequestInfoOverrides(overrides, fn) {
29
37
  ...requestInfo,
30
38
  ...overrides,
31
39
  };
32
- return requestInfoStore.run(newRequestInfo, fn);
40
+ return runWithRequestInfo(newRequestInfo, fn);
33
41
  }
@@ -1,5 +1,5 @@
1
1
  export declare const defineScript: (fn: ({ env }: {
2
- env: Env;
2
+ env: Cloudflare.Env;
3
3
  }) => Promise<unknown>) => {
4
- fetch(request: Request, env: Env): Promise<Response>;
4
+ fetch: (request: Request, env: Env, cf: ExecutionContext) => Promise<Response>;
5
5
  };
@@ -1,8 +1,11 @@
1
+ import { defineApp } from "./worker";
2
+ import { env } from "cloudflare:workers";
1
3
  export const defineScript = (fn) => {
2
- return {
3
- async fetch(request, env) {
4
+ const app = defineApp([
5
+ async () => {
4
6
  await fn({ env });
5
7
  return new Response("Done!");
6
8
  },
7
- };
9
+ ]);
10
+ return app;
8
11
  };
@@ -136,10 +136,8 @@ export const debugSync = async (opts) => {
136
136
  throw e;
137
137
  }
138
138
  // Initial sync for watch mode. We do it *after* acquiring the lock.
139
- let initialSyncOk = false;
140
139
  try {
141
140
  await performSync(sdkDir, targetDir);
142
- initialSyncOk = true;
143
141
  }
144
142
  catch (error) {
145
143
  console.error("❌ Initial sync failed:", error);
@@ -196,9 +194,9 @@ export const debugSync = async (opts) => {
196
194
  };
197
195
  process.on("SIGINT", cleanup);
198
196
  process.on("SIGTERM", cleanup);
199
- if (initialSyncOk) {
200
- runWatchedCommand();
201
- }
197
+ // Run the watched command even if the initial sync fails. This allows the
198
+ // user to see application errors and iterate more quickly.
199
+ runWatchedCommand();
202
200
  };
203
201
  if (import.meta.url === new URL(process.argv[1], import.meta.url).href) {
204
202
  const args = process.argv.slice(2);
@@ -1,9 +1,13 @@
1
+ import path from "path";
1
2
  import { resolve } from "path";
2
3
  import { writeFile } from "fs/promises";
3
4
  import { unstable_readConfig } from "wrangler";
4
5
  import { createServer as createViteServer } from "vite";
5
6
  import tmp from "tmp-promise";
6
7
  import baseDebug from "debug";
8
+ import enhancedResolve from "enhanced-resolve";
9
+ import { readFile } from "fs/promises";
10
+ import { Lang, parse } from "@ast-grep/napi";
7
11
  import { redwood } from "../vite/index.mjs";
8
12
  import { findWranglerConfig } from "../lib/findWranglerConfig.mjs";
9
13
  const debug = baseDebug("rwsdk:worker-run");
@@ -22,28 +26,71 @@ export const runWorkerScript = async (relativeScriptPath) => {
22
26
  debug("Using wrangler config: %s", workerConfigPath);
23
27
  const workerConfig = unstable_readConfig({
24
28
  config: workerConfigPath,
29
+ env: "dev",
25
30
  });
26
- const tmpWorkerPath = await tmp.file({
27
- postfix: ".json",
31
+ const durableObjectsToExport = workerConfig.durable_objects?.bindings
32
+ .filter((binding) => !binding.script_name)
33
+ .map((binding) => binding.class_name) ?? [];
34
+ const workerEntryRelativePath = workerConfig.main;
35
+ const workerEntryPath = workerEntryRelativePath ?? path.join(process.cwd(), "src/worker.tsx");
36
+ const durableObjectExports = [];
37
+ if (durableObjectsToExport.length > 0) {
38
+ const resolver = enhancedResolve.create.sync({
39
+ extensions: [".mts", ".ts", ".tsx", ".mjs", ".js", ".jsx", ".json"],
40
+ });
41
+ const workerEntryContents = await readFile(workerEntryPath, "utf-8");
42
+ const workerEntryAst = parse(Lang.Tsx, workerEntryContents);
43
+ const exportDeclarations = [
44
+ ...workerEntryAst.root().findAll('export { $$$EXPORTS } from "$MODULE"'),
45
+ ...workerEntryAst.root().findAll("export { $$$EXPORTS } from '$MODULE'"),
46
+ ...workerEntryAst.root().findAll("export { $$$EXPORTS } from '$MODULE'"),
47
+ ];
48
+ for (const exportDeclaration of exportDeclarations) {
49
+ const moduleMatch = exportDeclaration.getMatch("MODULE");
50
+ const exportsMatch = exportDeclaration.getMultipleMatches("EXPORTS");
51
+ if (!moduleMatch || exportsMatch.length === 0) {
52
+ continue;
53
+ }
54
+ const modulePath = moduleMatch.text();
55
+ const specifiers = exportsMatch.map((m) => m.text().trim());
56
+ for (const specifier of specifiers) {
57
+ if (durableObjectsToExport.includes(specifier)) {
58
+ const resolvedPath = resolver(path.dirname(workerEntryPath), modulePath);
59
+ durableObjectExports.push(`export { ${specifier} } from "${resolvedPath}";`);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ const tmpDir = await tmp.dir({
65
+ prefix: "rw-worker-run-",
66
+ unsafeCleanup: true,
28
67
  });
68
+ const relativeTmpWorkerEntryPath = "worker.tsx";
69
+ const tmpWorkerPath = path.join(tmpDir.path, "wrangler.json");
70
+ const tmpWorkerEntryPath = path.join(tmpDir.path, relativeTmpWorkerEntryPath);
29
71
  const scriptWorkerConfig = {
30
72
  ...workerConfig,
31
- configPath: tmpWorkerPath.path,
32
- userConfigPath: tmpWorkerPath.path,
33
- main: scriptPath,
73
+ configPath: tmpWorkerPath,
74
+ userConfigPath: tmpWorkerPath,
75
+ main: relativeTmpWorkerEntryPath,
34
76
  };
35
77
  try {
36
- await writeFile(tmpWorkerPath.path, JSON.stringify(scriptWorkerConfig, null, 2));
37
- debug("Worker config written to: %s", tmpWorkerPath.path);
78
+ await writeFile(tmpWorkerPath, JSON.stringify(scriptWorkerConfig, null, 2));
79
+ await writeFile(tmpWorkerEntryPath, `
80
+ ${durableObjectExports.join("\n")}
81
+ export { default } from "${scriptPath}";
82
+ `);
83
+ debug("Worker config written to: %s", tmpWorkerPath);
84
+ debug("Worker entry written to: %s", tmpWorkerEntryPath);
38
85
  process.env.RWSDK_WORKER_RUN = "1";
39
86
  const server = await createViteServer({
40
87
  configFile: false,
41
88
  plugins: [
42
89
  redwood({
43
- configPath: tmpWorkerPath.path,
90
+ configPath: tmpWorkerPath,
44
91
  includeCloudflarePlugin: true,
45
92
  entry: {
46
- worker: scriptPath,
93
+ worker: tmpWorkerEntryPath,
47
94
  },
48
95
  }),
49
96
  ],
@@ -65,13 +112,12 @@ export const runWorkerScript = async (relativeScriptPath) => {
65
112
  }
66
113
  finally {
67
114
  debug("Closing server...");
68
- await server.close();
115
+ server.close();
69
116
  debug("Server closed");
70
117
  }
71
118
  }
72
119
  finally {
73
120
  debug("Closing inspector servers...");
74
- await tmpWorkerPath.cleanup();
75
121
  debug("Temporary files cleaned up");
76
122
  }
77
123
  // todo(justinvdm, 01 Apr 2025): Investigate what handles are remaining open
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {