@hot-updater/server 0.34.0 → 0.35.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-updater/server",
3
- "version": "0.34.0",
3
+ "version": "0.35.0",
4
4
  "type": "module",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "sideEffects": false,
@@ -28,14 +28,14 @@
28
28
  "import": "./dist/adapters/prisma.mjs",
29
29
  "require": "./dist/adapters/prisma.cjs"
30
30
  },
31
+ "./db": {
32
+ "import": "./dist/db/index.mjs",
33
+ "require": "./dist/db/index.cjs"
34
+ },
31
35
  "./node": {
32
36
  "import": "./dist/node.mjs",
33
37
  "require": "./dist/node.cjs"
34
38
  },
35
- "./runtime": {
36
- "import": "./dist/runtime.mjs",
37
- "require": "./dist/runtime.cjs"
38
- },
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "files": [
@@ -55,10 +55,10 @@
55
55
  "dependencies": {
56
56
  "rou3": "0.7.9",
57
57
  "semver": "^7.7.2",
58
- "@hot-updater/bsdiff": "0.34.0",
59
- "@hot-updater/core": "0.34.0",
60
- "@hot-updater/js": "0.34.0",
61
- "@hot-updater/plugin-core": "0.34.0"
58
+ "@hot-updater/bsdiff": "0.35.0",
59
+ "@hot-updater/core": "0.35.0",
60
+ "@hot-updater/plugin-core": "0.35.0",
61
+ "@hot-updater/js": "0.35.0"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@prisma/client": "*",
@@ -91,8 +91,8 @@
91
91
  "mongodb": "6.20.0",
92
92
  "msw": "^2.7.0",
93
93
  "uuidv7": "^1.0.2",
94
- "@hot-updater/standalone": "0.34.0",
95
- "@hot-updater/test-utils": "0.34.0"
94
+ "@hot-updater/standalone": "0.35.0",
95
+ "@hot-updater/test-utils": "0.35.0"
96
96
  },
97
97
  "scripts": {
98
98
  "build": "tsdown",
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { createHotUpdater } from "../db";
3
+ import { generateSchema } from "../db";
4
+ import { createHotUpdater } from "../index";
4
5
  import { drizzleAdapter } from "./drizzle";
5
6
 
6
7
  describe("drizzleAdapter", () => {
@@ -16,7 +17,7 @@ describe("drizzleAdapter", () => {
16
17
  }),
17
18
  });
18
19
 
19
- const schema = hotUpdater.generateSchema("latest");
20
+ const schema = generateSchema(hotUpdater, "latest");
20
21
 
21
22
  expect(schema.path).toBe("hot-updater-schema.ts");
22
23
  expect(schema.code).toContain("pgTable");
@@ -4,7 +4,7 @@ import { Kysely } from "kysely";
4
4
  import { PGliteDialect } from "kysely-pglite-dialect";
5
5
  import { afterEach, describe, expect, it } from "vitest";
6
6
 
7
- import { createHotUpdater } from "../db";
7
+ import { createHotUpdater } from "../index";
8
8
  import {
9
9
  HOT_UPDATER_SCHEMA_VERSION,
10
10
  HOT_UPDATER_SETTINGS_TABLE,
@@ -19,33 +19,70 @@ import { createHandler, type HandlerRoutes } from "./handler";
19
19
  import { normalizeBasePath } from "./route";
20
20
  import { createStorageAccess } from "./storageAccess";
21
21
 
22
- export type HotUpdaterAPI<TContext = unknown> = DatabaseAPI<TContext> & {
23
- basePath: string;
24
- handler: (
22
+ export type RuntimeHotUpdaterAPI<TContext = unknown> = DatabaseAPI<TContext> & {
23
+ readonly basePath: string;
24
+ readonly handler: (
25
25
  request: Request,
26
26
  context?: HotUpdaterContext<TContext>,
27
27
  ) => Promise<Response>;
28
- adapterName: string;
28
+ readonly adapterName: string;
29
29
  };
30
30
 
31
+ export type HotUpdaterAPI<TContext = unknown> = RuntimeHotUpdaterAPI<TContext>;
32
+
31
33
  export interface CreateHotUpdaterOptions<TContext = unknown> {
32
- database: DatabaseAdapter<TContext>;
33
- storages?: (
34
+ readonly database: DatabaseAdapter<TContext>;
35
+ readonly storages?: readonly (
34
36
  | RuntimeStoragePlugin<TContext>
35
37
  | StoragePluginFactory<TContext>
36
38
  )[];
37
- storagePlugins?: (
39
+ /**
40
+ * @deprecated Use `storages` instead. This field will be removed in a future version.
41
+ */
42
+ readonly storagePlugins?: readonly (
38
43
  | RuntimeStoragePlugin<TContext>
39
44
  | StoragePluginFactory<TContext>
40
45
  )[];
41
- basePath?: string;
42
- cwd?: string;
43
- routes?: HandlerRoutes;
46
+ readonly basePath?: string;
47
+ readonly cwd?: string;
48
+ readonly routes?: HandlerRoutes;
44
49
  }
45
50
 
46
- export function createHotUpdater<TContext = unknown>(
51
+ type PluginDatabaseCore<TContext> = {
52
+ readonly api: DatabaseAPI<TContext>;
53
+ readonly adapterName: string;
54
+ readonly createMigrator: () => never;
55
+ readonly generateSchema: () => never;
56
+ };
57
+
58
+ export const hotUpdaterCoreMetadata = Symbol.for(
59
+ "@hot-updater/server/core-metadata",
60
+ );
61
+
62
+ export type HotUpdaterCoreMetadata<TContext = unknown> = {
63
+ readonly adapterCapabilities: DatabaseAdapterCapabilities;
64
+ readonly core: PluginDatabaseCore<TContext>;
65
+ };
66
+
67
+ export type HotUpdaterCore<TContext = unknown> = {
68
+ readonly api: RuntimeHotUpdaterAPI<TContext>;
69
+ readonly adapterCapabilities: DatabaseAdapterCapabilities;
70
+ readonly core: PluginDatabaseCore<TContext>;
71
+ };
72
+
73
+ export function getHotUpdaterCoreMetadata<TContext = unknown>(
74
+ hotUpdater: RuntimeHotUpdaterAPI<TContext>,
75
+ ): HotUpdaterCoreMetadata<TContext> | undefined {
76
+ return (
77
+ hotUpdater as RuntimeHotUpdaterAPI<TContext> & {
78
+ readonly [hotUpdaterCoreMetadata]?: HotUpdaterCoreMetadata<TContext>;
79
+ }
80
+ )[hotUpdaterCoreMetadata];
81
+ }
82
+
83
+ export function createHotUpdaterCore<TContext = unknown>(
47
84
  options: CreateHotUpdaterOptions<TContext>,
48
- ): HotUpdaterAPI<TContext> {
85
+ ): HotUpdaterCore<TContext> {
49
86
  const database = options.database;
50
87
  const basePath = normalizeBasePath(options.basePath ?? "/api");
51
88
  const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map(
@@ -59,19 +96,17 @@ export function createHotUpdater<TContext = unknown>(
59
96
  createStorageAccess(storagePlugins);
60
97
 
61
98
  if (!isDatabasePluginFactory(database) && !isDatabasePlugin(database)) {
62
- throw new Error(
63
- "@hot-updater/server/runtime only supports database plugins.",
64
- );
99
+ throw new Error("@hot-updater/server only supports database plugins.");
65
100
  }
66
101
 
67
- const capabilities = database as DatabaseAdapterCapabilities;
102
+ const adapterCapabilities = database as DatabaseAdapterCapabilities;
68
103
  const plugin: DatabasePlugin<TContext> = isDatabasePluginFactory(database)
69
104
  ? database()
70
105
  : database;
71
- const adapterName = capabilities.adapterName ?? plugin.name;
106
+ const adapterName = adapterCapabilities.adapterName ?? plugin.name;
72
107
  const assertSchemaReady = createSchemaReadinessChecker(
73
108
  adapterName,
74
- capabilities.createMigrator,
109
+ adapterCapabilities.createMigrator,
75
110
  );
76
111
  const core = createPluginDatabaseCore<TContext>(
77
112
  () => plugin,
@@ -93,7 +128,7 @@ export function createHotUpdater<TContext = unknown>(
93
128
  // Some framework adapters strip the mounted base path or pass extra
94
129
  // bindings/execution context arguments. Ignore those extras here so the
95
130
  // handler can still be mounted directly as a plain Request handler.
96
- const handler: HotUpdaterAPI<TContext>["handler"] = (
131
+ const handler: RuntimeHotUpdaterAPI<TContext>["handler"] = (
97
132
  request,
98
133
  context,
99
134
  ...extraArgs: unknown[]
@@ -107,12 +142,27 @@ export function createHotUpdater<TContext = unknown>(
107
142
 
108
143
  const api = {
109
144
  basePath,
110
- adapterName: core.adapterName,
145
+ adapterName: adapterCapabilities.adapterName ?? core.adapterName,
111
146
  handler,
112
147
  };
113
148
  Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
114
- return api as HotUpdaterAPI<TContext>;
149
+ Object.defineProperty(api, hotUpdaterCoreMetadata, {
150
+ enumerable: false,
151
+ value: {
152
+ adapterCapabilities,
153
+ core,
154
+ } satisfies HotUpdaterCoreMetadata<TContext>,
155
+ });
156
+
157
+ return {
158
+ api: api as RuntimeHotUpdaterAPI<TContext>,
159
+ adapterCapabilities,
160
+ core,
161
+ };
115
162
  }
116
163
 
117
- export { createHandler };
118
- export { HOT_UPDATER_SERVER_VERSION } from "./version";
164
+ export function createHotUpdater<TContext = unknown>(
165
+ options: CreateHotUpdaterOptions<TContext>,
166
+ ): RuntimeHotUpdaterAPI<TContext> {
167
+ return createHotUpdaterCore(options).api;
168
+ }
@@ -28,13 +28,14 @@ import { drizzleAdapter } from "../adapters/drizzle";
28
28
  import { kyselyAdapter } from "../adapters/kysely";
29
29
  import { mongoAdapter } from "../adapters/mongodb";
30
30
  import { prismaAdapter } from "../adapters/prisma";
31
+ import { createHotUpdater } from "../index";
31
32
  import { bundleToRow } from "./bundleRows";
32
33
  import {
33
34
  createSchemaMigrationSql,
34
35
  createTableSql,
35
36
  hotUpdaterSchemaVersions,
36
37
  } from "./hotUpdaterSchema";
37
- import { createHotUpdater } from "./index";
38
+ import { createMigrator, generateSchema } from "./index";
38
39
  import { generateDrizzleSchema } from "./schemaGenerators";
39
40
  import type { DatabasePluginFactory, ORMProvider } from "./types";
40
41
 
@@ -340,7 +341,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
340
341
  });
341
342
 
342
343
  beforeAll(async () => {
343
- const migrator = hotUpdater.createMigrator();
344
+ const migrator = createMigrator(hotUpdater);
344
345
  const result = await migrator.migrateToLatest({
345
346
  mode: "from-schema",
346
347
  updateSettings: true,
@@ -382,7 +383,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
382
383
 
383
384
  describe("schema generation", () => {
384
385
  it("includes relations, defaults, and indexes in Prisma output", () => {
385
- const code = prismaSchemaHotUpdater.generateSchema("latest").code;
386
+ const code = generateSchema(prismaSchemaHotUpdater, "latest").code;
386
387
 
387
388
  expect(code).toContain('channel String @default("production")');
388
389
  expect(code).toContain('metadata Json @default("{}")');
@@ -407,14 +408,14 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
407
408
  });
408
409
 
409
410
  it("omits the metadata JSON default for SQLite Prisma output", () => {
410
- const code = sqlitePrismaSchemaHotUpdater.generateSchema("latest").code;
411
+ const code = generateSchema(sqlitePrismaSchemaHotUpdater, "latest").code;
411
412
 
412
413
  expect(code).toContain("metadata Json");
413
414
  expect(code).not.toContain('metadata Json @default("{}")');
414
415
  });
415
416
 
416
417
  it("generates ORM schema from the requested version snapshot", () => {
417
- const code = prismaSchemaHotUpdater.generateSchema("0.21.0").code;
418
+ const code = generateSchema(prismaSchemaHotUpdater, "0.21.0").code;
418
419
 
419
420
  expect(code).toContain('value String @default("0.21.0")');
420
421
  expect(code).not.toContain("rollout_cohort_count");
@@ -422,7 +423,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
422
423
  });
423
424
 
424
425
  it("includes foreign keys and indexes in Drizzle output", () => {
425
- const code = drizzleSchemaHotUpdater.generateSchema("latest").code;
426
+ const code = generateSchema(drizzleSchemaHotUpdater, "latest").code;
426
427
  const bundlesBlock = code.match(
427
428
  /export const bundles = [\s\S]*?(?=\n\nexport const bundle_patches = )/,
428
429
  )?.[0];
@@ -526,7 +527,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
526
527
  });
527
528
 
528
529
  try {
529
- const migrator = migrationHotUpdater.createMigrator();
530
+ const migrator = createMigrator(migrationHotUpdater);
530
531
  const result = await migrator.migrateToLatest({
531
532
  mode: "from-schema",
532
533
  updateSettings: false,
@@ -593,7 +594,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
593
594
  values ('version', '0.21.0');
594
595
  `);
595
596
 
596
- const migrator = migrationHotUpdater.createMigrator();
597
+ const migrator = createMigrator(migrationHotUpdater);
597
598
  const result = await migrator.migrateToLatest({
598
599
  mode: "from-schema",
599
600
  updateSettings: true,
@@ -639,7 +640,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
639
640
  });
640
641
 
641
642
  try {
642
- const migrator = migrationHotUpdater.createMigrator();
643
+ const migrator = createMigrator(migrationHotUpdater);
643
644
  const result = await migrator.migrateToLatest({
644
645
  mode: "from-schema",
645
646
  updateSettings: false,
@@ -671,7 +672,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
671
672
  });
672
673
 
673
674
  try {
674
- const migrator = migrationHotUpdater.createMigrator();
675
+ const migrator = createMigrator(migrationHotUpdater);
675
676
  const result = await migrator.migrateToLatest({
676
677
  mode: "from-schema",
677
678
  updateSettings: false,
@@ -703,9 +704,9 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
703
704
  const mongoHotUpdater = createHotUpdater({
704
705
  database: mongoAdapter({ client }),
705
706
  });
706
- const result = await mongoHotUpdater
707
- .createMigrator()
708
- .migrateToLatest({ mode: "from-schema" });
707
+ const result = await createMigrator(mongoHotUpdater).migrateToLatest({
708
+ mode: "from-schema",
709
+ });
709
710
 
710
711
  expect(result.operations).toEqual(
711
712
  expect.arrayContaining([
@@ -742,7 +743,7 @@ describe("server/db hotUpdater getUpdateInfo (PGlite + Kysely)", async () => {
742
743
 
743
744
  try {
744
745
  await expect(
745
- migrationHotUpdater.createMigrator().migrateToLatest({
746
+ createMigrator(migrationHotUpdater).migrateToLatest({
746
747
  mode: "from-database",
747
748
  }),
748
749
  ).rejects.toThrow(
package/src/db/index.ts CHANGED
@@ -1,119 +1,47 @@
1
- import type {
2
- DatabasePlugin,
3
- HotUpdaterContext,
4
- RuntimeStoragePlugin,
5
- } from "@hot-updater/plugin-core";
6
- import { assertRuntimeStoragePlugin } from "@hot-updater/plugin-core";
7
-
8
- export * from "./createBundleDiff";
9
- import { createHandler, type HandlerRoutes } from "../handler";
10
- import { normalizeBasePath } from "../route";
11
- import { createStorageAccess } from "../storageAccess";
12
- import { createPluginDatabaseCore } from "./pluginCore";
13
- import { generateSchemaFromHotUpdaterSchema } from "./schemaGenerators";
14
- import { createSchemaReadinessChecker } from "./schemaReadiness";
15
1
  import {
16
- type DatabaseAdapterCapabilities,
17
- type DatabaseAdapter,
18
- type DatabaseAPI,
19
- type Migrator,
20
- type SchemaGenerator,
21
- isDatabasePluginFactory,
22
- type StoragePluginFactory,
23
- } from "./types";
2
+ getHotUpdaterCoreMetadata,
3
+ type RuntimeHotUpdaterAPI,
4
+ } from "../createHotUpdaterCore";
5
+ import { generateSchemaFromHotUpdaterSchema } from "./schemaGenerators";
6
+ import { type Migrator, type SchemaGenerator } from "./types";
24
7
 
8
+ export * from "./createBundleDiff";
25
9
  export type { Migrator, SchemaGenerator } from "./types";
26
10
  export { HotUpdaterSchemaMigrationRequiredError } from "./schemaReadiness";
27
11
  export { HOT_UPDATER_SERVER_VERSION } from "../version";
28
12
 
29
- export type HotUpdaterAPI<TContext = unknown> = DatabaseAPI<TContext> & {
30
- basePath: string;
31
- handler: (
32
- request: Request,
33
- context?: HotUpdaterContext<TContext>,
34
- ) => Promise<Response>;
35
- adapterName: string;
36
- createMigrator: () => Migrator;
37
- generateSchema: SchemaGenerator;
13
+ export type HotUpdaterDBTarget = {
14
+ readonly adapterName: string;
38
15
  };
39
16
 
40
- export interface CreateHotUpdaterOptions<TContext = unknown> {
41
- database: DatabaseAdapter<TContext>;
42
- /**
43
- * Storage plugins for handling file uploads and downloads.
44
- */
45
- storages?: (
46
- | RuntimeStoragePlugin<TContext>
47
- | StoragePluginFactory<TContext>
48
- )[];
49
- /**
50
- * @deprecated Use `storages` instead. This field will be removed in a future version.
51
- */
52
- storagePlugins?: (
53
- | RuntimeStoragePlugin<TContext>
54
- | StoragePluginFactory<TContext>
55
- )[];
56
- basePath?: string;
57
- cwd?: string;
58
- routes?: HandlerRoutes;
59
- }
60
-
61
- export function createHotUpdater<TContext = unknown>(
62
- options: CreateHotUpdaterOptions<TContext>,
63
- ): HotUpdaterAPI<TContext> {
64
- const basePath = normalizeBasePath(options.basePath ?? "/api");
65
-
66
- // Initialize storage plugins - call factories if they are functions
67
- const storagePlugins = (options.storages ?? options.storagePlugins ?? []).map(
68
- (plugin) => {
69
- const storagePlugin = typeof plugin === "function" ? plugin() : plugin;
70
- assertRuntimeStoragePlugin(storagePlugin);
71
- return storagePlugin;
72
- },
17
+ const getDBMetadata = (hotUpdater: HotUpdaterDBTarget) => {
18
+ const metadata = getHotUpdaterCoreMetadata(
19
+ hotUpdater as RuntimeHotUpdaterAPI,
73
20
  );
74
- const { readStorageText, resolveFileUrl } =
75
- createStorageAccess(storagePlugins);
21
+ if (!metadata) {
22
+ throw new Error(
23
+ "Database tooling requires a hotUpdater instance created by @hot-updater/server.",
24
+ );
25
+ }
26
+ return metadata;
27
+ };
76
28
 
77
- const database = options.database;
29
+ export function createMigrator(hotUpdater: HotUpdaterDBTarget): Migrator {
30
+ const { adapterCapabilities, core } = getDBMetadata(hotUpdater);
31
+ return (adapterCapabilities.createMigrator ?? core.createMigrator)();
32
+ }
78
33
 
79
- const capabilities = database as DatabaseAdapterCapabilities;
80
- const plugin: DatabasePlugin<TContext> = isDatabasePluginFactory(database)
81
- ? database()
82
- : database;
83
- const adapterName = capabilities.adapterName ?? plugin.name;
84
- const assertSchemaReady = createSchemaReadinessChecker(
85
- adapterName,
86
- capabilities.createMigrator,
87
- );
88
- const core = createPluginDatabaseCore<TContext>(
89
- () => plugin,
90
- resolveFileUrl,
91
- isDatabasePluginFactory(database)
92
- ? {
93
- createMutationPlugin: () => database(),
94
- beforeOperation: assertSchemaReady,
95
- readStorageText,
96
- }
97
- : { beforeOperation: assertSchemaReady, readStorageText },
34
+ export function generateSchema(
35
+ hotUpdater: HotUpdaterDBTarget,
36
+ ...args: Parameters<SchemaGenerator>
37
+ ): ReturnType<SchemaGenerator> {
38
+ const { adapterCapabilities, core } = getDBMetadata(hotUpdater);
39
+ const schemaGenerator =
40
+ adapterCapabilities.generateSchema ?? core.generateSchema;
41
+ return generateSchemaFromHotUpdaterSchema(
42
+ hotUpdater.adapterName,
43
+ adapterCapabilities.provider,
44
+ args[0],
45
+ schemaGenerator(...args),
98
46
  );
99
-
100
- const generateSchema = capabilities.generateSchema ?? core.generateSchema;
101
- const api = {
102
- basePath,
103
- adapterName: capabilities.adapterName ?? core.adapterName,
104
- createMigrator: capabilities.createMigrator ?? core.createMigrator,
105
- generateSchema: (...args: Parameters<SchemaGenerator>) =>
106
- generateSchemaFromHotUpdaterSchema(
107
- api.adapterName,
108
- capabilities.provider,
109
- args[0],
110
- generateSchema(...args),
111
- ),
112
- handler: createHandler(core.api, {
113
- basePath,
114
- routes: options.routes,
115
- }),
116
- };
117
- Object.defineProperties(api, Object.getOwnPropertyDescriptors(core.api));
118
- return api as HotUpdaterAPI<TContext>;
119
47
  }
@@ -11,7 +11,8 @@ import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
11
11
 
12
12
  import { standaloneRepository } from "../../../plugins/standalone/src";
13
13
  import { kyselyAdapter } from "./adapters/kysely";
14
- import { createHotUpdater } from "./db";
14
+ import { createMigrator } from "./db";
15
+ import { createHotUpdater } from "./index";
15
16
 
16
17
  /**
17
18
  * Integration tests between @hot-updater/server handler and @hot-updater/standalone repository
@@ -46,7 +47,7 @@ const server = setupServer();
46
47
 
47
48
  beforeAll(async () => {
48
49
  // Initialize database
49
- const migrator = api.createMigrator();
50
+ const migrator = createMigrator(api);
50
51
  const result = await migrator.migrateToLatest({
51
52
  mode: "from-schema",
52
53
  updateSettings: true,
package/src/index.ts CHANGED
@@ -1,4 +1,10 @@
1
- export * from "./db";
2
- export * from "./handler";
1
+ export { createHandler } from "./handler";
2
+ export type { HandlerAPI, HandlerOptions, HandlerRoutes } from "./handler";
3
+ export { createHotUpdater } from "./createHotUpdaterCore";
4
+ export type {
5
+ CreateHotUpdaterOptions,
6
+ HotUpdaterAPI,
7
+ RuntimeHotUpdaterAPI,
8
+ } from "./createHotUpdaterCore";
3
9
  export * from "./types";
4
- export * from "./version";
10
+ export { HOT_UPDATER_SERVER_VERSION } from "./version";
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { toNodeHandler } from "./node";
4
+
5
+ describe("server node entry", () => {
6
+ it("converts a Web Request handler to Node middleware", async () => {
7
+ const hotUpdater = {
8
+ handler: async (request: Request) =>
9
+ Response.json({
10
+ method: request.method,
11
+ pathname: new URL(request.url).pathname,
12
+ }),
13
+ };
14
+ const middleware = toNodeHandler(hotUpdater);
15
+ const headers = new Map<string, string | string[]>();
16
+ const response = {
17
+ body: "",
18
+ statusCode: 0,
19
+ status(code: number) {
20
+ this.statusCode = code;
21
+ return this;
22
+ },
23
+ setHeader(name: string, value: string | string[]) {
24
+ headers.set(name, value);
25
+ },
26
+ send(body: string) {
27
+ this.body = body;
28
+ },
29
+ end() {},
30
+ };
31
+
32
+ await middleware(
33
+ {
34
+ method: "GET",
35
+ url: "/api/check",
36
+ headers: { host: "example.com" },
37
+ protocol: "https",
38
+ get: (name: string) => (name === "host" ? "example.com" : undefined),
39
+ },
40
+ response,
41
+ );
42
+
43
+ expect(response.statusCode).toBe(200);
44
+ expect(headers.get("content-type")).toContain("application/json");
45
+ expect(JSON.parse(response.body)).toEqual({
46
+ method: "GET",
47
+ pathname: "/api/check",
48
+ });
49
+ });
50
+ });
package/src/node.ts CHANGED
@@ -1,5 +1,3 @@
1
- import type { HotUpdaterAPI } from "./types";
2
-
3
1
  /**
4
2
  * Node.js request/response types (compatible with Express, Connect, etc.)
5
3
  */
@@ -21,6 +19,12 @@ interface NodeResponse {
21
19
  [key: string]: unknown;
22
20
  }
23
21
 
22
+ type HandlerHotUpdaterAPI = {
23
+ readonly handler: (request: Request) => Promise<Response>;
24
+ };
25
+
26
+ export { HOT_UPDATER_SERVER_VERSION } from "./version";
27
+
24
28
  /**
25
29
  * Converts a Hot Updater handler to a Node.js-compatible middleware
26
30
  * Works with Express, Connect, and other frameworks using Node.js req/res
@@ -40,7 +44,7 @@ interface NodeResponse {
40
44
  * ```
41
45
  */
42
46
  export function toNodeHandler(
43
- hotUpdater: HotUpdaterAPI,
47
+ hotUpdater: HandlerHotUpdaterAPI,
44
48
  ): (req: any, res: any, next?: any) => Promise<void> {
45
49
  return async (req: NodeRequest, res: NodeResponse) => {
46
50
  try {