arri 0.12.8 → 0.15.1

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/README.md CHANGED
@@ -149,7 +149,9 @@ Setup your npm scripts:
149
149
 
150
150
  ## Usage
151
151
 
152
- ### File Based Routing
152
+ ### Creating Procedures
153
+
154
+ #### File Based Routing
153
155
 
154
156
  Arri RPC comes with an optional file based router that will automatically register functions in the `./procedures` directory that end with the `.rpc.ts` file extension.
155
157
 
@@ -184,7 +186,7 @@ export default defineRpc({
184
186
  });
185
187
  ```
186
188
 
187
- #### Customizing the File Based Router
189
+ ##### Customizing the File Based Router
188
190
 
189
191
  ```ts
190
192
  export default defineConfig({
@@ -194,7 +196,7 @@ export default defineConfig({
194
196
  });
195
197
  ```
196
198
 
197
- ### Manual Routing
199
+ #### Manual Routing
198
200
 
199
201
  ```ts
200
202
  // using the app instance
@@ -208,6 +210,76 @@ router.rpc('sayHello', {...})
208
210
  app.use(router)
209
211
  ```
210
212
 
213
+ #### Creating Event Stream Procedures
214
+
215
+ Event stream procedures make use of [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) to stream events to clients.
216
+
217
+ Normal events will have the event type `message`. Error events will have the event type `error`, and a `done` event will be sent when closing the connection. Additionally the `EventStreamConnection` will periodically since a `ping` event to keep the connection alive
218
+
219
+ ```ts
220
+ /// standard event ///
221
+ id: string | undefined;
222
+ event: "message";
223
+ data: Response; // whatever you have specified as the response serialized to json
224
+
225
+ /// error event ///
226
+ id: string | undefined;
227
+ event: "error";
228
+ data: ArriRequestError; // serialized to json
229
+
230
+ /// done event ///
231
+ event: "done";
232
+ data: "this stream has ended";
233
+
234
+ /// ping event ///
235
+ event: "ping";
236
+ data: "";
237
+ ```
238
+
239
+ ##### Example Usage:
240
+
241
+ ```ts
242
+ // procedures/users/watchUser.rpc.ts
243
+ export default defineEventStreamRpc({
244
+ params: a.object({
245
+ userId: a.string(),
246
+ }),
247
+ response: a.object({
248
+ id: a.string(),
249
+ name: a.string(),
250
+ createdAt: a.timestamp(),
251
+ updatedAt: a.timestamp(),
252
+ }),
253
+ handler({ params, connection }) {
254
+ // send a message every second
255
+ const interval = setInterval(async () => {
256
+ await connection.push({
257
+ id: "1",
258
+ name: "John Doe",
259
+ createdAt: new Date(),
260
+ updatedAt: new Date(),
261
+ });
262
+ }, 1000);
263
+ // cleanup when the client disconnects
264
+ connection.on("disconnect", () => {
265
+ clearInterval(interval);
266
+ });
267
+ // start streaming events to the client
268
+ connection.start();
269
+ },
270
+ });
271
+ ```
272
+
273
+ #### EventStreamConnection methods
274
+
275
+ ```ts
276
+ connection.push(data: Data, eventId?: string)
277
+ connection.pushError(error: ArriRequestError, eventId?: string)
278
+ connection.start()
279
+ connection.end()
280
+ connection.on(e: 'disconnect' | 'end', callback: () => any)
281
+ ```
282
+
211
283
  ### Adding Non-RPC Routes
212
284
 
213
285
  You can also add generic endpoints for instances when a message-based RPC endpoint doesn't fit.
package/dist/cli.cjs CHANGED
@@ -9,9 +9,10 @@ const arriCodegenUtils = require('arri-codegen-utils');
9
9
  const c12 = require('c12');
10
10
  const consola = require('consola');
11
11
  const esbuild = require('esbuild');
12
+ const esbuildPluginReplace = require('esbuild-plugin-replace');
12
13
  const path = require('pathe');
13
14
  const prettier = require('prettier');
14
- const config = require('./shared/arri.fa32b1ba.cjs');
15
+ const config = require('./shared/arri.2eb37786.cjs');
15
16
  const globby = require('globby');
16
17
  const scule = require('scule');
17
18
  const arriCodegenDart = require('arri-codegen-dart');
@@ -45,10 +46,11 @@ const prettier__default = /*#__PURE__*/_interopDefaultCompat(prettier);
45
46
  const chokidar__default = /*#__PURE__*/_interopDefaultCompat(chokidar);
46
47
  const Degit__default = /*#__PURE__*/_interopDefaultCompat(Degit);
47
48
 
48
- const DEFAULT_SERVER_ENTRY_FILE = "__default_arri_server.js";
49
- const DEFAULT_CODEGEN_FILE = "__default_arri_codegen.js";
50
- const SERVER_ENTRY_OUTPUT = "server.js";
51
- const CODEGEN_OUTPUT = "codegen.js";
49
+ const GEN_APP_FILE = "__arri_app.js";
50
+ const GEN_SERVER_ENTRY_FILE = "__arri_server.js";
51
+ const OUT_APP_FILE = "app.js";
52
+ const OUT_SERVER_ENTRY = "server.js";
53
+ const OUT_CODEGEN = "codegen.js";
52
54
  async function setupWorkingDir(config) {
53
55
  const arriDir = path__default.resolve(config.rootDir, config.buildDir);
54
56
  const outDir = path__default.resolve(config.rootDir, ".output");
@@ -61,12 +63,15 @@ async function setupWorkingDir(config) {
61
63
  await fs__default.mkdir(arriDir);
62
64
  await fs__default.mkdir(outDir);
63
65
  }
64
- async function createRoutesModule(config) {
66
+ async function createAppWithRoutesModule(config) {
67
+ const appModule = path__default.resolve(config.rootDir, config.srcDir, config.entry);
68
+ const appImportParts = path__default.relative(path__default.resolve(config.rootDir, config.srcDir), appModule).split(".");
69
+ appImportParts.pop();
65
70
  const routes = [];
66
71
  const existingRoutes = [];
67
72
  await Promise.all(
68
73
  config.procedureGlobPatterns.map(async (pattern) => {
69
- const results = await getVirtualRouteBatch(pattern, config);
74
+ const results = await getFsRouteBatch(pattern, config);
70
75
  for (const result of results) {
71
76
  if (!existingRoutes.includes(result.name)) {
72
77
  routes.push(result);
@@ -77,22 +82,29 @@ async function createRoutesModule(config) {
77
82
  );
78
83
  routes.sort((a, b) => a.name < b.name ? -1 : 1);
79
84
  const module = await prettier__default.format(
80
- `${routes.map(
85
+ `import app from './${appImportParts.join(".")}';
86
+ ${routes.map(
81
87
  (route) => `import ${route.importName} from '${route.importPath}';`
82
88
  ).join("\n")}
83
89
  const routes = [${routes.map(
84
90
  (route) => `{ id: '${route.name}', route: ${route.importName} }`
85
91
  ).join(",\n")}
86
92
  ];
87
- export default routes`,
93
+ for(const route of routes) {
94
+ app.rpc({
95
+ name: route.id,
96
+ ...route.route,
97
+ });
98
+ }
99
+ export default app`,
88
100
  { parser: "typescript", tabWidth: 4 }
89
101
  );
90
102
  await fs__default.writeFile(
91
- path__default.resolve(config.rootDir, config.buildDir, "routes.js"),
103
+ path__default.resolve(config.rootDir, config.buildDir, GEN_APP_FILE),
92
104
  module
93
105
  );
94
106
  }
95
- async function getVirtualRouteBatch(globPattern, config) {
107
+ async function getFsRouteBatch(globPattern, config) {
96
108
  if (!config.procedureDir) {
97
109
  return [];
98
110
  }
@@ -198,19 +210,15 @@ async function startBuild(config, skipCodeGen = false) {
198
210
  logger$2.log("Bundling server....");
199
211
  await setupWorkingDir(config);
200
212
  await Promise.all([
201
- createRoutesModule(config),
202
- createBuildEntryModule(config),
203
- createBuildCodegenModule(config)
213
+ createAppWithRoutesModule(config),
214
+ createServerEntryFile(config),
215
+ createCodegenEntryFile(config),
216
+ transpileFiles(config)
204
217
  ]);
205
- await transpileFiles(config);
206
- await bundleFiles(config);
218
+ await bundleAppEntry(config);
207
219
  logger$2.log("Finished bundling");
208
220
  const clientCount = config.clientGenerators.length;
209
- const codegenModule = path__default.resolve(
210
- config.rootDir,
211
- ".output",
212
- CODEGEN_OUTPUT
213
- );
221
+ const codegenModule = path__default.resolve(config.rootDir, ".output", OUT_CODEGEN);
214
222
  if (!skipCodeGen) {
215
223
  logger$2.log("Generating Arri app definition (__definition.json)");
216
224
  node_child_process.execSync(`node ${codegenModule}`, { env: process.env });
@@ -237,48 +245,18 @@ async function startBuild(config, skipCodeGen = false) {
237
245
  logger$2.log("Cleaning up files");
238
246
  await fs__default.rm(codegenModule);
239
247
  logger$2.success(
240
- `Build finished! You can start your server by running "node .output/${SERVER_ENTRY_OUTPUT}"`
248
+ `Build finished! You can start your server by running "node .output/${OUT_SERVER_ENTRY}"`
241
249
  );
242
250
  }
243
- async function bundleFiles(config, allowCodegen = true) {
244
- if (allowCodegen) {
245
- await esbuild.build({
246
- ...config.esbuild,
247
- entryPoints: [
248
- path__default.resolve(
249
- config.rootDir,
250
- config.buildDir,
251
- DEFAULT_CODEGEN_FILE
252
- )
253
- ],
254
- platform: config.esbuild.platform ?? "node",
255
- target: config.esbuild.target ?? "node20",
256
- packages: "external",
257
- bundle: true,
258
- format: "esm",
259
- sourcemap: false,
260
- minifyWhitespace: false,
261
- banner: {
262
- js: `import { createRequire as topLevelCreateRequire } from 'module';
263
- const require = topLevelCreateRequire(import.meta.url);`
264
- },
265
- outfile: path__default.resolve(config.rootDir, ".output", CODEGEN_OUTPUT)
266
- });
267
- }
268
- let buildEntry = path__default.resolve(
251
+ async function bundleAppEntry(config, allowCodegen = true) {
252
+ const appEntry = path__default.resolve(
269
253
  config.rootDir,
270
254
  config.buildDir,
271
- DEFAULT_SERVER_ENTRY_FILE
255
+ GEN_APP_FILE
272
256
  );
273
- if (config.buildEntry) {
274
- const parts = config.buildEntry.split(".");
275
- parts.pop();
276
- const mergedParts = `${parts.join(".")}.js`;
277
- buildEntry = path__default.resolve(config.rootDir, config.buildDir, mergedParts);
278
- }
279
257
  await esbuild.build({
280
258
  ...config.esbuild,
281
- entryPoints: [buildEntry],
259
+ entryPoints: [appEntry],
282
260
  platform: config.esbuild.platform ?? "node",
283
261
  target: config.esbuild.target ?? "node20",
284
262
  bundle: true,
@@ -291,11 +269,34 @@ const require = topLevelCreateRequire(import.meta.url);`
291
269
  const require = topLevelCreateRequire(import.meta.url);`
292
270
  },
293
271
  allowOverwrite: true,
294
- outfile: path__default.resolve(config.rootDir, ".output", SERVER_ENTRY_OUTPUT)
272
+ outfile: path__default.resolve(config.rootDir, ".output", OUT_APP_FILE)
295
273
  });
296
274
  }
297
- async function createBuildEntryModule(config) {
298
- if (config.buildEntry) {
275
+ async function createServerEntryFile(config) {
276
+ if (config.serverEntry) {
277
+ const buildEntry = path__default.resolve(
278
+ config.rootDir,
279
+ config.srcDir,
280
+ config.serverEntry
281
+ );
282
+ await esbuild.build({
283
+ ...config.esbuild,
284
+ entryPoints: [buildEntry],
285
+ platform: config.esbuild.platform ?? "node",
286
+ target: config.esbuild.target ?? "node20",
287
+ bundle: false,
288
+ packages: "external",
289
+ format: "esm",
290
+ sourcemap: true,
291
+ minifyWhitespace: true,
292
+ banner: {
293
+ js: `import { createRequire as topLevelCreateRequire } from 'module';
294
+ const require = topLevelCreateRequire(import.meta.url);`
295
+ },
296
+ plugins: [esbuildPluginReplace.replace({ "virtual:arri/app": "./app.js" })],
297
+ allowOverwrite: true,
298
+ outfile: path__default.resolve(config.rootDir, ".output", OUT_SERVER_ENTRY)
299
+ });
299
300
  return;
300
301
  }
301
302
  const appModule = path__default.resolve(config.rootDir, config.srcDir, config.entry);
@@ -303,34 +304,18 @@ async function createBuildEntryModule(config) {
303
304
  appImportParts.pop();
304
305
  const virtualEntry = `import { toNodeListener } from 'arri';
305
306
  import { listen } from 'listhen';
306
- import routes from './routes.js';
307
- import app from './${appImportParts.join(".")}.js';
308
-
309
- for (const route of routes) {
310
- app.rpc({
311
- name: route.id,
312
- method: route.route.method,
313
- params: route.route.params,
314
- response: route.route.response,
315
- handler: route.route.handler,
316
- postHandler: route.route.postHandler,
317
- });
318
- }
307
+ import app from './${OUT_APP_FILE}';
319
308
 
320
309
  void listen(toNodeListener(app.h3App), {
321
310
  port: process.env.PORT ?? ${config.port},
322
311
  public: true,
323
312
  });`;
324
313
  await fs__default.writeFile(
325
- path__default.resolve(
326
- config.rootDir,
327
- config.buildDir,
328
- DEFAULT_SERVER_ENTRY_FILE
329
- ),
314
+ path__default.resolve(config.rootDir, ".output", GEN_SERVER_ENTRY_FILE),
330
315
  virtualEntry
331
316
  );
332
317
  }
333
- async function createBuildCodegenModule(config) {
318
+ async function createCodegenEntryFile(config) {
334
319
  const appModule = path__default.resolve(config.rootDir, config.srcDir, config.entry);
335
320
  const appImportParts = path__default.relative(path__default.resolve(config.rootDir, config.srcDir), appModule).split(".");
336
321
  appImportParts.pop();
@@ -338,20 +323,7 @@ async function createBuildCodegenModule(config) {
338
323
  `
339
324
  import { writeFileSync } from 'node:fs';
340
325
  import path from 'pathe';
341
- import app from './${appImportParts.join(".")}.js';
342
- import routes from './routes.js';
343
-
344
- for (const route of routes) {
345
- app.rpc({
346
- name: route.id,
347
- method: route.route.method,
348
- path: route.route.path,
349
- params: route.route.params,
350
- response: route.route.response,
351
- handler: route.route.handler,
352
- postHandler: route.route.postHandler,
353
- });
354
- }
326
+ import app from './${OUT_APP_FILE}';
355
327
 
356
328
  const __dirname = new URL(".", import.meta.url).pathname;
357
329
  const def = app.getAppDefinition();
@@ -362,7 +334,7 @@ async function createBuildCodegenModule(config) {
362
334
  { tabWidth: 4, parser: "typescript" }
363
335
  );
364
336
  await fs__default.writeFile(
365
- path__default.resolve(config.rootDir, config.buildDir, DEFAULT_CODEGEN_FILE),
337
+ path__default.resolve(config.rootDir, ".output", OUT_CODEGEN),
366
338
  virtualModule
367
339
  );
368
340
  }
@@ -464,26 +436,26 @@ const dev = citty.defineCommand({
464
436
  await startDevServer(config$1);
465
437
  }
466
438
  });
467
- const startListener = (config, showQr = false) => listhen.listenAndWatch(
468
- path__default.resolve(config.rootDir, ".output", SERVER_ENTRY_OUTPUT),
469
- {
470
- public: true,
471
- port: config.port,
472
- logger: logger$1,
473
- qr: showQr
474
- }
475
- );
439
+ const startListener = (config, showQr = false) => listhen.listenAndWatch(path__default.resolve(config.rootDir, ".output", OUT_SERVER_ENTRY), {
440
+ public: true,
441
+ port: config.port,
442
+ logger: logger$1,
443
+ qr: showQr
444
+ });
476
445
  async function bundleFilesContext(config) {
446
+ const serverContent = `import app from './${OUT_APP_FILE}';
447
+
448
+ export default app.h3App;`;
449
+ await fs__default.writeFile(
450
+ path__default.resolve(config.rootDir, ".output", OUT_SERVER_ENTRY),
451
+ serverContent
452
+ );
477
453
  return await esbuild__namespace.context({
478
454
  ...config.esbuild,
479
455
  entryPoints: [
480
- path__default.resolve(
481
- config.rootDir,
482
- config.buildDir,
483
- DEFAULT_SERVER_ENTRY_FILE
484
- )
456
+ path__default.resolve(config.rootDir, config.buildDir, GEN_APP_FILE)
485
457
  ],
486
- outfile: path__default.resolve(config.rootDir, ".output", SERVER_ENTRY_OUTPUT),
458
+ outfile: path__default.resolve(config.rootDir, ".output", OUT_APP_FILE),
487
459
  format: "esm",
488
460
  bundle: true,
489
461
  sourcemap: true,
@@ -495,8 +467,11 @@ async function bundleFilesContext(config) {
495
467
  async function startDevServer(config) {
496
468
  await setupWorkingDir(config);
497
469
  let fileWatcher;
498
- await Promise.all([createRoutesModule(config), createEntryModule(config)]);
499
- await transpileFiles(config);
470
+ await Promise.all([
471
+ createEntryModule(config),
472
+ createAppWithRoutesModule(config),
473
+ transpileFiles(config)
474
+ ]);
500
475
  const bundleFiles = await bundleFilesContext(config);
501
476
  await bundleFiles.rebuild();
502
477
  const listener = await startListener(config, true);
@@ -534,7 +509,7 @@ async function startDevServer(config) {
534
509
  if (event === "add" || event === "addDir") {
535
510
  return;
536
511
  }
537
- await createRoutesModule(config);
512
+ await createAppWithRoutesModule(config);
538
513
  let bundleCreated = false;
539
514
  try {
540
515
  logger$1.log(
@@ -587,28 +562,11 @@ async function createEntryModule(config) {
587
562
  appImportParts.pop();
588
563
  const virtualEntry = `import { toNodeListener } from 'h3';
589
564
  import { listen } from 'listhen';
590
- import routes from './routes';
591
- import app from './${appImportParts.join(".")}';
592
-
593
- for (const route of routes) {
594
- app.rpc({
595
- name: route.id,
596
- method: route.route.method,
597
- path: route.route.path,
598
- params: route.route.params,
599
- response: route.route.response,
600
- handler: route.route.handler,
601
- postHandler: route.route.postHandler,
602
- });
603
- }
565
+ import app from './${GEN_APP_FILE}';
604
566
 
605
567
  export default app.h3App;`;
606
568
  await fs__default.writeFile(
607
- path__default.resolve(
608
- config.rootDir,
609
- config.buildDir,
610
- DEFAULT_SERVER_ENTRY_FILE
611
- ),
569
+ path__default.resolve(config.rootDir, config.buildDir, GEN_SERVER_ENTRY_FILE),
612
570
  virtualEntry
613
571
  );
614
572
  }