@gigadrive/network-config 3.0.1 → 3.1.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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/detection/detect-framework.ts
2
- import { FileSystem as FileSystem3, Path as Path3 } from "@effect/platform";
3
- import { Effect as Effect4 } from "effect";
2
+ import { FileSystem as FileSystem4, Path as Path3 } from "@effect/platform";
3
+ import { Effect as Effect6 } from "effect";
4
4
 
5
5
  // src/detection/detect-package-manager.ts
6
6
  import { FileSystem, Path } from "@effect/platform";
@@ -157,21 +157,65 @@ var laravel = {
157
157
  };
158
158
 
159
159
  // src/detection/frameworks/nestjs.ts
160
- var nestjs = {
161
- slug: "nestjs",
162
- name: "NestJS",
163
- language: "node",
164
- detectors: [{ matchPackage: "@nestjs/core" }],
165
- priority: 90,
166
- getDefaultConfig: () => ({
160
+ import { FileSystem as FileSystem2 } from "@effect/platform";
161
+ import { Effect as Effect2 } from "effect";
162
+ var isRecord = (value) => typeof value === "object" && value !== null;
163
+ var parseNestCliJson = (content) => {
164
+ try {
165
+ const parsed = JSON.parse(content);
166
+ if (!isRecord(parsed)) return void 0;
167
+ return parsed;
168
+ } catch {
169
+ return void 0;
170
+ }
171
+ };
172
+ var getProjectOutputPath = (config) => {
173
+ if (typeof config.compilerOptions?.outputPath === "string") {
174
+ return config.compilerOptions.outputPath;
175
+ }
176
+ if (typeof config.defaultProject === "string") {
177
+ const outputPath2 = config.projects?.[config.defaultProject]?.compilerOptions?.outputPath;
178
+ if (typeof outputPath2 === "string") return outputPath2;
179
+ }
180
+ const firstProject = Object.values(config.projects ?? {})[0];
181
+ const outputPath = firstProject?.compilerOptions?.outputPath;
182
+ return typeof outputPath === "string" ? outputPath : void 0;
183
+ };
184
+ var normalizeOutputPath = (outputPath) => {
185
+ const normalized = outputPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
186
+ if (!normalized || normalized === "." || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || normalized.includes(":") || normalized.split("/").some((segment) => segment === "..")) {
187
+ return void 0;
188
+ }
189
+ return normalized;
190
+ };
191
+ var createNestConfig = (outputPath) => {
192
+ const outputDirectory = normalizeOutputPath(outputPath) ?? "dist";
193
+ const entrypoint = `${outputDirectory}/main.js`;
194
+ return {
167
195
  runtime: "node-22",
168
196
  memory: 256,
169
197
  maxDuration: 30,
170
198
  streaming: true,
171
199
  commands: ["nest build"],
172
- entrypoint: "dist/main.js",
173
- routes: [{ source: "/*", destination: "dist/main.js" }],
200
+ entrypoint,
201
+ routes: [{ source: "/*", destination: entrypoint }],
174
202
  environmentVariables: { NODE_ENV: "production" }
203
+ };
204
+ };
205
+ var nestjs = {
206
+ slug: "nestjs",
207
+ name: "NestJS",
208
+ language: "node",
209
+ detectors: [{ matchPackage: "@nestjs/core" }],
210
+ priority: 90,
211
+ getDefaultConfig: () => createNestConfig("dist"),
212
+ refineDefaultConfig: (defaults, projectFolder) => Effect2.gen(function* () {
213
+ const fs = yield* FileSystem2.FileSystem;
214
+ const content = yield* fs.readFileString(`${projectFolder}/nest-cli.json`).pipe(Effect2.catchAll(() => Effect2.succeed(void 0)));
215
+ if (!content) return defaults;
216
+ const nestConfig = parseNestCliJson(content);
217
+ const outputPath = nestConfig ? getProjectOutputPath(nestConfig) : void 0;
218
+ return outputPath ? createNestConfig(outputPath) : defaults;
175
219
  })
176
220
  };
177
221
 
@@ -326,7 +370,7 @@ var FRAMEWORK_DEFINITIONS = [
326
370
  ].sort((a, b) => b.priority - a.priority);
327
371
 
328
372
  // src/detection/generate-config.ts
329
- import { Effect as Effect2 } from "effect";
373
+ import { Effect as Effect3 } from "effect";
330
374
 
331
375
  // src/regions.ts
332
376
  var REGION_DEFINITIONS = [
@@ -366,11 +410,11 @@ var getInstallCommand = (pm) => {
366
410
  return "npm install";
367
411
  }
368
412
  };
369
- var generateConfig = Effect2.fn("generateConfig")(function* (framework, packageManager) {
370
- const defaults = framework.getDefaultConfig(packageManager);
413
+ var generateConfig = Effect3.fn("generateConfig")(function* (framework, packageManager, refinedDefaults) {
414
+ const defaults = refinedDefaults ?? framework.getDefaultConfig(packageManager);
371
415
  const installCmd = defaults.installCommand ?? getInstallCommand(packageManager);
372
416
  const commands = [installCmd, ...defaults.commands];
373
- yield* Effect2.logDebug(`Generating config for ${framework.name}`, {
417
+ yield* Effect3.logDebug(`Generating config for ${framework.name}`, {
374
418
  runtime: defaults.runtime,
375
419
  entrypoint: defaults.entrypoint,
376
420
  commands
@@ -382,7 +426,8 @@ var generateConfig = Effect2.fn("generateConfig")(function* (framework, packageM
382
426
  memory: defaults.memory,
383
427
  maxDuration: defaults.maxDuration,
384
428
  streaming: defaults.streaming,
385
- symlinks: defaults.symlinks
429
+ symlinks: defaults.symlinks,
430
+ package: defaults.package
386
431
  }
387
432
  ] : [];
388
433
  const routes = defaults.entrypoint ? defaults.routes.map((route) => ({
@@ -414,8 +459,8 @@ var generateConfig = Effect2.fn("generateConfig")(function* (framework, packageM
414
459
  });
415
460
 
416
461
  // src/detection/read-dependencies.ts
417
- import { FileSystem as FileSystem2, Path as Path2 } from "@effect/platform";
418
- import { Effect as Effect3 } from "effect";
462
+ import { FileSystem as FileSystem3, Path as Path2 } from "@effect/platform";
463
+ import { Effect as Effect5 } from "effect";
419
464
 
420
465
  // src/detection/types.ts
421
466
  import { Schema } from "effect";
@@ -448,25 +493,25 @@ var MANIFEST_CONFIG = {
448
493
  node: { file: "package.json", depKeys: ["dependencies", "devDependencies"] },
449
494
  php: { file: "composer.json", depKeys: ["require", "require-dev"] }
450
495
  };
451
- var readManifest = Effect3.fn("readManifest")(function* (filePath) {
452
- const fs = yield* FileSystem2.FileSystem;
453
- const exists = yield* fs.exists(filePath).pipe(Effect3.catchAll(() => Effect3.succeed(false)));
496
+ var readManifest = Effect5.fn("readManifest")(function* (filePath) {
497
+ const fs = yield* FileSystem3.FileSystem;
498
+ const exists = yield* fs.exists(filePath).pipe(Effect5.catchAll(() => Effect5.succeed(false)));
454
499
  if (!exists) {
455
- return yield* Effect3.fail(new ManifestNotFoundError({ message: "Manifest not found", filePath }));
500
+ return yield* Effect5.fail(new ManifestNotFoundError({ message: "Manifest not found", filePath }));
456
501
  }
457
- const content = yield* fs.readFileString(filePath).pipe(Effect3.mapError(() => new ManifestReadError({ message: "Failed to read manifest", filePath })));
458
- const parsed = yield* Effect3.try({
502
+ const content = yield* fs.readFileString(filePath).pipe(Effect5.mapError(() => new ManifestReadError({ message: "Failed to read manifest", filePath })));
503
+ const parsed = yield* Effect5.try({
459
504
  try: () => JSON.parse(content),
460
505
  catch: () => new ManifestParseError({ message: "Failed to parse manifest JSON", filePath })
461
506
  });
462
507
  return parsed;
463
508
  });
464
- var readDependencies = Effect3.fn("readDependencies")(function* (projectFolder, language) {
509
+ var readDependencies = Effect5.fn("readDependencies")(function* (projectFolder, language) {
465
510
  const pathService = yield* Path2.Path;
466
511
  const config = MANIFEST_CONFIG[language];
467
512
  const filePath = pathService.join(projectFolder, config.file);
468
513
  const manifest = yield* readManifest(filePath).pipe(
469
- Effect3.catchTag("ManifestNotFoundError", () => Effect3.succeed(null))
514
+ Effect5.catchTag("ManifestNotFoundError", () => Effect5.succeed(null))
470
515
  );
471
516
  if (!manifest) {
472
517
  return /* @__PURE__ */ new Set();
@@ -484,8 +529,8 @@ var readDependencies = Effect3.fn("readDependencies")(function* (projectFolder,
484
529
  });
485
530
 
486
531
  // src/detection/detect-framework.ts
487
- var matchesDetectionItem = Effect4.fn("matchesDetectionItem")(function* (item, projectFolder, framework, cachedReadDeps) {
488
- const fs = yield* FileSystem3.FileSystem;
532
+ var matchesDetectionItem = Effect6.fn("matchesDetectionItem")(function* (item, projectFolder, framework, cachedReadDeps) {
533
+ const fs = yield* FileSystem4.FileSystem;
489
534
  const pathService = yield* Path3.Path;
490
535
  if (item.matchPackage != null) {
491
536
  const deps = yield* cachedReadDeps(`${projectFolder}\0${framework.language}`);
@@ -495,12 +540,12 @@ var matchesDetectionItem = Effect4.fn("matchesDetectionItem")(function* (item, p
495
540
  }
496
541
  if (item.path != null) {
497
542
  const filePath = pathService.join(projectFolder, item.path);
498
- const exists = yield* fs.exists(filePath).pipe(Effect4.catchAll(() => Effect4.succeed(false)));
543
+ const exists = yield* fs.exists(filePath).pipe(Effect6.catchAll(() => Effect6.succeed(false)));
499
544
  if (!exists) {
500
545
  return false;
501
546
  }
502
547
  if (item.matchContent != null) {
503
- const content = yield* fs.readFileString(filePath).pipe(Effect4.catchAll(() => Effect4.succeed("")));
548
+ const content = yield* fs.readFileString(filePath).pipe(Effect6.catchAll(() => Effect6.succeed("")));
504
549
  let regexMatch = false;
505
550
  try {
506
551
  regexMatch = new RegExp(item.matchContent).test(content);
@@ -513,7 +558,7 @@ var matchesDetectionItem = Effect4.fn("matchesDetectionItem")(function* (item, p
513
558
  }
514
559
  return true;
515
560
  });
516
- var matchesFramework = Effect4.fn("matchesFramework")(function* (framework, projectFolder, cachedReadDeps) {
561
+ var matchesFramework = Effect6.fn("matchesFramework")(function* (framework, projectFolder, cachedReadDeps) {
517
562
  for (const detector of framework.detectors) {
518
563
  const matches = yield* matchesDetectionItem(detector, projectFolder, framework, cachedReadDeps);
519
564
  if (!matches) {
@@ -522,9 +567,9 @@ var matchesFramework = Effect4.fn("matchesFramework")(function* (framework, proj
522
567
  }
523
568
  return true;
524
569
  });
525
- var detectFramework = Effect4.fn("detectFramework")(function* (projectFolder) {
526
- yield* Effect4.logDebug(`Detecting framework in ${projectFolder}`);
527
- const cachedReadDeps = yield* Effect4.cachedFunction((key) => {
570
+ var detectFramework = Effect6.fn("detectFramework")(function* (projectFolder) {
571
+ yield* Effect6.logDebug(`Detecting framework in ${projectFolder}`);
572
+ const cachedReadDeps = yield* Effect6.cachedFunction((key) => {
528
573
  const sepIndex = key.lastIndexOf("\0");
529
574
  const folder = key.slice(0, sepIndex);
530
575
  const language = key.slice(sepIndex + 1);
@@ -533,13 +578,16 @@ var detectFramework = Effect4.fn("detectFramework")(function* (projectFolder) {
533
578
  for (const framework of FRAMEWORK_DEFINITIONS) {
534
579
  const matches = yield* matchesFramework(framework, projectFolder, cachedReadDeps);
535
580
  if (matches) {
536
- yield* Effect4.log(`Detected framework: ${framework.name}`, { slug: framework.slug });
537
- const packageManager = yield* detectPackageManager(projectFolder);
538
- const config = yield* generateConfig(framework, packageManager);
581
+ yield* Effect6.log(`Detected framework: ${framework.name}`, { slug: framework.slug });
582
+ const detectedPackageManager = yield* detectPackageManager(projectFolder);
583
+ const packageManager = framework.language === "php" ? "composer" : detectedPackageManager;
584
+ const defaults = framework.getDefaultConfig(packageManager);
585
+ const refinedDefaults = framework.refineDefaultConfig ? yield* framework.refineDefaultConfig(defaults, projectFolder) : defaults;
586
+ const config = yield* generateConfig(framework, packageManager, refinedDefaults);
539
587
  return { framework, packageManager, config };
540
588
  }
541
589
  }
542
- return yield* Effect4.fail(
590
+ return yield* Effect6.fail(
543
591
  new FrameworkNotDetectedError({
544
592
  message: `No framework detected in project directory`,
545
593
  directory: projectFolder
@@ -548,9 +596,9 @@ var detectFramework = Effect4.fn("detectFramework")(function* (projectFolder) {
548
596
  });
549
597
 
550
598
  // src/detection/merge-config.ts
551
- import { Effect as Effect5 } from "effect";
552
- var mergeWithFrameworkDefaults = Effect5.fn("mergeWithFrameworkDefaults")(function* (userConfig, frameworkConfig) {
553
- yield* Effect5.logDebug("Merging user config with framework defaults");
599
+ import { Effect as Effect7 } from "effect";
600
+ var mergeWithFrameworkDefaults = Effect7.fn("mergeWithFrameworkDefaults")(function* (userConfig, frameworkConfig) {
601
+ yield* Effect7.logDebug("Merging user config with framework defaults");
554
602
  const merged = {
555
603
  regions: userConfig.regions,
556
604
  commands: userConfig.commands.length > 0 ? userConfig.commands : frameworkConfig.commands,
@@ -621,11 +669,11 @@ var filterFunctionsFromAssets = (config) => {
621
669
  };
622
670
 
623
671
  // src/parse-config.ts
624
- import { Effect as Effect11 } from "effect";
672
+ import { Effect as Effect13 } from "effect";
625
673
 
626
674
  // src/services/raw-config-reader.ts
627
- import { FileSystem as FileSystem4, Path as Path4 } from "@effect/platform";
628
- import { Effect as Effect6 } from "effect";
675
+ import { FileSystem as FileSystem5, Path as Path4 } from "@effect/platform";
676
+ import { Effect as Effect8 } from "effect";
629
677
  import { parse as parseYaml } from "yaml";
630
678
  var ALLOWED_CONFIG_NAMES = [
631
679
  "gigadrive.yaml",
@@ -634,34 +682,34 @@ var ALLOWED_CONFIG_NAMES = [
634
682
  "nebula.yml",
635
683
  "nebula.json"
636
684
  ];
637
- var RawConfigReader = class extends Effect6.Service()("RawConfigReader", {
638
- effect: Effect6.gen(function* () {
639
- const fs = yield* FileSystem4.FileSystem;
685
+ var RawConfigReader = class extends Effect8.Service()("RawConfigReader", {
686
+ effect: Effect8.gen(function* () {
687
+ const fs = yield* FileSystem5.FileSystem;
640
688
  const pathService = yield* Path4.Path;
641
- const findConfig = Effect6.fn("RawConfigReader.findConfig")(function* (projectFolder) {
689
+ const findConfig = Effect8.fn("RawConfigReader.findConfig")(function* (projectFolder) {
642
690
  for (const name of ALLOWED_CONFIG_NAMES) {
643
691
  const filePath = pathService.join(projectFolder, name);
644
- const exists = yield* fs.exists(filePath).pipe(Effect6.catchAll(() => Effect6.succeed(false)));
692
+ const exists = yield* fs.exists(filePath).pipe(Effect8.catchAll(() => Effect8.succeed(false)));
645
693
  if (exists) {
646
694
  return filePath;
647
695
  }
648
696
  }
649
697
  return null;
650
698
  });
651
- const readRawConfig = Effect6.fn("RawConfigReader.readRawConfig")(function* (filePath, options) {
652
- const exists = yield* fs.exists(filePath).pipe(Effect6.catchAll(() => Effect6.succeed(false)));
699
+ const readRawConfig = Effect8.fn("RawConfigReader.readRawConfig")(function* (filePath, options) {
700
+ const exists = yield* fs.exists(filePath).pipe(Effect8.catchAll(() => Effect8.succeed(false)));
653
701
  if (!exists) {
654
- return yield* Effect6.fail(
702
+ return yield* Effect8.fail(
655
703
  new ConfigFileNotFoundError({ message: `Config file not found at ${filePath}`, filePath })
656
704
  );
657
705
  }
658
706
  const fileContents = yield* fs.readFileString(filePath).pipe(
659
- Effect6.mapError(
707
+ Effect8.mapError(
660
708
  () => new ConfigFileParseError({ message: `Failed to read config file at ${filePath}`, filePath })
661
709
  )
662
710
  );
663
711
  if (fileContents.length === 0) {
664
- return yield* Effect6.fail(
712
+ return yield* Effect8.fail(
665
713
  new ConfigFileEmptyError({ message: `Config file is empty at ${filePath}`, filePath })
666
714
  );
667
715
  }
@@ -676,7 +724,7 @@ var RawConfigReader = class extends Effect6.Service()("RawConfigReader", {
676
724
  parsed = parseYaml(fileContents);
677
725
  }
678
726
  } catch (error) {
679
- return yield* Effect6.fail(
727
+ return yield* Effect8.fail(
680
728
  new ConfigFileParseError({
681
729
  message: `Failed to parse config file at ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`,
682
730
  filePath,
@@ -685,21 +733,21 @@ var RawConfigReader = class extends Effect6.Service()("RawConfigReader", {
685
733
  );
686
734
  }
687
735
  if (parsed == null) {
688
- return yield* Effect6.fail(
736
+ return yield* Effect8.fail(
689
737
  new ConfigFileParseError({ message: `Config file could not be parsed at ${filePath}`, filePath })
690
738
  );
691
739
  }
692
740
  if (!options?.disableVersionCheck && typeof parsed.version !== "number") {
693
- return yield* Effect6.fail(
741
+ return yield* Effect8.fail(
694
742
  new ConfigVersionError({ message: `Config file is missing version at ${filePath}`, filePath })
695
743
  );
696
744
  }
697
745
  return parsed;
698
746
  });
699
747
  const readConfigFile = (configFilePath) => readRawConfig(configFilePath, { disableVersionCheck: true }).pipe(
700
- Effect6.map((parsed) => parsed),
701
- Effect6.catchTag("ConfigFileNotFoundError", () => Effect6.succeed(null)),
702
- Effect6.withSpan("RawConfigReader.readConfigFile")
748
+ Effect8.map((parsed) => parsed),
749
+ Effect8.catchTag("ConfigFileNotFoundError", () => Effect8.succeed(null)),
750
+ Effect8.withSpan("RawConfigReader.readConfigFile")
703
751
  );
704
752
  return { findConfig, readRawConfig, readConfigFile };
705
753
  })
@@ -709,17 +757,17 @@ var RawConfigReader = class extends Effect6.Service()("RawConfigReader", {
709
757
  // src/services/schema-validator.ts
710
758
  import Ajv from "ajv";
711
759
  import addFormats from "ajv-formats";
712
- import { Effect as Effect7 } from "effect";
713
- var SchemaValidator = class extends Effect7.Service()("SchemaValidator", {
714
- effect: Effect7.gen(function* () {
760
+ import { Effect as Effect9 } from "effect";
761
+ var SchemaValidator = class extends Effect9.Service()("SchemaValidator", {
762
+ effect: Effect9.gen(function* () {
715
763
  const ajv = new Ajv();
716
764
  addFormats(ajv);
717
- const validate = Effect7.fn("SchemaValidator.validate")(function* (config, schema, filePath) {
765
+ const validate = Effect9.fn("SchemaValidator.validate")(function* (config, schema, filePath) {
718
766
  const compiledValidate = ajv.compile(schema);
719
767
  const valid = compiledValidate(config);
720
768
  if (!valid) {
721
769
  const errors = compiledValidate.errors?.map((e) => `${e.instancePath || "/"} ${e.message ?? ""}`.trim()) ?? [];
722
- return yield* Effect7.fail(
770
+ return yield* Effect9.fail(
723
771
  new ConfigSchemaValidationError({
724
772
  message: `Config file at ${filePath} is invalid: ${ajv.errorsText(compiledValidate.errors)}`,
725
773
  filePath,
@@ -734,9 +782,9 @@ var SchemaValidator = class extends Effect7.Service()("SchemaValidator", {
734
782
  };
735
783
 
736
784
  // src/services/v4-config-parser.ts
737
- import { FileSystem as FileSystem5, Path as Path5 } from "@effect/platform";
785
+ import { FileSystem as FileSystem6, Path as Path5 } from "@effect/platform";
738
786
  import { getFilesForPattern } from "@gigadrive/build-utils";
739
- import { Effect as Effect8 } from "effect";
787
+ import { Effect as Effect10 } from "effect";
740
788
  import { minimatch } from "minimatch";
741
789
  import safeRegex from "safe-regex2";
742
790
  var MAX_ASSET_DEPTH = 100;
@@ -749,6 +797,23 @@ var DEFAULT_FUNCTION_SETTINGS = {
749
797
  excludeFiles: void 0,
750
798
  includeFiles: void 0
751
799
  };
800
+ var toArray = (value) => {
801
+ if (value == null) return void 0;
802
+ return Array.isArray(value) ? value : [value];
803
+ };
804
+ var runtimeStreamsByDefault = (runtime) => runtime == null || runtime.startsWith("node-") || runtime.startsWith("bun-");
805
+ var normalizeRouteDestination = (destination) => {
806
+ const [withoutHash] = destination.split("#", 1);
807
+ const [withoutQuery] = withoutHash.split("?", 1);
808
+ return withoutQuery.replace(/^\/+/, "");
809
+ };
810
+ var destinationMatchesEntrypoint = (destination, entrypointPath) => {
811
+ const normalizedDestination = normalizeRouteDestination(destination);
812
+ if (normalizedDestination === entrypointPath) return true;
813
+ if (!normalizedDestination.includes("$")) return false;
814
+ const pattern = normalizedDestination.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\$[\w]+/g, ".+");
815
+ return new RegExp(`^${pattern}$`).test(entrypointPath);
816
+ };
752
817
  var matchesPattern = (path2, pattern) => {
753
818
  if (minimatch(path2, pattern)) return true;
754
819
  try {
@@ -767,10 +832,12 @@ var getFunctionSettings = (path2, config) => {
767
832
  }
768
833
  return settings;
769
834
  };
770
- var resolveRouteHandler = (destination, redirect) => {
835
+ var resolveRouteHandler = (destination, redirect, entrypoints = []) => {
771
836
  const isExternal = destination.toLowerCase().startsWith("http://") || destination.toLowerCase().startsWith("https://");
772
837
  if (redirect === true) return "HTTP_REDIRECT";
773
- return isExternal ? "HTTP_PROXY" : "SERVERLESS_FUNCTION";
838
+ if (isExternal) return "HTTP_PROXY";
839
+ const matchingEntrypoints = entrypoints.filter((item) => destinationMatchesEntrypoint(destination, item.path));
840
+ return matchingEntrypoints.length > 0 && matchingEntrypoints.every((entrypoint) => entrypoint.streaming === true) ? "SERVERLESS_FUNCTION_STREAMING" : "SERVERLESS_FUNCTION";
774
841
  };
775
842
  var isRegion = (r) => AVAILABLE_REGIONS.includes(r);
776
843
  var resolveRegions = (configRegions) => {
@@ -778,28 +845,27 @@ var resolveRegions = (configRegions) => {
778
845
  const valid = configRegions?.filter(isRegion);
779
846
  return valid && valid.length > 0 ? valid : AVAILABLE_REGIONS;
780
847
  };
781
- var mapRoute = (route) => ({
848
+ var mapRoute = (route, entrypoints) => ({
782
849
  path: route.source,
783
850
  destination: route.destination,
784
- handler: resolveRouteHandler(route.destination, route.redirect),
851
+ handler: resolveRouteHandler(route.destination, route.redirect, entrypoints),
785
852
  headers: route.headers ?? {},
786
853
  methods: route.methods ?? ["ANY"],
787
854
  positiveRequirements: route.has,
788
855
  negativeRequirements: route.missing,
789
856
  status: route.statusCode
790
857
  });
791
- var parseEntrypoints = Effect8.fn("parseEntrypoints")(function* (config, projectFolder) {
858
+ var parseEntrypoints = Effect10.fn("parseEntrypoints")(function* (config, projectFolder) {
792
859
  const entrypoints = [];
793
860
  if (config.functions == null) return entrypoints;
794
861
  for (const [fnPath, func] of Object.entries(config.functions)) {
795
- const settings = getFunctionSettings(fnPath, config);
796
- if (settings == null) {
797
- yield* new FunctionConfigError({
862
+ if (getFunctionSettings(fnPath, config) == null) {
863
+ return yield* new FunctionConfigError({
798
864
  message: `Settings invalid for function at path '${fnPath}'`,
799
865
  functionPath: fnPath
800
866
  });
801
867
  }
802
- const matchedFiles = yield* Effect8.tryPromise({
868
+ const matchedFiles = yield* Effect10.tryPromise({
803
869
  try: () => getFilesForPattern(fnPath, projectFolder, func.excludeFiles),
804
870
  catch: (error) => new FunctionConfigError({
805
871
  message: `Failed to resolve files for function pattern '${fnPath}': ${error instanceof Error ? error.message : String(error)}`,
@@ -809,27 +875,40 @@ var parseEntrypoints = Effect8.fn("parseEntrypoints")(function* (config, project
809
875
  for (const file of matchedFiles) {
810
876
  if (entrypoints.some((ep) => ep.path === file)) continue;
811
877
  if (Object.keys(config.functions).some((f) => f === file && f !== fnPath)) continue;
878
+ const settings = getFunctionSettings(file, config);
879
+ if (settings == null) {
880
+ return yield* new FunctionConfigError({
881
+ message: `Settings invalid for function at path '${file}'`,
882
+ functionPath: file
883
+ });
884
+ }
885
+ const runtime = settings.runtime ?? "node-20";
886
+ const streaming = settings.streaming ?? runtimeStreamsByDefault(runtime);
812
887
  entrypoints.push({
813
888
  path: file,
814
- runtime: settings.runtime ?? "node-20",
889
+ runtime,
815
890
  memory: settings.memory ?? 128,
816
891
  maxDuration: settings.max_duration ?? 30,
817
892
  schedule: func.schedule,
818
893
  symlinks: func.symlinks,
819
- streaming: settings.runtime == null || settings.runtime.startsWith("node-") || settings.runtime.startsWith("bun-")
894
+ streaming,
895
+ package: settings.includeFiles != null || settings.excludeFiles != null ? {
896
+ includeFiles: toArray(settings.includeFiles),
897
+ excludeFiles: toArray(settings.excludeFiles)
898
+ } : void 0
820
899
  });
821
900
  }
822
901
  }
823
902
  return entrypoints;
824
903
  });
825
- var collectAssets = Effect8.fn("collectAssets")(function* (config, projectFolder) {
904
+ var collectAssets = Effect10.fn("collectAssets")(function* (config, projectFolder) {
826
905
  if (config.assets == null) return [];
827
- const fs = yield* FileSystem5.FileSystem;
906
+ const fs = yield* FileSystem6.FileSystem;
828
907
  const pathService = yield* Path5.Path;
829
908
  const assetsPath = pathService.join(projectFolder, config.assets);
830
- const assetsExist = yield* fs.exists(assetsPath).pipe(Effect8.catchAll(() => Effect8.succeed(false)));
909
+ const assetsExist = yield* fs.exists(assetsPath).pipe(Effect10.catchAll(() => Effect10.succeed(false)));
831
910
  if (!assetsExist) return [];
832
- const stat = yield* fs.stat(assetsPath).pipe(Effect8.catchAll(() => Effect8.succeed(null)));
911
+ const stat = yield* fs.stat(assetsPath).pipe(Effect10.catchAll(() => Effect10.succeed(null)));
833
912
  if (!stat || stat.type !== "Directory") return [];
834
913
  const allFiles = yield* collectFilesRecursively(assetsPath);
835
914
  const assets = [];
@@ -840,18 +919,18 @@ var collectAssets = Effect8.fn("collectAssets")(function* (config, projectFolder
840
919
  }
841
920
  return assets;
842
921
  });
843
- var collectFilesRecursively = Effect8.fn("collectFilesRecursively")(
922
+ var collectFilesRecursively = Effect10.fn("collectFilesRecursively")(
844
923
  function* (basePath, relativePath = "", depth = 0) {
845
924
  if (depth > MAX_ASSET_DEPTH) return [];
846
- const fs = yield* FileSystem5.FileSystem;
925
+ const fs = yield* FileSystem6.FileSystem;
847
926
  const pathSvc = yield* Path5.Path;
848
927
  const currentPath = relativePath ? pathSvc.join(basePath, relativePath) : basePath;
849
- const entries = yield* fs.readDirectory(currentPath).pipe(Effect8.catchAll(() => Effect8.succeed([])));
928
+ const entries = yield* fs.readDirectory(currentPath).pipe(Effect10.catchAll(() => Effect10.succeed([])));
850
929
  const result = [];
851
930
  for (const name of entries) {
852
931
  const fullPath = pathSvc.join(currentPath, name);
853
932
  const entryRelative = relativePath ? pathSvc.join(relativePath, name) : name;
854
- const stat = yield* fs.stat(fullPath).pipe(Effect8.catchAll(() => Effect8.succeed(null)));
933
+ const stat = yield* fs.stat(fullPath).pipe(Effect10.catchAll(() => Effect10.succeed(null)));
855
934
  if (!stat) continue;
856
935
  if (stat.type === "Directory") {
857
936
  const nested = yield* collectFilesRecursively(basePath, entryRelative, depth + 1);
@@ -863,16 +942,16 @@ var collectFilesRecursively = Effect8.fn("collectFilesRecursively")(
863
942
  return result;
864
943
  }
865
944
  );
866
- var V4ConfigParser = class extends Effect8.Service()("V4ConfigParser", {
945
+ var V4ConfigParser = class extends Effect10.Service()("V4ConfigParser", {
867
946
  accessors: true,
868
- effect: Effect8.succeed({
947
+ effect: Effect10.succeed({
869
948
  /**
870
949
  * Parses a ConfigV4 into a NormalizedConfig.
871
950
  *
872
951
  * @param config - The raw V4 config object
873
952
  * @param projectFolder - Absolute path to the project root
874
953
  */
875
- parse: Effect8.fn("V4ConfigParser.parse")(function* (config, projectFolder) {
954
+ parse: Effect10.fn("V4ConfigParser.parse")(function* (config, projectFolder) {
876
955
  const entrypoints = yield* parseEntrypoints(config, projectFolder);
877
956
  const assets = yield* collectAssets(config, projectFolder);
878
957
  return {
@@ -888,7 +967,7 @@ var V4ConfigParser = class extends Effect8.Service()("V4ConfigParser", {
888
967
  entrypoints,
889
968
  errors: [],
890
969
  warnings: [],
891
- routes: (config.routes ?? []).map(mapRoute)
970
+ routes: (config.routes ?? []).map((route) => mapRoute(route, entrypoints))
892
971
  };
893
972
  })
894
973
  })
@@ -899,7 +978,7 @@ var V4ConfigParser = class extends Effect8.Service()("V4ConfigParser", {
899
978
  import { Path as Path7 } from "@effect/platform";
900
979
  import { getFilesForPattern as getFilesForPattern2 } from "@gigadrive/build-utils";
901
980
  import { deepMerge } from "@gigadrive/commons";
902
- import { Effect as Effect10 } from "effect";
981
+ import { Effect as Effect12 } from "effect";
903
982
 
904
983
  // src/build-output-v3/determine-repo-root.ts
905
984
  import path from "path";
@@ -935,24 +1014,24 @@ var determineRepoRoot = (projectFolder, config) => {
935
1014
  };
936
1015
 
937
1016
  // src/build-output-v3/get-default-path-map.ts
938
- import { FileSystem as FileSystem6, Path as Path6 } from "@effect/platform";
939
- import { Effect as Effect9 } from "effect";
940
- var getDefaultPathMap = Effect9.fn("getDefaultPathMap")(function* (directory) {
941
- const fs = yield* FileSystem6.FileSystem;
1017
+ import { FileSystem as FileSystem7, Path as Path6 } from "@effect/platform";
1018
+ import { Effect as Effect11 } from "effect";
1019
+ var getDefaultPathMap = Effect11.fn("getDefaultPathMap")(function* (directory) {
1020
+ const fs = yield* FileSystem7.FileSystem;
942
1021
  const pathService = yield* Path6.Path;
943
1022
  const result = {};
944
- const exists = yield* fs.exists(directory).pipe(Effect9.catchAll(() => Effect9.succeed(false)));
1023
+ const exists = yield* fs.exists(directory).pipe(Effect11.catchAll(() => Effect11.succeed(false)));
945
1024
  if (!exists) {
946
1025
  return result;
947
1026
  }
948
1027
  const MAX_DEPTH = 100;
949
- const processDirectory = (currentPath, basePath = "", depth = 0) => Effect9.gen(function* () {
1028
+ const processDirectory = (currentPath, basePath = "", depth = 0) => Effect11.gen(function* () {
950
1029
  if (depth > MAX_DEPTH) return;
951
- const entries = yield* fs.readDirectory(currentPath).pipe(Effect9.catchAll(() => Effect9.succeed([])));
1030
+ const entries = yield* fs.readDirectory(currentPath).pipe(Effect11.catchAll(() => Effect11.succeed([])));
952
1031
  for (const name of entries) {
953
1032
  const fullPath = pathService.join(currentPath, name);
954
1033
  const relativePath = basePath ? pathService.join(basePath, name) : name;
955
- const stat = yield* fs.stat(fullPath).pipe(Effect9.catchAll(() => Effect9.succeed(null)));
1034
+ const stat = yield* fs.stat(fullPath).pipe(Effect11.catchAll(() => Effect11.succeed(null)));
956
1035
  if (!stat) continue;
957
1036
  if (stat.type === "Directory") {
958
1037
  yield* processDirectory(fullPath, relativePath, depth + 1);
@@ -1026,11 +1105,11 @@ var translateVercelRuntime = (runtime) => {
1026
1105
  var CONFIG_PATH = ".vercel/output/config.json";
1027
1106
  var FUNCTIONS_FOLDER = ".vercel/output/functions";
1028
1107
  var FUNCTION_CONFIGS_PATTERN = `${FUNCTIONS_FOLDER}/**/*.func/.vc-config.json`;
1029
- var VercelBuildOutputParser = class extends Effect10.Service()("VercelBuildOutputParser", {
1030
- effect: Effect10.gen(function* () {
1108
+ var VercelBuildOutputParser = class extends Effect12.Service()("VercelBuildOutputParser", {
1109
+ effect: Effect12.gen(function* () {
1031
1110
  const rawConfigReader = yield* RawConfigReader;
1032
1111
  const pathService = yield* Path7.Path;
1033
- const parse = Effect10.fn("VercelBuildOutputParser.parse")(function* (parseResult, projectFolder) {
1112
+ const parse = Effect12.fn("VercelBuildOutputParser.parse")(function* (parseResult, projectFolder) {
1034
1113
  const config = yield* rawConfigReader.readConfigFile(pathService.join(projectFolder, CONFIG_PATH));
1035
1114
  if (!config || typeof config.version !== "number" || config.version < 3) {
1036
1115
  return parseResult;
@@ -1041,8 +1120,8 @@ var VercelBuildOutputParser = class extends Effect10.Service()("VercelBuildOutpu
1041
1120
  merged.regions = [...new Set(merged.regions)];
1042
1121
  return merged;
1043
1122
  });
1044
- const loadFunctions = Effect10.fn("VercelBuildOutputParser.loadFunctions")(function* (config, parseResult, projectFolder) {
1045
- const functionConfigs = yield* Effect10.tryPromise({
1123
+ const loadFunctions = Effect12.fn("VercelBuildOutputParser.loadFunctions")(function* (config, parseResult, projectFolder) {
1124
+ const functionConfigs = yield* Effect12.tryPromise({
1046
1125
  try: () => getFilesForPattern2(FUNCTION_CONFIGS_PATTERN, projectFolder),
1047
1126
  catch: (error) => new ConfigFileParseError({
1048
1127
  message: `Failed to scan for function configs: ${error instanceof Error ? error.message : String(error)}`,
@@ -1088,7 +1167,7 @@ var VercelBuildOutputParser = class extends Effect10.Service()("VercelBuildOutpu
1088
1167
  Object.assign(environmentVariables, env);
1089
1168
  }
1090
1169
  }
1091
- const streaming = runtime.startsWith("node-") || runtime.startsWith("bun-");
1170
+ const streaming = functionConfig.supportsResponseStreaming ?? functionConfig.experimentalResponseStreaming ?? runtime.startsWith("node-");
1092
1171
  let filePathMap = {};
1093
1172
  for (const [key, value] of Object.entries(configFilePathMap)) {
1094
1173
  filePathMap[pathService.join(projectFolder, key)] = value;
@@ -1224,6 +1303,10 @@ var schema_default = {
1224
1303
  $ref: "#/$defs/runtimes",
1225
1304
  description: "The runtime to use for the function."
1226
1305
  },
1306
+ streaming: {
1307
+ type: "boolean",
1308
+ description: "Enable function response streaming. When omitted, Node and Bun runtimes stream by default."
1309
+ },
1227
1310
  schedule: {
1228
1311
  type: "string",
1229
1312
  description: "An expression to schedule the function to run at specific times.",
@@ -1496,7 +1579,7 @@ var schema_default = {
1496
1579
  };
1497
1580
 
1498
1581
  // src/parse-config.ts
1499
- var parseConfigRaw = Effect11.fn("parseConfigRaw")(function* (filePath, projectFolder) {
1582
+ var parseConfigRaw = Effect13.fn("parseConfigRaw")(function* (filePath, projectFolder) {
1500
1583
  const rawReader = yield* RawConfigReader;
1501
1584
  const validator = yield* SchemaValidator;
1502
1585
  const v4Parser = yield* V4ConfigParser;
@@ -1507,7 +1590,7 @@ var parseConfigRaw = Effect11.fn("parseConfigRaw")(function* (filePath, projectF
1507
1590
  };
1508
1591
  const schema = schemaMap[version];
1509
1592
  if (!schema) {
1510
- return yield* Effect11.fail(
1593
+ return yield* Effect13.fail(
1511
1594
  new ConfigVersionError({
1512
1595
  message: `Config file at ${filePath} has an unsupported version: ${version}`,
1513
1596
  filePath,
@@ -1518,11 +1601,11 @@ var parseConfigRaw = Effect11.fn("parseConfigRaw")(function* (filePath, projectF
1518
1601
  yield* validator.validate(parsed, schema, filePath);
1519
1602
  return yield* v4Parser.parse(parsed, projectFolder);
1520
1603
  });
1521
- var parseConfig = Effect11.fn("parseConfig")(function* (filePath, projectFolder) {
1604
+ var parseConfig = Effect13.fn("parseConfig")(function* (filePath, projectFolder) {
1522
1605
  const parseResult = yield* parseConfigRaw(filePath, projectFolder);
1523
1606
  return yield* postProcessConfig(parseResult, projectFolder);
1524
1607
  });
1525
- var postProcessConfig = Effect11.fn("postProcessConfig")(function* (config, projectFolder) {
1608
+ var postProcessConfig = Effect13.fn("postProcessConfig")(function* (config, projectFolder) {
1526
1609
  const vercelParser = yield* VercelBuildOutputParser;
1527
1610
  let result = yield* vercelParser.parse(config, projectFolder);
1528
1611
  if (result.entrypoints.length === 0 && (result.assets?.paths?.length || 0) === 0 && result.routes.length === 0) {