@effect-gql/cli 1.1.0 → 1.2.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/bin.cjs CHANGED
@@ -3,8 +3,10 @@
3
3
 
4
4
  var effect = require('effect');
5
5
  var core = require('@effect-gql/core');
6
- var fs = require('fs');
7
- var path = require('path');
6
+ var fs3 = require('fs');
7
+ var path2 = require('path');
8
+ var readline = require('readline');
9
+ var child_process = require('child_process');
8
10
 
9
11
  function _interopNamespace(e) {
10
12
  if (e && e.__esModule) return e;
@@ -24,8 +26,9 @@ function _interopNamespace(e) {
24
26
  return Object.freeze(n);
25
27
  }
26
28
 
27
- var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
28
- var path__namespace = /*#__PURE__*/_interopNamespace(path);
29
+ var fs3__namespace = /*#__PURE__*/_interopNamespace(fs3);
30
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
31
+ var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
29
32
 
30
33
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
31
34
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -34,8 +37,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
34
37
  throw Error('Dynamic require of "' + x + '" is not supported');
35
38
  });
36
39
  var loadSchema = (modulePath) => effect.Effect.gen(function* () {
37
- const absolutePath = path__namespace.resolve(process.cwd(), modulePath);
38
- if (!fs__namespace.existsSync(absolutePath)) {
40
+ const absolutePath = path2__namespace.resolve(process.cwd(), modulePath);
41
+ if (!fs3__namespace.existsSync(absolutePath)) {
39
42
  return yield* effect.Effect.fail(new Error(`File not found: ${absolutePath}`));
40
43
  }
41
44
  const module = yield* effect.Effect.tryPromise({
@@ -76,20 +79,20 @@ var run = (options) => effect.Effect.gen(function* () {
76
79
  const schema = yield* loadSchema(options.modulePath);
77
80
  const sdl = generateSDL(schema, { sort: options.sort });
78
81
  if (options.output) {
79
- const outputPath = path__namespace.resolve(process.cwd(), options.output);
80
- fs__namespace.writeFileSync(outputPath, sdl);
82
+ const outputPath = path2__namespace.resolve(process.cwd(), options.output);
83
+ fs3__namespace.writeFileSync(outputPath, sdl);
81
84
  yield* effect.Console.log(`Schema written to ${outputPath}`);
82
85
  } else {
83
86
  yield* effect.Console.log(sdl);
84
87
  }
85
88
  });
86
89
  var watch2 = (options) => effect.Effect.gen(function* () {
87
- const absolutePath = path__namespace.resolve(process.cwd(), options.modulePath);
88
- const dir = path__namespace.dirname(absolutePath);
90
+ const absolutePath = path2__namespace.resolve(process.cwd(), options.modulePath);
91
+ const dir = path2__namespace.dirname(absolutePath);
89
92
  yield* effect.Console.log(`Watching for changes in ${dir}...`);
90
93
  yield* run(options).pipe(effect.Effect.catchAll((error) => effect.Console.error(`Error: ${error.message}`)));
91
94
  yield* effect.Effect.async(() => {
92
- const watcher = fs__namespace.watch(dir, { recursive: true }, (_, filename) => {
95
+ const watcher = fs3__namespace.watch(dir, { recursive: true }, (_, filename) => {
93
96
  if (filename?.endsWith(".ts") || filename?.endsWith(".js")) {
94
97
  effect.Effect.runPromise(
95
98
  run(options).pipe(
@@ -179,6 +182,1123 @@ var runGenerateSchema = (args) => effect.Effect.gen(function* () {
179
182
  }
180
183
  });
181
184
 
185
+ // src/commands/create/types.ts
186
+ var SERVER_TYPES = ["node", "bun", "express", "web"];
187
+ var isValidServerType = (value) => SERVER_TYPES.includes(value);
188
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
189
+ var isValidPackageManager = (value) => PACKAGE_MANAGERS.includes(value);
190
+
191
+ // src/commands/create/args.ts
192
+ var parseArgs2 = (args) => {
193
+ const positional = [];
194
+ let serverType;
195
+ let directory = effect.Option.none();
196
+ let monorepo = effect.Option.none();
197
+ let skipInstall = false;
198
+ let packageManager = effect.Option.none();
199
+ let interactive = false;
200
+ for (let i = 0; i < args.length; i++) {
201
+ const arg = args[i];
202
+ if (arg === "-h" || arg === "--help") {
203
+ return { help: true };
204
+ } else if (arg === "-i" || arg === "--interactive") {
205
+ interactive = true;
206
+ } else if (arg === "-s" || arg === "--server-type") {
207
+ const value = args[++i];
208
+ if (!value) {
209
+ return { error: "Missing value for --server-type" };
210
+ }
211
+ if (!isValidServerType(value)) {
212
+ return {
213
+ error: `Invalid server type: ${value}. Must be one of: ${SERVER_TYPES.join(", ")}`
214
+ };
215
+ }
216
+ serverType = value;
217
+ } else if (arg === "-d" || arg === "--directory") {
218
+ const value = args[++i];
219
+ if (!value) {
220
+ return { error: "Missing value for --directory" };
221
+ }
222
+ directory = effect.Option.some(value);
223
+ } else if (arg === "--monorepo") {
224
+ monorepo = effect.Option.some(true);
225
+ } else if (arg === "--skip-install") {
226
+ skipInstall = true;
227
+ } else if (arg === "--package-manager") {
228
+ const value = args[++i];
229
+ if (!value) {
230
+ return { error: "Missing value for --package-manager" };
231
+ }
232
+ if (!isValidPackageManager(value)) {
233
+ return { error: `Invalid package manager: ${value}. Must be one of: npm, pnpm, yarn, bun` };
234
+ }
235
+ packageManager = effect.Option.some(value);
236
+ } else if (!arg.startsWith("-")) {
237
+ positional.push(arg);
238
+ } else {
239
+ return { error: `Unknown option: ${arg}` };
240
+ }
241
+ }
242
+ if (interactive) {
243
+ return { interactive: true };
244
+ }
245
+ if (args.length === 0) {
246
+ return { interactive: true };
247
+ }
248
+ if (positional.length === 0) {
249
+ return {
250
+ error: "Missing project name. Use --interactive or provide: effect-gql create <name> --server-type <type>"
251
+ };
252
+ }
253
+ if (!serverType) {
254
+ return { error: "Missing --server-type. Must be one of: " + SERVER_TYPES.join(", ") };
255
+ }
256
+ const options = {
257
+ name: positional[0],
258
+ serverType,
259
+ directory,
260
+ monorepo,
261
+ skipInstall,
262
+ packageManager
263
+ };
264
+ return { options };
265
+ };
266
+
267
+ // src/commands/create/templates/package-json.ts
268
+ var VERSIONS = {
269
+ core: "^1.1.0",
270
+ node: "^1.1.0",
271
+ bun: "^1.1.0",
272
+ express: "^1.1.0",
273
+ web: "^1.1.0",
274
+ effect: "^3.19.0",
275
+ platform: "^0.94.0",
276
+ platformNode: "^0.104.0",
277
+ platformBun: "^0.87.0",
278
+ graphql: "^16.0.0",
279
+ expressLib: "^5.0.0",
280
+ tsx: "^4.19.0",
281
+ typescript: "^5.0.0"
282
+ };
283
+ var getDependencies = (serverType, isMonorepo) => {
284
+ const toVersion = (pkg, version) => isMonorepo && pkg.startsWith("@effect-gql/") ? "workspace:*" : version;
285
+ const baseDeps = {
286
+ "@effect-gql/core": toVersion("@effect-gql/core", VERSIONS.core),
287
+ "@effect/platform": VERSIONS.platform,
288
+ effect: VERSIONS.effect,
289
+ graphql: VERSIONS.graphql
290
+ };
291
+ const baseDevDeps = {
292
+ tsx: VERSIONS.tsx,
293
+ typescript: VERSIONS.typescript
294
+ };
295
+ switch (serverType) {
296
+ case "node":
297
+ return {
298
+ dependencies: {
299
+ ...baseDeps,
300
+ "@effect-gql/node": toVersion("@effect-gql/node", VERSIONS.node),
301
+ "@effect/platform-node": VERSIONS.platformNode
302
+ },
303
+ devDependencies: baseDevDeps
304
+ };
305
+ case "bun":
306
+ return {
307
+ dependencies: {
308
+ ...baseDeps,
309
+ "@effect-gql/bun": toVersion("@effect-gql/bun", VERSIONS.bun),
310
+ "@effect/platform-bun": VERSIONS.platformBun
311
+ },
312
+ devDependencies: {
313
+ typescript: VERSIONS.typescript
314
+ // Bun has built-in TypeScript support, no tsx needed
315
+ }
316
+ };
317
+ case "express":
318
+ return {
319
+ dependencies: {
320
+ ...baseDeps,
321
+ "@effect-gql/express": toVersion("@effect-gql/express", VERSIONS.express),
322
+ express: VERSIONS.expressLib
323
+ },
324
+ devDependencies: {
325
+ ...baseDevDeps,
326
+ "@types/express": "^5.0.0"
327
+ }
328
+ };
329
+ case "web":
330
+ return {
331
+ dependencies: {
332
+ ...baseDeps,
333
+ "@effect-gql/web": toVersion("@effect-gql/web", VERSIONS.web)
334
+ },
335
+ devDependencies: baseDevDeps
336
+ };
337
+ }
338
+ };
339
+ var getScripts = (serverType) => {
340
+ switch (serverType) {
341
+ case "node":
342
+ return {
343
+ start: "tsx src/index.ts",
344
+ dev: "tsx watch src/index.ts",
345
+ build: "tsc",
346
+ typecheck: "tsc --noEmit"
347
+ };
348
+ case "bun":
349
+ return {
350
+ start: "bun run src/index.ts",
351
+ dev: "bun --watch src/index.ts",
352
+ build: "bun build src/index.ts --outdir dist --target bun",
353
+ typecheck: "tsc --noEmit"
354
+ };
355
+ case "express":
356
+ return {
357
+ start: "tsx src/index.ts",
358
+ dev: "tsx watch src/index.ts",
359
+ build: "tsc",
360
+ typecheck: "tsc --noEmit"
361
+ };
362
+ case "web":
363
+ return {
364
+ start: "tsx src/index.ts",
365
+ dev: "tsx watch src/index.ts",
366
+ build: "tsc",
367
+ typecheck: "tsc --noEmit"
368
+ // Users will typically add wrangler/deno commands as needed
369
+ };
370
+ }
371
+ };
372
+ var generatePackageJson = (ctx) => {
373
+ const deps = getDependencies(ctx.serverType, ctx.isMonorepo);
374
+ const pkg = {
375
+ name: ctx.name,
376
+ version: "0.0.1",
377
+ private: true,
378
+ type: "module",
379
+ scripts: getScripts(ctx.serverType),
380
+ dependencies: deps.dependencies,
381
+ devDependencies: deps.devDependencies
382
+ };
383
+ return JSON.stringify(pkg, null, 2) + "\n";
384
+ };
385
+
386
+ // src/commands/create/templates/tsconfig.ts
387
+ var generateTsConfig = (ctx) => {
388
+ const config = ctx.isMonorepo ? {
389
+ // In a monorepo, extend from root tsconfig
390
+ extends: "../../tsconfig.json",
391
+ compilerOptions: {
392
+ outDir: "./dist",
393
+ rootDir: "./src",
394
+ noEmit: true
395
+ },
396
+ include: ["src/**/*"],
397
+ exclude: ["node_modules", "dist"]
398
+ } : {
399
+ // Standalone project config
400
+ compilerOptions: {
401
+ target: "ES2022",
402
+ module: "NodeNext",
403
+ moduleResolution: "NodeNext",
404
+ lib: ["ES2022"],
405
+ outDir: "./dist",
406
+ rootDir: "./src",
407
+ strict: true,
408
+ esModuleInterop: true,
409
+ skipLibCheck: true,
410
+ forceConsistentCasingInFileNames: true,
411
+ declaration: true,
412
+ declarationMap: true,
413
+ sourceMap: true,
414
+ noEmit: true,
415
+ // Effect requires these for decorators
416
+ experimentalDecorators: true,
417
+ emitDecoratorMetadata: true
418
+ },
419
+ include: ["src/**/*"],
420
+ exclude: ["node_modules", "dist"]
421
+ };
422
+ return JSON.stringify(config, null, 2) + "\n";
423
+ };
424
+
425
+ // src/commands/create/templates/server.ts
426
+ var generateNodeServer = (ctx) => `/**
427
+ * ${ctx.name} - GraphQL Server
428
+ *
429
+ * A GraphQL server built with Effect GQL and Node.js.
430
+ */
431
+
432
+ import { Effect, Layer } from "effect"
433
+ import * as S from "effect/Schema"
434
+ import { HttpRouter, HttpServerResponse } from "@effect/platform"
435
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
436
+ import { serve } from "@effect-gql/node"
437
+
438
+ // =============================================================================
439
+ // Domain Models
440
+ // =============================================================================
441
+
442
+ /**
443
+ * Define your types with Effect Schema.
444
+ * This single definition is used for both TypeScript types AND GraphQL types.
445
+ */
446
+ const User = S.Struct({
447
+ id: S.String,
448
+ name: S.String,
449
+ email: S.String,
450
+ })
451
+
452
+ type User = S.Schema.Type<typeof User>
453
+
454
+ // =============================================================================
455
+ // In-Memory Data Store (replace with your database)
456
+ // =============================================================================
457
+
458
+ const users: User[] = [
459
+ { id: "1", name: "Alice", email: "alice@example.com" },
460
+ { id: "2", name: "Bob", email: "bob@example.com" },
461
+ ]
462
+
463
+ // =============================================================================
464
+ // GraphQL Schema
465
+ // =============================================================================
466
+
467
+ const schema = GraphQLSchemaBuilder.empty
468
+ .pipe(
469
+ // Simple query
470
+ query("hello", {
471
+ type: S.String,
472
+ description: "Returns a friendly greeting",
473
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
474
+ }),
475
+
476
+ // Query with arguments
477
+ query("user", {
478
+ args: S.Struct({ id: S.String }),
479
+ type: S.NullOr(User),
480
+ description: "Get a user by ID",
481
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
482
+ }),
483
+
484
+ // Query that returns a list
485
+ query("users", {
486
+ type: S.Array(User),
487
+ description: "Get all users",
488
+ resolve: () => Effect.succeed(users),
489
+ }),
490
+
491
+ // Mutation
492
+ mutation("createUser", {
493
+ args: S.Struct({
494
+ name: S.String,
495
+ email: S.String,
496
+ }),
497
+ type: User,
498
+ description: "Create a new user",
499
+ resolve: (args) =>
500
+ Effect.sync(() => {
501
+ const newUser: User = {
502
+ id: String(users.length + 1),
503
+ name: args.name,
504
+ email: args.email,
505
+ }
506
+ users.push(newUser)
507
+ return newUser
508
+ }),
509
+ })
510
+ )
511
+ .buildSchema()
512
+
513
+ // =============================================================================
514
+ // HTTP Router
515
+ // =============================================================================
516
+
517
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
518
+ path: "/graphql",
519
+ graphiql: {
520
+ path: "/graphiql",
521
+ endpoint: "/graphql",
522
+ },
523
+ })
524
+
525
+ const router = HttpRouter.empty.pipe(
526
+ HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })),
527
+ HttpRouter.concat(graphqlRouter)
528
+ )
529
+
530
+ // =============================================================================
531
+ // Server Startup
532
+ // =============================================================================
533
+
534
+ serve(router, Layer.empty, {
535
+ port: 4000,
536
+ onStart: (url: string) => {
537
+ console.log(\`Server ready at \${url}\`)
538
+ console.log(\`GraphQL endpoint: \${url}/graphql\`)
539
+ console.log(\`GraphiQL playground: \${url}/graphiql\`)
540
+ console.log(\`Health check: \${url}/health\`)
541
+ },
542
+ })
543
+ `;
544
+ var generateBunServer = (ctx) => `/**
545
+ * ${ctx.name} - GraphQL Server
546
+ *
547
+ * A GraphQL server built with Effect GQL and Bun.
548
+ */
549
+
550
+ import { Effect, Layer } from "effect"
551
+ import * as S from "effect/Schema"
552
+ import { HttpRouter, HttpServerResponse } from "@effect/platform"
553
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
554
+ import { serve } from "@effect-gql/bun"
555
+
556
+ // =============================================================================
557
+ // Domain Models
558
+ // =============================================================================
559
+
560
+ const User = S.Struct({
561
+ id: S.String,
562
+ name: S.String,
563
+ email: S.String,
564
+ })
565
+
566
+ type User = S.Schema.Type<typeof User>
567
+
568
+ // =============================================================================
569
+ // In-Memory Data Store (replace with your database)
570
+ // =============================================================================
571
+
572
+ const users: User[] = [
573
+ { id: "1", name: "Alice", email: "alice@example.com" },
574
+ { id: "2", name: "Bob", email: "bob@example.com" },
575
+ ]
576
+
577
+ // =============================================================================
578
+ // GraphQL Schema
579
+ // =============================================================================
580
+
581
+ const schema = GraphQLSchemaBuilder.empty
582
+ .pipe(
583
+ query("hello", {
584
+ type: S.String,
585
+ description: "Returns a friendly greeting",
586
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
587
+ }),
588
+
589
+ query("user", {
590
+ args: S.Struct({ id: S.String }),
591
+ type: S.NullOr(User),
592
+ description: "Get a user by ID",
593
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
594
+ }),
595
+
596
+ query("users", {
597
+ type: S.Array(User),
598
+ description: "Get all users",
599
+ resolve: () => Effect.succeed(users),
600
+ }),
601
+
602
+ mutation("createUser", {
603
+ args: S.Struct({
604
+ name: S.String,
605
+ email: S.String,
606
+ }),
607
+ type: User,
608
+ description: "Create a new user",
609
+ resolve: (args) =>
610
+ Effect.sync(() => {
611
+ const newUser: User = {
612
+ id: String(users.length + 1),
613
+ name: args.name,
614
+ email: args.email,
615
+ }
616
+ users.push(newUser)
617
+ return newUser
618
+ }),
619
+ })
620
+ )
621
+ .buildSchema()
622
+
623
+ // =============================================================================
624
+ // HTTP Router
625
+ // =============================================================================
626
+
627
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
628
+ path: "/graphql",
629
+ graphiql: {
630
+ path: "/graphiql",
631
+ endpoint: "/graphql",
632
+ },
633
+ })
634
+
635
+ const router = HttpRouter.empty.pipe(
636
+ HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })),
637
+ HttpRouter.concat(graphqlRouter)
638
+ )
639
+
640
+ // =============================================================================
641
+ // Server Startup
642
+ // =============================================================================
643
+
644
+ serve(router, Layer.empty, {
645
+ port: 4000,
646
+ onStart: (url: string) => {
647
+ console.log(\`Server ready at \${url}\`)
648
+ console.log(\`GraphQL endpoint: \${url}/graphql\`)
649
+ console.log(\`GraphiQL playground: \${url}/graphiql\`)
650
+ console.log(\`Health check: \${url}/health\`)
651
+ },
652
+ })
653
+ `;
654
+ var generateExpressServer = (ctx) => `/**
655
+ * ${ctx.name} - GraphQL Server
656
+ *
657
+ * A GraphQL server built with Effect GQL and Express.
658
+ */
659
+
660
+ import express from "express"
661
+ import { Effect, Layer } from "effect"
662
+ import * as S from "effect/Schema"
663
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
664
+ import { toMiddleware } from "@effect-gql/express"
665
+
666
+ // =============================================================================
667
+ // Domain Models
668
+ // =============================================================================
669
+
670
+ const User = S.Struct({
671
+ id: S.String,
672
+ name: S.String,
673
+ email: S.String,
674
+ })
675
+
676
+ type User = S.Schema.Type<typeof User>
677
+
678
+ // =============================================================================
679
+ // In-Memory Data Store (replace with your database)
680
+ // =============================================================================
681
+
682
+ const users: User[] = [
683
+ { id: "1", name: "Alice", email: "alice@example.com" },
684
+ { id: "2", name: "Bob", email: "bob@example.com" },
685
+ ]
686
+
687
+ // =============================================================================
688
+ // GraphQL Schema
689
+ // =============================================================================
690
+
691
+ const schema = GraphQLSchemaBuilder.empty
692
+ .pipe(
693
+ query("hello", {
694
+ type: S.String,
695
+ description: "Returns a friendly greeting",
696
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
697
+ }),
698
+
699
+ query("user", {
700
+ args: S.Struct({ id: S.String }),
701
+ type: S.NullOr(User),
702
+ description: "Get a user by ID",
703
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
704
+ }),
705
+
706
+ query("users", {
707
+ type: S.Array(User),
708
+ description: "Get all users",
709
+ resolve: () => Effect.succeed(users),
710
+ }),
711
+
712
+ mutation("createUser", {
713
+ args: S.Struct({
714
+ name: S.String,
715
+ email: S.String,
716
+ }),
717
+ type: User,
718
+ description: "Create a new user",
719
+ resolve: (args) =>
720
+ Effect.sync(() => {
721
+ const newUser: User = {
722
+ id: String(users.length + 1),
723
+ name: args.name,
724
+ email: args.email,
725
+ }
726
+ users.push(newUser)
727
+ return newUser
728
+ }),
729
+ })
730
+ )
731
+ .buildSchema()
732
+
733
+ // =============================================================================
734
+ // GraphQL Router
735
+ // =============================================================================
736
+
737
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
738
+ path: "/graphql",
739
+ graphiql: {
740
+ path: "/graphiql",
741
+ endpoint: "/graphql",
742
+ },
743
+ })
744
+
745
+ // =============================================================================
746
+ // Express App
747
+ // =============================================================================
748
+
749
+ const app = express()
750
+ app.use(express.json())
751
+
752
+ // Health check
753
+ app.get("/health", (_, res) => {
754
+ res.json({ status: "ok" })
755
+ })
756
+
757
+ // Mount GraphQL middleware
758
+ app.use(toMiddleware(graphqlRouter, Layer.empty))
759
+
760
+ // =============================================================================
761
+ // Server Startup
762
+ // =============================================================================
763
+
764
+ const port = process.env.PORT || 4000
765
+
766
+ app.listen(port, () => {
767
+ console.log(\`Server ready at http://localhost:\${port}\`)
768
+ console.log(\`GraphQL endpoint: http://localhost:\${port}/graphql\`)
769
+ console.log(\`GraphiQL playground: http://localhost:\${port}/graphiql\`)
770
+ console.log(\`Health check: http://localhost:\${port}/health\`)
771
+ })
772
+ `;
773
+ var generateWebHandler = (ctx) => `/**
774
+ * ${ctx.name} - GraphQL Server
775
+ *
776
+ * A GraphQL server built with Effect GQL using Web standard APIs.
777
+ * Compatible with Cloudflare Workers, Deno, and other WASM runtimes.
778
+ */
779
+
780
+ import { Effect, Layer } from "effect"
781
+ import * as S from "effect/Schema"
782
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
783
+ import { toHandler } from "@effect-gql/web"
784
+
785
+ // =============================================================================
786
+ // Domain Models
787
+ // =============================================================================
788
+
789
+ const User = S.Struct({
790
+ id: S.String,
791
+ name: S.String,
792
+ email: S.String,
793
+ })
794
+
795
+ type User = S.Schema.Type<typeof User>
796
+
797
+ // =============================================================================
798
+ // In-Memory Data Store (replace with your database)
799
+ // =============================================================================
800
+
801
+ const users: User[] = [
802
+ { id: "1", name: "Alice", email: "alice@example.com" },
803
+ { id: "2", name: "Bob", email: "bob@example.com" },
804
+ ]
805
+
806
+ // =============================================================================
807
+ // GraphQL Schema
808
+ // =============================================================================
809
+
810
+ const schema = GraphQLSchemaBuilder.empty
811
+ .pipe(
812
+ query("hello", {
813
+ type: S.String,
814
+ description: "Returns a friendly greeting",
815
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
816
+ }),
817
+
818
+ query("user", {
819
+ args: S.Struct({ id: S.String }),
820
+ type: S.NullOr(User),
821
+ description: "Get a user by ID",
822
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
823
+ }),
824
+
825
+ query("users", {
826
+ type: S.Array(User),
827
+ description: "Get all users",
828
+ resolve: () => Effect.succeed(users),
829
+ }),
830
+
831
+ mutation("createUser", {
832
+ args: S.Struct({
833
+ name: S.String,
834
+ email: S.String,
835
+ }),
836
+ type: User,
837
+ description: "Create a new user",
838
+ resolve: (args) =>
839
+ Effect.sync(() => {
840
+ const newUser: User = {
841
+ id: String(users.length + 1),
842
+ name: args.name,
843
+ email: args.email,
844
+ }
845
+ users.push(newUser)
846
+ return newUser
847
+ }),
848
+ })
849
+ )
850
+ .buildSchema()
851
+
852
+ // =============================================================================
853
+ // GraphQL Router
854
+ // =============================================================================
855
+
856
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
857
+ path: "/graphql",
858
+ graphiql: {
859
+ path: "/graphiql",
860
+ endpoint: "/graphql",
861
+ },
862
+ })
863
+
864
+ // =============================================================================
865
+ // Web Handler
866
+ // =============================================================================
867
+
868
+ const { handler } = toHandler(graphqlRouter, Layer.empty)
869
+
870
+ /**
871
+ * Export for Cloudflare Workers
872
+ */
873
+ export default {
874
+ async fetch(request: Request): Promise<Response> {
875
+ const url = new URL(request.url)
876
+
877
+ // Health check
878
+ if (url.pathname === "/health") {
879
+ return new Response(JSON.stringify({ status: "ok" }), {
880
+ headers: { "Content-Type": "application/json" },
881
+ })
882
+ }
883
+
884
+ return handler(request)
885
+ },
886
+ }
887
+
888
+ /**
889
+ * For local development with Deno or other runtimes, you can use:
890
+ *
891
+ * Deno.serve((request) => {
892
+ * const url = new URL(request.url)
893
+ * if (url.pathname === "/health") {
894
+ * return new Response(JSON.stringify({ status: "ok" }), {
895
+ * headers: { "Content-Type": "application/json" },
896
+ * })
897
+ * }
898
+ * return handler(request)
899
+ * })
900
+ */
901
+ `;
902
+ var generateServerTemplate = (ctx) => {
903
+ switch (ctx.serverType) {
904
+ case "node":
905
+ return generateNodeServer(ctx);
906
+ case "bun":
907
+ return generateBunServer(ctx);
908
+ case "express":
909
+ return generateExpressServer(ctx);
910
+ case "web":
911
+ return generateWebHandler(ctx);
912
+ }
913
+ };
914
+
915
+ // src/commands/create/templates/index.ts
916
+ var generateGitignore = () => `# Dependencies
917
+ node_modules/
918
+
919
+ # Build output
920
+ dist/
921
+ *.tsbuildinfo
922
+
923
+ # IDE
924
+ .idea/
925
+ .vscode/
926
+ *.swp
927
+ *.swo
928
+
929
+ # OS
930
+ .DS_Store
931
+ Thumbs.db
932
+
933
+ # Environment
934
+ .env
935
+ .env.local
936
+ .env.*.local
937
+
938
+ # Logs
939
+ *.log
940
+ npm-debug.log*
941
+ yarn-debug.log*
942
+ yarn-error.log*
943
+
944
+ # Testing
945
+ coverage/
946
+ `;
947
+ var generateProject = (ctx) => [
948
+ {
949
+ path: "package.json",
950
+ content: generatePackageJson(ctx)
951
+ },
952
+ {
953
+ path: "tsconfig.json",
954
+ content: generateTsConfig(ctx)
955
+ },
956
+ {
957
+ path: "src/index.ts",
958
+ content: generateServerTemplate(ctx)
959
+ },
960
+ {
961
+ path: ".gitignore",
962
+ content: generateGitignore()
963
+ }
964
+ ];
965
+ var fileExists = (filePath) => effect.Effect.sync(() => fs3__namespace.existsSync(filePath));
966
+ var readFile = (filePath) => effect.Effect.try({
967
+ try: () => fs3__namespace.readFileSync(filePath, "utf-8"),
968
+ catch: (error) => new Error(`Failed to read ${filePath}: ${error}`)
969
+ });
970
+ var detectPackageManager = (dir) => effect.Effect.gen(function* () {
971
+ if (yield* fileExists(path2__namespace.join(dir, "pnpm-lock.yaml"))) return "pnpm";
972
+ if (yield* fileExists(path2__namespace.join(dir, "bun.lockb"))) return "bun";
973
+ if (yield* fileExists(path2__namespace.join(dir, "yarn.lock"))) return "yarn";
974
+ return "npm";
975
+ });
976
+ var detectMonorepo = (targetDir) => effect.Effect.gen(function* () {
977
+ let currentDir = path2__namespace.dirname(path2__namespace.resolve(targetDir));
978
+ const root = path2__namespace.parse(currentDir).root;
979
+ while (currentDir !== root) {
980
+ const pnpmWorkspacePath = path2__namespace.join(currentDir, "pnpm-workspace.yaml");
981
+ if (yield* fileExists(pnpmWorkspacePath)) {
982
+ return {
983
+ isMonorepo: true,
984
+ packageManager: effect.Option.some("pnpm"),
985
+ workspacesRoot: effect.Option.some(currentDir)
986
+ };
987
+ }
988
+ const pkgPath = path2__namespace.join(currentDir, "package.json");
989
+ if (yield* fileExists(pkgPath)) {
990
+ const content = yield* readFile(pkgPath).pipe(effect.Effect.catchAll(() => effect.Effect.succeed("")));
991
+ if (content) {
992
+ try {
993
+ const pkg = JSON.parse(content);
994
+ if (pkg.workspaces) {
995
+ const pm = yield* detectPackageManager(currentDir);
996
+ return {
997
+ isMonorepo: true,
998
+ packageManager: effect.Option.some(pm),
999
+ workspacesRoot: effect.Option.some(currentDir)
1000
+ };
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ }
1006
+ const turboPath = path2__namespace.join(currentDir, "turbo.json");
1007
+ if (yield* fileExists(turboPath)) {
1008
+ const pm = yield* detectPackageManager(currentDir);
1009
+ return {
1010
+ isMonorepo: true,
1011
+ packageManager: effect.Option.some(pm),
1012
+ workspacesRoot: effect.Option.some(currentDir)
1013
+ };
1014
+ }
1015
+ currentDir = path2__namespace.dirname(currentDir);
1016
+ }
1017
+ return {
1018
+ isMonorepo: false,
1019
+ packageManager: effect.Option.none(),
1020
+ workspacesRoot: effect.Option.none()
1021
+ };
1022
+ });
1023
+ var getInstallCommand = (pm) => {
1024
+ switch (pm) {
1025
+ case "npm":
1026
+ return "npm install";
1027
+ case "pnpm":
1028
+ return "pnpm install";
1029
+ case "yarn":
1030
+ return "yarn";
1031
+ case "bun":
1032
+ return "bun install";
1033
+ }
1034
+ };
1035
+ var getRunPrefix = (pm) => {
1036
+ switch (pm) {
1037
+ case "npm":
1038
+ return "npm run";
1039
+ case "pnpm":
1040
+ return "pnpm";
1041
+ case "yarn":
1042
+ return "yarn";
1043
+ case "bun":
1044
+ return "bun run";
1045
+ }
1046
+ };
1047
+
1048
+ // src/commands/create/scaffolder.ts
1049
+ var validateDirectory = (targetDir) => effect.Effect.try({
1050
+ try: () => {
1051
+ if (fs3__namespace.existsSync(targetDir)) {
1052
+ const files = fs3__namespace.readdirSync(targetDir);
1053
+ if (files.length > 0) {
1054
+ throw new Error(`Directory ${targetDir} is not empty`);
1055
+ }
1056
+ }
1057
+ },
1058
+ catch: (error) => error instanceof Error ? error : new Error(`Failed to validate directory: ${error}`)
1059
+ });
1060
+ var mkdirp = (dir) => effect.Effect.try({
1061
+ try: () => fs3__namespace.mkdirSync(dir, { recursive: true }),
1062
+ catch: (error) => new Error(`Failed to create directory ${dir}: ${error}`)
1063
+ });
1064
+ var writeFile = (filePath, content) => effect.Effect.gen(function* () {
1065
+ yield* mkdirp(path2__namespace.dirname(filePath));
1066
+ yield* effect.Effect.try({
1067
+ try: () => fs3__namespace.writeFileSync(filePath, content, "utf-8"),
1068
+ catch: (error) => new Error(`Failed to write ${filePath}: ${error}`)
1069
+ });
1070
+ });
1071
+ var runCommand = (command, args, cwd) => effect.Effect.async((resume) => {
1072
+ const child = child_process.spawn(command, args, {
1073
+ cwd,
1074
+ stdio: "inherit",
1075
+ shell: true
1076
+ });
1077
+ child.on("close", (code) => {
1078
+ if (code === 0) {
1079
+ resume(effect.Effect.succeed(void 0));
1080
+ } else {
1081
+ resume(effect.Effect.fail(new Error(`Command failed with exit code ${code}`)));
1082
+ }
1083
+ });
1084
+ child.on("error", (error) => {
1085
+ resume(effect.Effect.fail(new Error(`Failed to run command: ${error.message}`)));
1086
+ });
1087
+ return effect.Effect.sync(() => {
1088
+ child.kill();
1089
+ });
1090
+ });
1091
+ var installDependencies = (targetDir, packageManager) => effect.Effect.gen(function* () {
1092
+ yield* effect.Console.log("");
1093
+ yield* effect.Console.log("Installing dependencies...");
1094
+ const installCmd = getInstallCommand(packageManager);
1095
+ const [cmd, ...args] = installCmd.split(" ");
1096
+ yield* runCommand(cmd, args, targetDir);
1097
+ yield* effect.Console.log("Dependencies installed successfully!");
1098
+ });
1099
+ var printSuccessMessage = (ctx, targetDir) => effect.Effect.gen(function* () {
1100
+ const relativePath = path2__namespace.relative(process.cwd(), targetDir) || ".";
1101
+ const runPrefix = getRunPrefix(ctx.packageManager);
1102
+ yield* effect.Console.log("");
1103
+ yield* effect.Console.log(`Successfully created ${ctx.name}!`);
1104
+ yield* effect.Console.log("");
1105
+ yield* effect.Console.log("Next steps:");
1106
+ yield* effect.Console.log(` cd ${relativePath}`);
1107
+ yield* effect.Console.log(` ${runPrefix} dev`);
1108
+ yield* effect.Console.log("");
1109
+ yield* effect.Console.log("Your GraphQL server will be available at:");
1110
+ yield* effect.Console.log(" http://localhost:4000/graphql");
1111
+ yield* effect.Console.log(" http://localhost:4000/graphiql (playground)");
1112
+ yield* effect.Console.log("");
1113
+ });
1114
+ var scaffold = (options) => effect.Effect.gen(function* () {
1115
+ const targetDir = effect.pipe(
1116
+ options.directory,
1117
+ effect.Option.map((dir) => path2__namespace.resolve(process.cwd(), dir)),
1118
+ effect.Option.getOrElse(() => path2__namespace.resolve(process.cwd(), options.name))
1119
+ );
1120
+ const monorepoInfo = yield* detectMonorepo(targetDir);
1121
+ const isMonorepo = effect.pipe(
1122
+ options.monorepo,
1123
+ effect.Option.getOrElse(() => monorepoInfo.isMonorepo)
1124
+ );
1125
+ const packageManager = yield* effect.pipe(
1126
+ options.packageManager,
1127
+ effect.Option.orElse(() => monorepoInfo.packageManager),
1128
+ effect.Option.match({
1129
+ onNone: () => detectPackageManager(process.cwd()),
1130
+ onSome: (pm) => effect.Effect.succeed(pm)
1131
+ })
1132
+ );
1133
+ const ctx = {
1134
+ name: options.name,
1135
+ serverType: options.serverType,
1136
+ isMonorepo,
1137
+ packageManager
1138
+ };
1139
+ yield* effect.Console.log("");
1140
+ yield* effect.Console.log(`Creating ${ctx.name} with ${ctx.serverType} server...`);
1141
+ yield* validateDirectory(targetDir);
1142
+ const files = generateProject(ctx);
1143
+ for (const file of files) {
1144
+ const filePath = path2__namespace.join(targetDir, file.path);
1145
+ yield* writeFile(filePath, file.content);
1146
+ yield* effect.Console.log(` Created ${file.path}`);
1147
+ }
1148
+ if (!options.skipInstall) {
1149
+ yield* installDependencies(targetDir, packageManager).pipe(
1150
+ effect.Effect.catchAll(
1151
+ (error) => effect.Console.log(`Warning: Failed to install dependencies: ${error.message}`).pipe(
1152
+ effect.Effect.andThen(
1153
+ effect.Console.log(
1154
+ "You can install them manually by running: " + getInstallCommand(packageManager)
1155
+ )
1156
+ )
1157
+ )
1158
+ )
1159
+ );
1160
+ } else {
1161
+ yield* effect.Console.log("");
1162
+ yield* effect.Console.log(
1163
+ `Skipping dependency installation. Run '${getInstallCommand(packageManager)}' to install.`
1164
+ );
1165
+ }
1166
+ yield* printSuccessMessage(ctx, targetDir);
1167
+ });
1168
+
1169
+ // src/commands/create/index.ts
1170
+ var printCreateHelp = () => {
1171
+ console.log(`
1172
+ Usage: effect-gql create <name> --server-type <type> [options]
1173
+
1174
+ Create a new Effect GraphQL project.
1175
+
1176
+ Arguments:
1177
+ name Package/project name
1178
+
1179
+ Required Options:
1180
+ -s, --server-type Server type: node, bun, express, web
1181
+
1182
+ Optional Options:
1183
+ -d, --directory Target directory (default: ./<name>)
1184
+ --monorepo Create as workspace package (auto-detected)
1185
+ --skip-install Skip dependency installation
1186
+ --package-manager Package manager: npm, pnpm, yarn, bun
1187
+ -i, --interactive Interactive mode (prompts for options)
1188
+ -h, --help Show this help message
1189
+
1190
+ Server Types:
1191
+ node Node.js server using @effect-gql/node
1192
+ Best for: General Node.js deployments
1193
+
1194
+ bun Bun server using @effect-gql/bun
1195
+ Best for: Bun runtime with native WebSocket support
1196
+
1197
+ express Express middleware using @effect-gql/express
1198
+ Best for: Integrating into existing Express apps
1199
+
1200
+ web Web standard handler using @effect-gql/web
1201
+ Best for: Cloudflare Workers, Deno, edge runtimes
1202
+
1203
+ Examples:
1204
+ effect-gql create my-api --server-type node
1205
+ effect-gql create my-api -s bun
1206
+ effect-gql create my-api -s express -d ./packages/api --monorepo
1207
+ effect-gql create my-api -s web --skip-install
1208
+ effect-gql create --interactive
1209
+ `);
1210
+ };
1211
+ var prompt = (question) => effect.Effect.async((resume) => {
1212
+ const rl = readline__namespace.createInterface({
1213
+ input: process.stdin,
1214
+ output: process.stdout
1215
+ });
1216
+ rl.question(question, (answer) => {
1217
+ rl.close();
1218
+ resume(effect.Effect.succeed(answer.trim()));
1219
+ });
1220
+ rl.on("error", (error) => {
1221
+ rl.close();
1222
+ resume(effect.Effect.fail(new Error(`Input error: ${error.message}`)));
1223
+ });
1224
+ return effect.Effect.sync(() => rl.close());
1225
+ });
1226
+ var selectPrompt = (question, options) => effect.Effect.gen(function* () {
1227
+ yield* effect.Console.log(question);
1228
+ options.forEach((opt, i) => {
1229
+ console.log(` ${i + 1}) ${opt.label}`);
1230
+ });
1231
+ const answer = yield* prompt("Enter number: ");
1232
+ const index = parseInt(answer, 10) - 1;
1233
+ if (isNaN(index) || index < 0 || index >= options.length) {
1234
+ yield* effect.Console.log(`Invalid selection. Please enter 1-${options.length}.`);
1235
+ return yield* selectPrompt(question, options);
1236
+ }
1237
+ return options[index].value;
1238
+ });
1239
+ var confirmPrompt = (question, defaultValue) => effect.Effect.gen(function* () {
1240
+ const hint = "[y/N]";
1241
+ const answer = yield* prompt(`${question} ${hint} `);
1242
+ if (answer === "") return defaultValue;
1243
+ const lower = answer.toLowerCase();
1244
+ if (lower === "y" || lower === "yes") return true;
1245
+ if (lower === "n" || lower === "no") return false;
1246
+ yield* effect.Console.log("Please answer 'y' or 'n'.");
1247
+ return yield* confirmPrompt(question, defaultValue);
1248
+ });
1249
+ var runInteractive = () => effect.Effect.gen(function* () {
1250
+ yield* effect.Console.log("");
1251
+ yield* effect.Console.log("Create a new Effect GraphQL project");
1252
+ yield* effect.Console.log("====================================");
1253
+ yield* effect.Console.log("");
1254
+ const name = yield* prompt("Project name: ");
1255
+ if (!name) {
1256
+ return yield* effect.Effect.fail(new Error("Project name is required"));
1257
+ }
1258
+ if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) {
1259
+ yield* effect.Console.log("Warning: Name may not be a valid npm package name.");
1260
+ }
1261
+ const serverType = yield* selectPrompt("Select server type:", [
1262
+ { label: "Node.js (@effect-gql/node)", value: "node" },
1263
+ { label: "Bun (@effect-gql/bun)", value: "bun" },
1264
+ { label: "Express (@effect-gql/express)", value: "express" },
1265
+ { label: "Web/Workers (@effect-gql/web)", value: "web" }
1266
+ ]);
1267
+ const monorepo = yield* confirmPrompt("Create as monorepo workspace package?", false);
1268
+ const packageManager = monorepo ? effect.Option.some(
1269
+ yield* selectPrompt("Select package manager:", [
1270
+ { label: "pnpm", value: "pnpm" },
1271
+ { label: "npm", value: "npm" },
1272
+ { label: "yarn", value: "yarn" },
1273
+ { label: "bun", value: "bun" }
1274
+ ])
1275
+ ) : effect.Option.none();
1276
+ return {
1277
+ name,
1278
+ serverType,
1279
+ directory: effect.Option.none(),
1280
+ monorepo: effect.Option.some(monorepo),
1281
+ skipInstall: false,
1282
+ packageManager
1283
+ };
1284
+ });
1285
+ var runCreate = (args) => effect.Effect.gen(function* () {
1286
+ const parsed = parseArgs2(args);
1287
+ if ("help" in parsed) {
1288
+ printCreateHelp();
1289
+ return;
1290
+ }
1291
+ if ("error" in parsed) {
1292
+ yield* effect.Console.error(`Error: ${parsed.error}`);
1293
+ yield* effect.Console.log("");
1294
+ printCreateHelp();
1295
+ process.exitCode = 1;
1296
+ return;
1297
+ }
1298
+ const options = "interactive" in parsed ? yield* runInteractive() : parsed.options;
1299
+ yield* scaffold(options);
1300
+ });
1301
+
182
1302
  // src/bin.ts
183
1303
  var VERSION = "0.1.0";
184
1304
  var printHelp = () => {
@@ -189,7 +1309,7 @@ Usage: effect-gql <command> [options]
189
1309
 
190
1310
  Commands:
191
1311
  generate-schema Generate GraphQL SDL from a schema module
192
- create Create a new Effect GraphQL project (coming soon)
1312
+ create Create a new Effect GraphQL project
193
1313
 
194
1314
  Options:
195
1315
  -h, --help Show this help message
@@ -198,7 +1318,7 @@ Options:
198
1318
  Examples:
199
1319
  effect-gql generate-schema ./src/schema.ts
200
1320
  effect-gql generate-schema ./src/schema.ts -o schema.graphql
201
- effect-gql create my-app
1321
+ effect-gql create my-app --server-type node
202
1322
 
203
1323
  Run 'effect-gql <command> --help' for command-specific help.
204
1324
  `);
@@ -223,8 +1343,7 @@ var main = effect.Effect.gen(function* () {
223
1343
  yield* runGenerateSchema(commandArgs);
224
1344
  break;
225
1345
  case "create":
226
- yield* effect.Console.log("The 'create' command is coming soon!");
227
- yield* effect.Console.log("It will help you scaffold new Effect GraphQL projects.");
1346
+ yield* runCreate(commandArgs);
228
1347
  break;
229
1348
  default:
230
1349
  yield* effect.Console.error(`Unknown command: ${command}`);