Skip to content

feat(network-config): add framework auto-detection and migrate config parsing to Effect services#268

Merged
Zeryther merged 18 commits into
mainfrom
feature/framework-auto-detection
Feb 25, 2026
Merged

feat(network-config): add framework auto-detection and migrate config parsing to Effect services#268
Zeryther merged 18 commits into
mainfrom
feature/framework-auto-detection

Conversation

@Zeryther

@Zeryther Zeryther commented Feb 25, 2026

Copy link
Copy Markdown
Member

Summary

  • Framework auto-detection: Adds a new detection/ module in @gigadrive/network-config that automatically detects web frameworks (Next.js, Nuxt, Remix, SvelteKit, Astro, Hono, Elysia, Express, Fastify, NestJS, Vite, Laravel, Symfony) from project dependencies and generates deployment configs without requiring a gigadrive.yaml.
  • Effect-based config pipeline: Migrates the config parsing pipeline (RawConfigReader, SchemaValidator, V4ConfigParser, VercelBuildOutputParser) to Effect services with @effect/platform FileSystem, replacing direct node:fs usage and mock-fs test dependency with an in-memory FileSystem layer.
  • CLI integration: Updates ProjectConfigService to handle four resolution cases: config file + detected framework (merge), config file only, framework only (auto-detect), and neither (error) — enabling zero-config deployments for supported frameworks.

Changes

New: Framework detection (packages/network-config/src/detection/)

  • detect-framework.ts — reads project manifests, matches against framework definitions, returns config
  • detect-package-manager.ts — detects npm/yarn/pnpm/bun from lockfiles
  • read-dependencies.ts — reads package.json/composer.json dependencies via Effect FileSystem
  • generate-config.ts — converts FrameworkDefaultConfig to NormalizedConfig
  • merge-config.ts — merges user config with framework defaults (user always wins)
  • types.tsFrameworkDefinition, FrameworkDefaultConfig, detection types
  • frameworks/ — 13 framework definitions (nextjs, nuxt, remix, sveltekit, astro, vite, hono, elysia, express, fastify, nestjs, laravel, symfony)
  • ADDING_FRAMEWORKS.md — contributor guide for adding new frameworks

Refactored: Config parsing as Effect services (packages/network-config/src/services/)

  • RawConfigReader — finds and reads config files via FileSystem
  • SchemaValidator — AJV-based schema validation
  • V4ConfigParser — parses v4 config format
  • VercelBuildOutputParser — parses Vercel Build Output v3 (replaces deleted build-output-v3/parse.ts and read-config-file.ts)

Updated

  • parse-config.ts — now an Effect function; added postProcessConfig() for Vercel BOv3 post-processing
  • packages/cli/src/services/project-config.ts — four-case resolution logic with framework merging
  • packages/cli/src/index.ts — wires NetworkConfigLive layer
  • Removed mock-fs dependency; tests use in-memory FileSystem layer (test-utils.ts)
  • Updated AGENTS.md with Effect design decisions and detection architecture docs

Summary by CodeRabbit

  • New Features

    • Automatic framework auto-detection with package-manager-aware defaults, generated deployment config, and support for excludeFiles in archives; updated runtime options (node-22, php-84).
  • Bug Fixes

    • More robust, effect-driven config parsing and validation with clearer structured errors and safer fallback behavior.
  • Documentation

    • Added design guidance and a how‑to for implementing new framework detectors.
  • Tests

    • Tests migrated to an in‑memory, effect-based harness and expanded coverage for detection, config generation, merging, and packaging.

Zeryther and others added 2 commits February 24, 2026 05:36
Add a detection module that automatically identifies web frameworks
(13 supported: Next.js, Nuxt, NestJS, Remix, SvelteKit, Astro,
Laravel, Symfony, Hono, Elysia, Fastify, Express, Vite) and generates
deployment config defaults, eliminating the need for a gigadrive.yaml
in common projects.

- Language-aware dependency detection (package.json / composer.json)
- Lockfile-based package manager detection
- Priority-based framework matching with AND-logic detectors
- Config merging (user config takes precedence over framework defaults)
- Extract postProcessConfig() for Vercel Build Output v3 compatibility
- Integrate auto-detection fallback into CLI ProjectConfigService
- Fix condition order bug in empty-deployment validation
- Fix typos in regions.ts and v4/index.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ervices

Replace raw node:fs I/O and plain Error throws with Effect-native layered
services, typed tagged errors, and @effect/platform FileSystem abstraction.

New services: RawConfigReader, SchemaValidator, V4ConfigParser,
VercelBuildOutputParser — composed into NetworkConfigLive layer. CLI updated
to consume services natively instead of wrapping in Effect.tryPromise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Feb 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e40d279

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@gigadrive/network-config Major
gigadrive Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Feb 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
sdk-harmony Skipped Skipped Feb 25, 2026 7:07am

Request Review

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd80baf and e40d279.

📒 Files selected for processing (1)
  • packages/cli/src/services/project-config.ts

📝 Walkthrough

Walkthrough

Adds an Effect-based framework auto-detection and config pipeline to packages/network-config, new detection APIs/types/framework definitions/tests, refactors parsing into Effect services and a NetworkConfigLive layer, updates CLI to consume detection results (framework metadata and excludeFiles), removes mock-fs dev deps and several legacy sync parsing modules.

Changes

Cohort / File(s) Summary
Detection core & types
packages/network-config/src/detection/detect-framework.ts, packages/network-config/src/detection/detect-package-manager.ts, packages/network-config/src/detection/read-dependencies.ts, packages/network-config/src/detection/types.ts, packages/network-config/src/detection/index.ts
Add Effect-based detection pipeline, package-manager detection, manifest reader, new tagged errors and DetectionResult types; export detection API.
Framework definitions
packages/network-config/src/detection/frameworks/*.ts, packages/network-config/src/detection/frameworks/index.ts
Add many framework definition modules (nextjs, nuxt, remix, sveltekit, astro, vite, laravel, symfony, nestjs, express, fastify, hono, elysia) with detectors, priorities and getDefaultConfig; aggregated and priority-sorted export.
Config generation & merging
packages/network-config/src/detection/generate-config.ts, packages/network-config/src/detection/merge-config.ts
Introduce generateConfig and mergeWithFrameworkDefaults (Effect functions) to produce NormalizedConfig from frameworks and to merge user config with framework defaults with defined precedence.
Network-config services & layers
packages/network-config/src/services/*.ts, packages/network-config/src/services/index.ts
Introduce RawConfigReader, SchemaValidator, V4ConfigParser, VercelBuildOutputParser as Effect services and compose NetworkConfigLive layer requiring a FileSystem layer.
Parsing refactor & removals
packages/network-config/src/parse-raw-config.ts, packages/network-config/src/find-config.ts, packages/network-config/src/build-output-v3/parse.ts, packages/network-config/src/v4/parse.ts
Remove legacy synchronous parsing modules and tests, replace with Effect-based services and layered parsers.
CLI integration
packages/cli/src/index.ts, packages/cli/src/services/project-config.ts, packages/cli/src/services/project-config.test.ts
Wire NetworkConfigLive into CLI base layers; ProjectConfigService.resolve now handles detection, postProcess, merge and returns `{ config, configPath: string
Archive & CLI options
packages/cli/src/commands/platform/deploy/index.ts, packages/cli/src/services/archive.ts
Add excludeFiles option to archive creation and pass config.excludeFiles through deploy/archive flows.
Normalized config & errors
packages/network-config/src/normalized-config.ts, packages/network-config/src/errors.ts
Add excludeFiles?: string[] to NormalizedConfig and introduce structured config error classes (ConfigFile*, FunctionConfigError, etc.).
Tests & test utils
packages/network-config/src/test-utils.ts, packages/network-config/src/detection/*.test.ts, packages/network-config/src/services/*.test.ts
Add in-memory FileSystem Layer makeTestFs, extensive Effect-based tests for detection, package manager, generate/merge, raw-config reader, v4 parser, vercel parser; remove mock-fs usage.
Build-output helpers
packages/network-config/src/build-output-v3/get-default-path-map.ts, packages/network-config/src/build-output-v3/*
Convert get-default-path-map to Effect-based implementation; removed other old build-output-v3 modules/tests.
Package changes
package.json, packages/network-config/package.json
Remove root devDeps mock-fs and @types/mock-fs; add effect, @effect/platform and safe-regex2 to network-config package; add @effect/platform-node as devDep.
Docs & changeset
packages/network-config/AGENTS.md, packages/network-config/src/detection/ADDING_FRAMEWORKS.md, .changeset/nasty-red-round.md
Add framework-contribution guide and changeset; AGENTS.md contains duplicated "Design Decisions" blocks (duplicate documentation added).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(240,248,255,0.5)
    participant Client as Client
    participant PCS as ProjectConfigService
    participant Raw as RawConfigReader
    participant DF as detectFramework
    participant DPM as detectPackageManager
    participant GC as generateConfig
    participant MC as mergeWithFrameworkDefaults
    end

    Client->>PCS: resolve(projectFolder)
    PCS->>Raw: findConfig(projectFolder)
    alt config found
        Raw-->>PCS: configPath
        PCS->>Raw: readRawConfig(configPath)
        Raw-->>PCS: rawConfig
    else no config
        Raw-->>PCS: null
    end

    PCS->>DF: detectFramework(projectFolder)
    alt framework detected
        DF->>DPM: detectPackageManager(projectFolder)
        DPM-->>DF: packageManager
        DF->>GC: generateConfig(framework, packageManager)
        GC-->>DF: frameworkConfig
        DF-->>PCS: DetectionResult
    else none
        DF-->>PCS: FrameworkNotDetectedError
    end

    alt config + framework
        PCS->>MC: mergeWithFrameworkDefaults(parsedConfig, frameworkConfig)
        MC-->>PCS: mergedConfig
    else framework only
        PCS->>GC: postProcessConfig(frameworkConfig)
        GC-->>PCS: processedConfig
    end

    PCS-->>Client: { config, configPath: string | null, framework? }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐇 I sniffed lockfiles, read the tree of files,
Effects now hum and tests replaced old tiles,
Frameworks line up—Next, Laravel, and Nuxt—
Defaults filled in while user settings remain robust,
A rabbit hops: zero-config deploys with smiles!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the two main changes: framework auto-detection and migration of config parsing to Effect services, matching the actual changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/framework-auto-detection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

♻️ Duplicate comments (3)
packages/network-config/src/detection/frameworks/nestjs.ts (1)

9-9: Same getDefaultConfig signature drift as already flagged.

This file has the same pattern already called out in packages/network-config/src/detection/frameworks/sveltekit.ts (Line 9).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/frameworks/nestjs.ts` at line 9, The
getDefaultConfig function here uses the inconsistent zero-arg signature; update
the getDefaultConfig declaration in this file to match the canonical signature
used by other framework detectors (same shape as
packages/network-config/src/detection/frameworks/sveltekit.ts), i.e. change the
function signature to accept the same parameters (options/context) and return
the same typed config form; locate and edit the exported getDefaultConfig arrow
function to accept the matching parameter list and propagate those parameters
into its config computation so signatures are consistent across detectors.
packages/network-config/src/detection/frameworks/remix.ts (1)

9-9: Same getDefaultConfig signature drift as already flagged.

This file repeats the same arity mismatch pattern noted in packages/network-config/src/detection/frameworks/sveltekit.ts (Line 9).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/frameworks/remix.ts` at line 9,
getDefaultConfig in this file has the wrong function signature (arity) compared
to the expected detection API; update the getDefaultConfig declaration in
remix.ts to match the same signature used in the other framework detectors (the
one fixed in sveltekit.ts) — i.e., accept the same parameters (e.g., add the
missing argument such as projectRoot or options) and return the same config
shape, and ensure any calls or exported types referencing getDefaultConfig are
updated to the new signature so types and consumers remain consistent.
packages/network-config/src/detection/frameworks/nextjs.ts (1)

9-9: Same getDefaultConfig signature drift as already flagged.

This repeats the same arity mismatch pattern noted earlier in packages/network-config/src/detection/frameworks/sveltekit.ts (Line 9).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/frameworks/nextjs.ts` at line 9,
getDefaultConfig is declared with zero parameters (getDefaultConfig: () =>
{...}) but other framework detectors expect the same factory to accept a
context/arity (as flagged in sveltekit.ts); change the signature of
getDefaultConfig in nextjs.ts to match the expected arity (e.g.,
getDefaultConfig: (projectRoot?: string) => {...} or the shared context type
used by other detectors) and update any internal references to use the passed
parameter so the function signature is consistent across framework detectors.
🟡 Minor comments (4)
packages/network-config/src/detection/frameworks/sveltekit.ts-9-9 (1)

9-9: ⚠️ Potential issue | 🟡 Minor

Align getDefaultConfig parameter with the interface signature across all frameworks.

The FrameworkDefinition interface declares getDefaultConfig(packageManager: PackageManager), but this implementation omits the parameter. The caller in generate-config.ts passes packageManager to every framework's getDefaultConfig, so the signature should accept it explicitly for consistency—even if unused, this clarifies intent.

This pattern is consistent across all 13 framework definitions (astro, elysia, express, fastify, hono, laravel, nestjs, nextjs, nuxt, remix, symfony, sveltekit, vite). Update this one with (_packageManager) => ({ and apply the same fix to all other frameworks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/frameworks/sveltekit.ts` at line 9,
Update the sveltekit FrameworkDefinition implementation so its getDefaultConfig
matches the interface signature by accepting the packageManager parameter;
change the function to declare the parameter (e.g., _packageManager) even if
unused, aligning with FrameworkDefinition and the caller in generate-config.ts,
and apply the same change to the other framework definitions (astro, elysia,
express, fastify, hono, laravel, nestjs, nextjs, nuxt, remix, symfony, vite) so
all getDefaultConfig functions accept a PackageManager parameter.
packages/network-config/src/parse-config.ts-17-23 (1)

17-23: ⚠️ Potential issue | 🟡 Minor

Complete exported JSDoc blocks with required @returns/@example tags.

Both exported functions are missing required JSDoc tags (parseConfig is missing @returns and @example; postProcessConfig is missing @example).

📝 Proposed JSDoc patch
 /**
  * Parses a config file at the given path, validates it against the appropriate
  * JSON Schema, converts it to a NormalizedConfig, and applies post-processing.
  *
  * `@param` filePath - Absolute path to the config file
  * `@param` projectFolder - Absolute path to the project root
+ * `@returns` Effect that resolves to a post-processed NormalizedConfig
+ * `@example`
+ * await Effect.runPromise(parseConfig('/abs/project/gigadrive.yaml', '/abs/project'));
  */
 export const parseConfig = Effect.fn('parseConfig')(function* (filePath: string, projectFolder: string) {
   ...
 });

 /**
  * Applies post-processing transformers to a NormalizedConfig:
  * - Vercel Build Output v3 (merges functions/routes from .vercel/output if present)
  * - Empty deployment validation
  * - Filter functions from assets
  *
  * `@param` config - The parsed NormalizedConfig
  * `@param` projectFolder - Absolute path to the project root
  * `@returns` The post-processed NormalizedConfig
+ * `@example`
+ * await Effect.runPromise(postProcessConfig(config, '/abs/project'));
  */
 export const postProcessConfig = Effect.fn('postProcessConfig')(function* (

As per coding guidelines, Document exported functions with JSDoc comments including @param, @returns, and @example tags.

Also applies to: 55-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/parse-config.ts` around lines 17 - 23, Add
missing JSDoc tags to the exported functions parseConfig and postProcessConfig:
update the comment for parseConfig to include an `@returns` describing the
Promise<NormalizedConfig> (or the actual return type) and an `@example` showing
typical usage with filePath and projectFolder, and update postProcessConfig to
include an `@example` demonstrating input and returned normalized config; ensure
both JSDoc blocks keep existing `@param` entries, accurately describe types and
thrown errors (if any), and follow the project's JSDoc style conventions so the
exported functions are fully documented.
packages/network-config/src/detection/ADDING_FRAMEWORKS.md-9-18 (1)

9-18: ⚠️ Potential issue | 🟡 Minor

Specify a language for the fenced code block.

Line 9 opens a fenced block without a language, which triggers markdownlint MD040.

📝 Proposed fix
-```
+```text
 packages/network-config/src/detection/
   frameworks/
     index.ts          ← barrel: register your framework here
     nextjs.ts         ← example: meta-framework (high priority)
     elysia.ts         ← example: minimal server (low priority)
     laravel.ts        ← example: PHP framework (multi-detector)
     <your-framework>.ts  ← you add this
   types.ts            ← FrameworkDefinition interface
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @packages/network-config/src/detection/ADDING_FRAMEWORKS.md around lines 9 -
18, The fenced code block in ADDING_FRAMEWORKS.md is missing a language
specifier which triggers markdownlint MD040; update the opening fence to include
a language (e.g., change totext) for the block that lists the
packages/network-config/src/detection/ frameworks so the linter recognizes it as
a fenced code block with language; ensure only the opening fence is changed and
the content of the block (the list with index.ts, nextjs.ts, elysia.ts,
laravel.ts, .ts, types.ts) remains unchanged.


</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/detection/detect-framework.ts-78-78 (1)</summary><blockquote>

`78-78`: _⚠️ Potential issue_ | _🟡 Minor_

**Validate `projectFolder` before starting detection.**

Line 78 accepts empty/whitespace paths without a guard, which can lead to probing unintended paths and confusing failures. Add an early input guard.

<details>
<summary>🛡️ Suggested change</summary>

```diff
 export const detectFramework = Effect.fn('detectFramework')(function* (projectFolder: string) {
+  if (projectFolder.trim().length === 0) {
+    return yield* Effect.fail(new Error('projectFolder must be a non-empty path'));
+  }
+
   yield* Effect.logDebug(`Detecting framework in ${projectFolder}`);
```
</details>

As per coding guidelines, "Use guard clauses with early `throw new Error(...)` for invalid inputs".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-framework.ts` at line 78, The
detectFramework generator (export const detectFramework =
Effect.fn('detectFramework')(function* (projectFolder: string) { ... }) ) lacks
validation for projectFolder, allowing empty or whitespace-only paths; add an
early guard clause at the top of that function which trims projectFolder and
throws a new Error(...) if it's empty/undefined/only whitespace before any
detection logic runs so the function fails-fast on invalid input.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (12)</summary><blockquote>

<details>
<summary>packages/network-config/src/v4/parse.test.ts (3)</summary><blockquote>

`93-119`: **Prefer a typed cast over `config as any`.**

`readFixture` returns `Record<string, unknown>`, which is incompatible with `getFunctionSettings`'s expected parameter type. Casting to `any` silently disables all type checks on the argument, meaning future signature changes to `getFunctionSettings` won't be caught here. Cast to the actual config type (e.g., `config as V4Config` or whatever the parameter type of `getFunctionSettings` is) to preserve compile-time safety.

<details>
<summary>♻️ Suggested approach</summary>

Either narrow the return type of `readFixture` for YAML configs:
```diff
-async function readFixture(filePath: string): Promise<Record<string, unknown>> {
+async function readFixture(filePath: string): Promise<V4Config> {
```

Or use the precise cast at call sites instead of `as any`:
```diff
-expect(getFunctionSettings('src/index.ts', config as any)).toEqual({
+expect(getFunctionSettings('src/index.ts', config as V4Config)).toEqual({
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/v4/parse.test.ts` around lines 93 - 119, The
tests currently cast the parsed config with "config as any", which disables type
checks; instead cast to the concrete config type expected by getFunctionSettings
(e.g., config as V4Config) or adjust readFixture to return Record<string,
V4Config> so the calls like getFunctionSettings('src/index.ts', config as
V4Config) are correctly typed; update the four calls that pass config to
getFunctionSettings to use the precise cast and import/alias the V4Config type
where needed.
```

</details>

---

`32-33`: **Inconsistent fixture loading: use `readFixture` for the schema file too.**

Line 30 reads `example.yaml` through the new `readFixture` helper, but lines 32–33 still use `fs.readFileSync` + `JSON.parse` directly for `schema.json`. Since `readFixture` handles JSON already (line 20–22), it can replace this inline block for consistency.

<details>
<summary>♻️ Proposed refactor</summary>

```diff
-    const schemaFile = fs.readFileSync(path.join(__dirname, 'schema.json'), 'utf8');
-    const schema = JSON.parse(schemaFile);
+    const schema = await readFixture(path.join(__dirname, 'schema.json'));
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/v4/parse.test.ts` around lines 32 - 33, Replace
the inline fs.readFileSync + JSON.parse block that builds the schema (currently
producing schema from schemaFile) with the existing readFixture helper; i.e.,
assign const schema = readFixture('schema.json') so the test uses the same
fixture loader as the example.yaml path and benefits from readFixture's JSON
parsing behavior (ensure readFixture is in scope where schema is created).
```

</details>

---

`17-25`: **`readFileSync` blocks the event loop inside an async function; prefer `fs.promises.readFile`.**

The function is async solely because of `await import('yaml')` on the YAML branch. The `readFileSync` call on line 18 still blocks the Node.js event loop for every invocation. While this is tolerable in tests, it's inconsistent with the function's async signature.

<details>
<summary>♻️ Proposed refactor</summary>

```diff
 async function readFixture(filePath: string): Promise<Record<string, unknown>> {
-  const content = fs.readFileSync(filePath, 'utf8');
+  const content = await fs.promises.readFile(filePath, 'utf8');
   const ext = path.extname(filePath).toLowerCase();
   if (ext === '.json') {
     return JSON.parse(content) as Record<string, unknown>;
   }
   const { parse: parseYaml } = await import('yaml');
   return parseYaml(content) as Record<string, unknown>;
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/v4/parse.test.ts` around lines 17 - 25,
readFixture currently uses blocking fs.readFileSync inside an async function;
switch to the promise-based API so the function is fully non-blocking. Replace
the synchronous read with an awaited read from fs.promises (e.g., const content
= await fs.promises.readFile(filePath, 'utf8')) in the readFixture function and
keep the dynamic await import('yaml') branch unchanged; ensure types remain
Record<string, unknown> and behavior for JSON and YAML parsing is preserved.
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/services/v4-config-parser.test.ts (1)</summary><blockquote>

`39-52`: **Add parse-level assertions for entrypoint settings to prevent overlap regressions.**

Current parse test only checks regions and commands. It should also assert that parsed entrypoints preserve runtime/schedule/symlink behavior for specific files.

<details>
<summary>🧪 Suggested assertion extension</summary>

```diff
   expect(result.regions).toEqual(AVAILABLE_REGIONS);
   expect(result.commands).toEqual(['bun install']);
+  expect(result.entrypoints).toEqual(
+    expect.arrayContaining([
+      expect.objectContaining({ path: 'src/index.ts', runtime: 'bun-1', memory: 128, maxDuration: 15 }),
+      expect.objectContaining({ path: 'src/cron.ts', schedule: 'rate(5 minutes)' }),
+    ])
+  );
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.test.ts` around lines
39 - 52, The test for V4ConfigParser.parse only asserts regions and commands but
misses entrypoint-level assertions; update the test (in v4-config-parser.test.ts
where loadExample(), V4ConfigParser.parse and result are used) to also assert
that result.entrypoints (or the parsed entrypoint structure returned by parse)
contains the expected entries and that each targeted file preserves its runtime,
schedule and symlink flags (e.g., check runtime === expectedRuntime, schedule
=== expectedSchedule, symlink === expectedSymlink for the specific entrypoint
keys from the example ConfigV4) to prevent regressions in entrypoint parsing.
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/services/schema-validator.ts (1)</summary><blockquote>

`20-23`: **`new Ajv()` and `addFormats` instantiated inside `validate` — hoisting to service scope avoids repeated allocation**

A new `Ajv` instance (and `addFormats` registration) is created on every `validate` call. This is wasteful — `Ajv` is designed to be instantiated once and reused. Move the instantiation into the `Effect.gen` body so it is created once at service initialization.



<details>
<summary>♻️ Proposed refactor</summary>

```diff
 export class SchemaValidator extends Effect.Service<SchemaValidator>()('SchemaValidator', {
   effect: Effect.gen(function* () {
+    const ajv = new Ajv();
+    addFormats(ajv);
+
     const validate = Effect.fn('SchemaValidator.validate')(function* (
       config: Record<string, unknown>,
       schema: object,
       filePath: string
     ) {
-      const ajv = new Ajv();
-      addFormats(ajv);
-
       const compiledValidate = ajv.compile(schema);
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/schema-validator.ts` around lines 20 -
23, The code currently creates a new Ajv instance and calls addFormats on every
invocation of validate; move Ajv instantiation and addFormats registration out
of the validate function and into the service initialization inside the
Effect.gen body so a single Ajv instance is reused. Specifically, create ajv =
new Ajv() and call addFormats(ajv) once at the top-level of the Effect.gen block
that constructs the validator service, then in the validate function use
ajv.compile(schema) (or cache compiled validators) instead of new
Ajv()/addFormats per call; reference symbols: Ajv, addFormats, validate,
Effect.gen, and compiledValidate to locate and change the allocation site.
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/detection/generate-config.ts (1)</summary><blockquote>

`25-32`: **Add an `@example` section to the exported function JSDoc.**

The `generateConfig` JSDoc is missing an example usage block.


<details>
<summary>🧾 Suggested doc update</summary>

```diff
 /**
  * Generates a NormalizedConfig from a framework definition and detected package manager.
  * Prepends the install command and builds a complete deployment configuration.
  *
  * `@param` framework - The detected framework definition
  * `@param` packageManager - The detected package manager
  * `@returns` A complete NormalizedConfig with framework defaults
+ * `@example`
+ * const config = await Effect.runPromise(generateConfig(nextjs, 'pnpm'));
  */
```
</details>
As per coding guidelines, "Document exported functions with JSDoc comments including `@param`, `@returns`, and `@example` tags".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/generate-config.ts` around lines 25 -
32, Add an `@example` block to the JSDoc for the exported function generateConfig
showing how to call it and what it returns; update the doc above the
generateConfig declaration to include a short usage example (e.g., importing or
referencing a FrameworkDefinition, passing a packageManager like "npm" or
"yarn", calling generateConfig(framework, packageManager), and a brief example
of the returned NormalizedConfig shape) so the JSDoc contains `@param`, `@returns`,
and `@example` tags for quick reference.
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/detection/detect-package-manager.ts (1)</summary><blockquote>

`28-28`: **Use `path.join` for consistent path construction.**

This uses string interpolation (`\`${projectFolder}/${file}\``) while `raw-config-reader.ts` uses `path.join(projectFolder, name)` for the same operation. Using `path.join` is more robust (handles trailing slashes, etc.) and keeps the codebase consistent.

<details>
<summary>♻️ Suggested fix</summary>

```diff
 import { FileSystem } from '@effect/platform';
 import { Effect } from 'effect';
+import path from 'node:path';
 import type { PackageManager } from './types';
```

```diff
-      const exists = yield* fs.exists(`${projectFolder}/${file}`).pipe(Effect.catchAll(() => Effect.succeed(false)));
+      const exists = yield* fs.exists(path.join(projectFolder, file)).pipe(Effect.catchAll(() => Effect.succeed(false)));
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-package-manager.ts` at line 28,
The file uses string interpolation when calling fs.exists in
detect-package-manager.ts (const exists = yield*
fs.exists(`${projectFolder}/${file}`)...); change this to use
path.join(projectFolder, file) for robust path construction, update the import
to include Node's path module if not already imported, and ensure the same
pattern as raw-config-reader.ts is followed so trailing slashes and platform
separators are handled consistently.
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/services/raw-config-reader.ts (2)</summary><blockquote>

`64-72`: **Unreachable fallback branch for unknown file extensions.**

The `else` branch (lines 70-71) silently falls back to YAML parsing for unknown extensions, but `ALLOWED_CONFIG_NAMES` only contains `.yaml`, `.yml`, and `.json` files — so this branch is dead code. If a non-standard extension somehow reaches this code in the future, silently treating it as YAML could mask bugs.

Consider either removing the branch and failing explicitly, or adding a comment explaining the intentional fallback.

<details>
<summary>♻️ Option: fail explicitly on unexpected extensions</summary>

```diff
       if (fileExtension === '.json') {
         parsed = JSON.parse(fileContents) as Record<string, unknown>;
       } else if (['.yml', '.yaml'].includes(fileExtension)) {
         parsed = parseYaml(fileContents) as Record<string, unknown>;
       } else {
-        parsed = parseYaml(fileContents) as Record<string, unknown>;
+        return yield* Effect.fail(
+          new ConfigFileParseError({
+            message: `Unsupported config file extension "${fileExtension}" at ${filePath}`,
+            filePath,
+          })
+        );
       }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.ts` around lines 64 -
72, The current parsing block in raw-config-reader.ts silently falls back to
parseYaml for any unknown fileExtension (dead code behind ALLOWED_CONFIG_NAMES);
change the final else branch to explicitly throw a descriptive error instead of
calling parseYaml — reference the local variables fileExtension and parsed and
the functions JSON.parse and parseYaml (and the ALLOWED_CONFIG_NAMES constant)
so the error message can state the unexpected extension and list allowed
extensions; ensure callers of the function will surface the thrown error (or
adjust signature) rather than silently accepting YAML parsing for unknown types.
```

</details>

---

`7-7`: **Consider `as const` for `ALLOWED_CONFIG_NAMES`.**

Marking this array `as const` would provide a narrower tuple type and enable deriving a union type of allowed filenames if needed downstream.

```diff
-export const ALLOWED_CONFIG_NAMES = ['gigadrive.yaml', 'gigadrive.yml', 'nebula.yaml', 'nebula.yml', 'nebula.json'];
+export const ALLOWED_CONFIG_NAMES = ['gigadrive.yaml', 'gigadrive.yml', 'nebula.yaml', 'nebula.yml', 'nebula.json'] as const;
```

As per coding guidelines: "Prefer `as const` assertions for literal arrays that derive union types (e.g., AVAILABLE_REGIONS, AVAILABLE_RUNTIMES)."

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.ts` at line 7,
ALLOWED_CONFIG_NAMES is currently a mutable string array; change its declaration
to a readonly tuple by appending as const so the type becomes a literal tuple
and you can derive a union of allowed filenames downstream (update any code that
relied on it being string[] to accept readonly string[] or derive a union via
typeof ALLOWED_CONFIG_NAMES[number]); locate ALLOWED_CONFIG_NAMES in
raw-config-reader.ts and replace its declaration with the as const assertion and
adjust any type usages accordingly.
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/detection/detect-framework.test.ts (1)</summary><blockquote>

`9-14`: **Duplicate `makeTestFs` — prefer the shared helper from `test-utils.ts`.**

A richer `makeTestFs` already exists in `packages/network-config/src/test-utils.ts` with directory-existence handling, `readDirectory`, and `stat` support. This local copy only stubs `exists` and `readFileString`, which means any future detection logic touching `stat` or `readDirectory` will silently receive `undefined` methods, failing at runtime rather than at compile time (because of the `as unknown as` cast).

<details>
<summary>♻️ Suggested fix</summary>

```diff
-import { FileSystem } from '@effect/platform';
-import { Effect, Layer } from 'effect';
+import { Effect } from 'effect';
 import { describe, expect, it } from 'vitest';
 import { detectFramework } from './detect-framework';
+import { makeTestFs } from '../test-utils';
-
-/**
- * Creates a test FileSystem layer with in-memory files and existence checks.
- */
-const makeTestFs = (files: Record<string, string>) =>
-  Layer.succeed(FileSystem.FileSystem, {
-    exists: (path: string) => Effect.succeed(path in files),
-    readFileString: (path: string) =>
-      path in files ? Effect.succeed(files[path]) : Effect.fail(new Error(`File not found: ${path}`)),
-  } as unknown as FileSystem.FileSystem);
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-framework.test.ts` around lines
9 - 14, The local duplicate makeTestFs in detect-framework.test.ts should be
removed and replaced by the shared helper from
packages/network-config/src/test-utils.ts; import the consolidated makeTestFs
(the version that implements exists, readFileString, readDirectory, and stat)
and use it in this test so detection logic gets full FS behavior instead of the
stubbed two-method implementation (avoid casting with as unknown as
FileSystem.FileSystem and rely on the typed helper from test-utils.ts).
```

</details>

</blockquote></details>
<details>
<summary>packages/cli/src/services/project-config.test.ts (1)</summary><blockquote>

`43-49`: **Consider adding a type-safe stub instead of `as any`.**

The `StubRawConfigReader` bypasses type safety with `as any`. A narrower approach would be to type-assert only the missing methods, making it easier to catch breakage if the `RawConfigReader` interface changes.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/services/project-config.test.ts` around lines 43 - 49,
Replace the unsafe "as any" cast on the StubRawConfigReader with a type-safe
stub that matches RawConfigReader's shape: create a stub object typed as
Partial<RawConfigReader> (e.g. const rawConfigReaderStub:
Partial<RawConfigReader> = { findConfig: () => Effect.succeed(findConfigResult),
readRawConfig: () => Effect.succeed({}), readConfigFile: () =>
Effect.succeed(null) }) and then use either a narrow cast (rawConfigReaderStub
as RawConfigReader) or, better, use TypeScript's "satisfies RawConfigReader" on
the object literal to ensure missing/renamed methods are caught; then pass that
typed object into Layer.succeed(RawConfigReader, ... ) instead of using "as
any".
```

</details>

</blockquote></details>
<details>
<summary>packages/network-config/src/detection/detect-framework.ts (1)</summary><blockquote>

`68-77`: **Add an `@example` block to exported function docs.**

The exported `detectFramework` JSDoc is missing `@example`.

As per coding guidelines, "Document exported functions with JSDoc comments including `@param`, `@returns`, and `@example` tags".

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-framework.ts` around lines 68 -
77, Add an `@example` tag to the JSDoc for the exported detectFramework function
to illustrate typical usage: show calling detectFramework with a projectFolder
string (absolute or relative), awaiting the promise, and inspecting the returned
DetectionResult (framework, packageManager, generated config). Update the
comment block above the detectFramework export in detect-framework.ts to include
a short runnable example (e.g., async call within an IIFE or top-level await)
that demonstrates input and how to read the returned object's properties.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @packages/cli/src/index.ts:

  • Around line 39-48: ProjectConfigService currently requires RawConfigReader but
    its dependency is being provided at the usage site; modify the service
    definition (ProjectConfigService) to declare NetworkConfig as a dependency (add
    a dependencies field that includes NetworkConfig/RawConfigReader binding) so the
    required RawConfigReader/NetworkConfig is owned by ProjectConfigService.Default;
    then update index.ts to use ProjectConfigService.Default directly (remove
    Layer.provide(ProjectConfigService.Default, NetworkConfigLive)) and remove the
    duplicate NetworkConfigLive from the Layer.mergeAll call that builds
    BaseServices to avoid redundant composition.

In @packages/cli/src/services/project-config.ts:

  • Around line 79-88: postProcessConfig is being wrapped with Effect.catchAll
    which violates the guideline; replace the generic catchAll on the pipeline that
    calls postProcessConfig(detection.config, cwd) with a targeted Effect.catchTag
    or Effect.catchTags that handles the specific error tag(s) that
    postProcessConfig can emit, and only map those to a new ConfigParseError
    (preserving the original error/cause); leave other unexpected errors unhandled
    (or re-throw) so they aren’t swallowed. Ensure you update the pipeline where
    config is assigned (the result of postProcessConfig(detection.config,
    cwd).pipe(...)) and reference the same ConfigParseError construction used today
    when mapping the specific tag(s).
  • Around line 49-58: The current use of Effect.catchAll around parseConfig
    flattens distinct parse errors and violates the CLI guideline; replace the
    catchAll usage in the parseConfig block (and the identical block later) with
    Effect.catchTags/Effect.catchTag that match the specific error tags
    (ConfigFileNotFoundError, ConfigFileParseError, ConfigVersionError,
    ConfigSchemaValidationError) and map each to an appropriate ConfigParseError
    while preserving the original error as the cause, and extract the repeated logic
    into a shared helper (e.g., parseConfigWithDetailedErrors or wrapParseConfig)
    that calls parseConfig(configPath, cwd) and applies the catchTags-based mapping
    so both sites reuse the helper.
  • Around line 19-20: ProjectConfigService is using RawConfigReader via yield*
    RawConfigReader inside its effect but RawConfigReader is not declared in the
    service definition; update the ProjectConfigService declaration to include
    dependencies: [RawConfigReader.Default] (mirroring AuthService's pattern) so
    RawConfigReader is provided as an explicit dependency, keeping the effect
    (effect: Effect.gen(function* () { const rawReader = yield* RawConfigReader; ...
    })) unchanged.

In @packages/network-config/src/detection/frameworks/astro.ts:

  • Around line 9-20: The getDefaultConfig object currently sets commands: ['astro
    build'] which omits installing dependencies; update getDefaultConfig to prepend
    the appropriate install step using the packageManager parameter (e.g. determine
    the install command from packageManager and insert it before 'astro build' in
    the commands array) so that dependencies (including astro) are installed prior
    to running the build; modify the configuration construction that returns
    commands to use packageManager to generate the full commands array.

In @packages/network-config/src/detection/frameworks/express.ts:

  • Around line 9-18: getDefaultConfig currently sets commands: [] which skips
    dependency installation; update getDefaultConfig in
    packages/network-config/src/detection/frameworks/express.ts to add an install
    step driven by the existing packageManager logic (e.g., inject the
    packageManager's install command into the commands array, and optionally a build
    step if a build script is present) so that node_modules are installed during
    deployment; ensure you reference getDefaultConfig and the commands property when
    adding the install command and preserve existing streaming/entrypoint values.

In @packages/network-config/src/detection/frameworks/laravel.ts:

  • Line 6: The language field in the Laravel framework descriptor is set
    incorrectly to 'node'; update the language property in
    packages/network-config/src/detection/frameworks/laravel.ts to 'php' so it
    aligns with runtime: 'php-84' and the PHP detectors ('laravel/framework' and
    'artisan') and ensures detectFramework reads composer.json (similar to
    symfony.ts); locate and change the language: 'node' entry inside the exported
    framework descriptor for Laravel to language: 'php'.
  • Line 14: The build commands in getDefaultConfig currently hardcode "composer
    require bref/laravel-bridge --update-with-dependencies" (which mutates
    composer.lock) and hardcode Bun steps; change getDefaultConfig to accept a
    packageManager parameter (getDefaultConfig: (packageManager) => ({...})), remove
    the composer require from the commands array so bref/laravel-bridge is not
    injected at deploy time, and branch the install/build commands using the
    packageManager value (e.g., npm/yarn/pnpm vs bun) to construct the appropriate
    install and build commands instead of always using 'bun install'/'bun run
    build'; update the commands array reference in the returned config and mirror
    the pattern used in symfony.ts for packageManager handling.

In @packages/network-config/src/detection/frameworks/nuxt.ts:

  • Around line 9-20: getDefaultConfig currently ignores the packageManager
    parameter and only runs 'nuxt build', which will fail without installing deps;
    update the getDefaultConfig implementation in
    packages/network-config/src/detection/frameworks/nuxt.ts (the getDefaultConfig
    function) to accept and use the packageManager argument and prepend the
    appropriate install command (map packageManager values to 'npm install', 'yarn',
    'pnpm install', or 'bun install') to the commands array before 'nuxt build',
    ensuring the install step is first and preserving existing entries like 'nuxt
    build'.

In @packages/network-config/src/detection/frameworks/symfony.ts:

  • Around line 21-26: The default APP_SECRET value in the environmentVariables
    object is a known public placeholder and must not be shipped; update
    detection/frameworks/symfony.ts so the environmentVariables map does not include
    a hard-coded APP_SECRET key (omit APP_SECRET entirely) or replace it with an
    explicit sentinel that forces users to supply a value at deploy time, and ensure
    any code that merges defaults (the environmentVariables constant/object) will
    fail or warn if APP_SECRET is missing; specifically modify the
    environmentVariables export (remove the APP_SECRET entry) and add a clear
    runtime validation/warning where defaults are consumed to require the user to
    set APP_SECRET.
  • Line 10: The default Symfony runtime value currently uses the unsupported
    string 'php-84'; update that runtime value to 'php-83' so generated Symfony
    configs validate against the V4 schema. Locate the runtime field (runtime:
    'php-84') in the Symfony framework definition in frameworks/symfony.ts and
    replace the literal 'php-84' with 'php-83' (preserving surrounding formatting
    and any related export/const names).
  • Line 14: The commands array in the Symfony framework definition currently
    hardcodes "bun install" and "bun run build", ignoring the provided
    packageManager argument from the FrameworkDefinition; update the commands
    construction to use the packageManager variable (e.g., ${packageManager} install and ${packageManager} run build or a small mapping that chooses the
    appropriate install/build commands per package manager) so the install/build
    steps follow the detected package manager; apply the same change to the
    analogous commands array in laravel.ts.

In @packages/network-config/src/detection/generate-config.ts:

  • Around line 61-67: The routes mapping always forces handler:
    'SERVERLESS_FUNCTION', so when the configuration enables streaming those routes
    are not emitted as streaming; update the routes creation in generate-config.ts
    (the routes: defaults.routes.map(...) block) to choose the handler based on the
    streaming flag (e.g., use handler: defaults.streaming ? 'STREAMING_FUNCTION' as
    const : 'SERVERLESS_FUNCTION' as const) so frameworks with streaming: true emit
    streaming route handlers.

In @packages/network-config/src/detection/read-dependencies.ts:

  • Line 57: The current catch wipes every ManifestReadError (readManifest ->
    ManifestReadError) and returns null, hiding parse/I/O failures; change the error
    handling so only the "manifest not found" variant is swallowed: replace the
    broad Effect.catchTag('ManifestReadError', ...) with a targeted handler that
    catches the specific not-found variant (e.g., 'ManifestNotFound' or whatever tag
    your ManifestReadError uses) and returns null, while allowing other
    ManifestReadError variants (parse errors, I/O errors) to propagate (or re-throw
    with a descriptive message) so they surface to the caller instead of being
    silently ignored.

In @packages/network-config/src/services/v4-config-parser.ts:

  • Around line 95-133: The code currently calls getFunctionSettings(fnPath,
    config) once per pattern key, causing broad patterns to apply the wrong settings
    to matched files; move the settings lookup into the matchedFiles loop and call
    getFunctionSettings(file, config) so runtime/memory/max_duration/streaming are
    resolved per actual file path, while still using func.schedule and func.symlinks
    from the pattern; update any references to settings in the entrypoints.push
    (runtime, memory, maxDuration, streaming) to use the per-file settings variable
    (and keep the existing FunctionConfigError behavior for resolution failures).
  • Around line 24-25: In getFunctionSettings, avoid unguarded dynamic RegExp
    construction from user-supplied keys: validate/escape or sandbox the pattern
    before calling new RegExp(key) (or prefer minimatch), and wrap the RegExp
    creation/test in a try-catch that skips invalid patterns; additionally enforce
    simple safeguards (e.g., max pattern length) to reduce ReDoS risk—update the
    loop that iterates Object.entries(config.functions ?? {}) (and the line using
    minimatch(path, key) || new RegExp(key).test(path)) to first try minimatch, then
    safely attempt RegExp construction inside try-catch or use an escaped/validated
    pattern, and skip or log patterns that throw SyntaxError.

In @packages/network-config/src/services/vercel-build-output-parser.ts:

  • Around line 65-69: The code currently overwrites result.regions with
    parseResult.regions each iteration, dropping regions from earlier functions;
    change the logic that assigns to result.regions (where result:
    Partial and parseResult is the per-function parse result) to
    append/merge the new regions instead of replacing them (e.g., concat or push the
    parseResult.regions array, guarding for undefined) so all functions' regions
    accumulate rather than being rebuilt per iteration.
  • Around line 125-160: The route destination fields are using just the handler
    filename which can collide; update the destination in both the defaultRoute and
    the mapped config.routes (the objects created where destination: handler is set)
    to use the same full entrypoint path you pushed into result.entrypoints (the
    path produced by path.join(functionDirectory, handler)). In other words, replace
    the simple handler reference with the full entrypoint path (the same value used
    when constructing result.entrypoints) so defaultRoute and the objects inside
    configRoutes reference the identical path.

Minor comments:
In @packages/network-config/src/detection/ADDING_FRAMEWORKS.md:

  • Around line 9-18: The fenced code block in ADDING_FRAMEWORKS.md is missing a
    language specifier which triggers markdownlint MD040; update the opening fence
    to include a language (e.g., change totext) for the block that lists the
    packages/network-config/src/detection/ frameworks so the linter recognizes it as
    a fenced code block with language; ensure only the opening fence is changed and
    the content of the block (the list with index.ts, nextjs.ts, elysia.ts,
    laravel.ts, .ts, types.ts) remains unchanged.

In @packages/network-config/src/detection/detect-framework.ts:

  • Line 78: The detectFramework generator (export const detectFramework =
    Effect.fn('detectFramework')(function* (projectFolder: string) { ... }) ) lacks
    validation for projectFolder, allowing empty or whitespace-only paths; add an
    early guard clause at the top of that function which trims projectFolder and
    throws a new Error(...) if it's empty/undefined/only whitespace before any
    detection logic runs so the function fails-fast on invalid input.

In @packages/network-config/src/detection/frameworks/sveltekit.ts:

  • Line 9: Update the sveltekit FrameworkDefinition implementation so its
    getDefaultConfig matches the interface signature by accepting the packageManager
    parameter; change the function to declare the parameter (e.g., _packageManager)
    even if unused, aligning with FrameworkDefinition and the caller in
    generate-config.ts, and apply the same change to the other framework definitions
    (astro, elysia, express, fastify, hono, laravel, nestjs, nextjs, nuxt, remix,
    symfony, vite) so all getDefaultConfig functions accept a PackageManager
    parameter.

In @packages/network-config/src/parse-config.ts:

  • Around line 17-23: Add missing JSDoc tags to the exported functions
    parseConfig and postProcessConfig: update the comment for parseConfig to include
    an @returns describing the Promise (or the actual return type)
    and an @example showing typical usage with filePath and projectFolder, and
    update postProcessConfig to include an @example demonstrating input and returned
    normalized config; ensure both JSDoc blocks keep existing @param entries,
    accurately describe types and thrown errors (if any), and follow the project's
    JSDoc style conventions so the exported functions are fully documented.

Duplicate comments:
In @packages/network-config/src/detection/frameworks/nestjs.ts:

  • Line 9: The getDefaultConfig function here uses the inconsistent zero-arg
    signature; update the getDefaultConfig declaration in this file to match the
    canonical signature used by other framework detectors (same shape as
    packages/network-config/src/detection/frameworks/sveltekit.ts), i.e. change the
    function signature to accept the same parameters (options/context) and return
    the same typed config form; locate and edit the exported getDefaultConfig arrow
    function to accept the matching parameter list and propagate those parameters
    into its config computation so signatures are consistent across detectors.

In @packages/network-config/src/detection/frameworks/nextjs.ts:

  • Line 9: getDefaultConfig is declared with zero parameters (getDefaultConfig:
    () => {...}) but other framework detectors expect the same factory to accept a
    context/arity (as flagged in sveltekit.ts); change the signature of
    getDefaultConfig in nextjs.ts to match the expected arity (e.g.,
    getDefaultConfig: (projectRoot?: string) => {...} or the shared context type
    used by other detectors) and update any internal references to use the passed
    parameter so the function signature is consistent across framework detectors.

In @packages/network-config/src/detection/frameworks/remix.ts:

  • Line 9: getDefaultConfig in this file has the wrong function signature (arity)
    compared to the expected detection API; update the getDefaultConfig declaration
    in remix.ts to match the same signature used in the other framework detectors
    (the one fixed in sveltekit.ts) — i.e., accept the same parameters (e.g., add
    the missing argument such as projectRoot or options) and return the same config
    shape, and ensure any calls or exported types referencing getDefaultConfig are
    updated to the new signature so types and consumers remain consistent.

Nitpick comments:
In @packages/cli/src/services/project-config.test.ts:

  • Around line 43-49: Replace the unsafe "as any" cast on the StubRawConfigReader
    with a type-safe stub that matches RawConfigReader's shape: create a stub object
    typed as Partial (e.g. const rawConfigReaderStub:
    Partial = { findConfig: () => Effect.succeed(findConfigResult),
    readRawConfig: () => Effect.succeed({}), readConfigFile: () =>
    Effect.succeed(null) }) and then use either a narrow cast (rawConfigReaderStub
    as RawConfigReader) or, better, use TypeScript's "satisfies RawConfigReader" on
    the object literal to ensure missing/renamed methods are caught; then pass that
    typed object into Layer.succeed(RawConfigReader, ... ) instead of using "as
    any".

In @packages/network-config/src/detection/detect-framework.test.ts:

  • Around line 9-14: The local duplicate makeTestFs in detect-framework.test.ts
    should be removed and replaced by the shared helper from
    packages/network-config/src/test-utils.ts; import the consolidated makeTestFs
    (the version that implements exists, readFileString, readDirectory, and stat)
    and use it in this test so detection logic gets full FS behavior instead of the
    stubbed two-method implementation (avoid casting with as unknown as
    FileSystem.FileSystem and rely on the typed helper from test-utils.ts).

In @packages/network-config/src/detection/detect-framework.ts:

  • Around line 68-77: Add an @example tag to the JSDoc for the exported
    detectFramework function to illustrate typical usage: show calling
    detectFramework with a projectFolder string (absolute or relative), awaiting the
    promise, and inspecting the returned DetectionResult (framework, packageManager,
    generated config). Update the comment block above the detectFramework export in
    detect-framework.ts to include a short runnable example (e.g., async call within
    an IIFE or top-level await) that demonstrates input and how to read the returned
    object's properties.

In @packages/network-config/src/detection/detect-package-manager.ts:

  • Line 28: The file uses string interpolation when calling fs.exists in
    detect-package-manager.ts (const exists = yield*
    fs.exists(${projectFolder}/${file})...); change this to use
    path.join(projectFolder, file) for robust path construction, update the import
    to include Node's path module if not already imported, and ensure the same
    pattern as raw-config-reader.ts is followed so trailing slashes and platform
    separators are handled consistently.

In @packages/network-config/src/detection/generate-config.ts:

  • Around line 25-32: Add an @example block to the JSDoc for the exported
    function generateConfig showing how to call it and what it returns; update the
    doc above the generateConfig declaration to include a short usage example (e.g.,
    importing or referencing a FrameworkDefinition, passing a packageManager like
    "npm" or "yarn", calling generateConfig(framework, packageManager), and a brief
    example of the returned NormalizedConfig shape) so the JSDoc contains @param,
    @returns, and @example tags for quick reference.

In @packages/network-config/src/services/raw-config-reader.ts:

  • Around line 64-72: The current parsing block in raw-config-reader.ts silently
    falls back to parseYaml for any unknown fileExtension (dead code behind
    ALLOWED_CONFIG_NAMES); change the final else branch to explicitly throw a
    descriptive error instead of calling parseYaml — reference the local variables
    fileExtension and parsed and the functions JSON.parse and parseYaml (and the
    ALLOWED_CONFIG_NAMES constant) so the error message can state the unexpected
    extension and list allowed extensions; ensure callers of the function will
    surface the thrown error (or adjust signature) rather than silently accepting
    YAML parsing for unknown types.
  • Line 7: ALLOWED_CONFIG_NAMES is currently a mutable string array; change its
    declaration to a readonly tuple by appending as const so the type becomes a
    literal tuple and you can derive a union of allowed filenames downstream (update
    any code that relied on it being string[] to accept readonly string[] or derive
    a union via typeof ALLOWED_CONFIG_NAMES[number]); locate ALLOWED_CONFIG_NAMES in
    raw-config-reader.ts and replace its declaration with the as const assertion and
    adjust any type usages accordingly.

In @packages/network-config/src/services/schema-validator.ts:

  • Around line 20-23: The code currently creates a new Ajv instance and calls
    addFormats on every invocation of validate; move Ajv instantiation and
    addFormats registration out of the validate function and into the service
    initialization inside the Effect.gen body so a single Ajv instance is reused.
    Specifically, create ajv = new Ajv() and call addFormats(ajv) once at the
    top-level of the Effect.gen block that constructs the validator service, then in
    the validate function use ajv.compile(schema) (or cache compiled validators)
    instead of new Ajv()/addFormats per call; reference symbols: Ajv, addFormats,
    validate, Effect.gen, and compiledValidate to locate and change the allocation
    site.

In @packages/network-config/src/services/v4-config-parser.test.ts:

  • Around line 39-52: The test for V4ConfigParser.parse only asserts regions and
    commands but misses entrypoint-level assertions; update the test (in
    v4-config-parser.test.ts where loadExample(), V4ConfigParser.parse and result
    are used) to also assert that result.entrypoints (or the parsed entrypoint
    structure returned by parse) contains the expected entries and that each
    targeted file preserves its runtime, schedule and symlink flags (e.g., check
    runtime === expectedRuntime, schedule === expectedSchedule, symlink ===
    expectedSymlink for the specific entrypoint keys from the example ConfigV4) to
    prevent regressions in entrypoint parsing.

In @packages/network-config/src/v4/parse.test.ts:

  • Around line 93-119: The tests currently cast the parsed config with "config as
    any", which disables type checks; instead cast to the concrete config type
    expected by getFunctionSettings (e.g., config as V4Config) or adjust readFixture
    to return Record<string, V4Config> so the calls like
    getFunctionSettings('src/index.ts', config as V4Config) are correctly typed;
    update the four calls that pass config to getFunctionSettings to use the precise
    cast and import/alias the V4Config type where needed.
  • Around line 32-33: Replace the inline fs.readFileSync + JSON.parse block that
    builds the schema (currently producing schema from schemaFile) with the existing
    readFixture helper; i.e., assign const schema = readFixture('schema.json') so
    the test uses the same fixture loader as the example.yaml path and benefits from
    readFixture's JSON parsing behavior (ensure readFixture is in scope where schema
    is created).
  • Around line 17-25: readFixture currently uses blocking fs.readFileSync inside
    an async function; switch to the promise-based API so the function is fully
    non-blocking. Replace the synchronous read with an awaited read from fs.promises
    (e.g., const content = await fs.promises.readFile(filePath, 'utf8')) in the
    readFixture function and keep the dynamic await import('yaml') branch unchanged;
    ensure types remain Record<string, unknown> and behavior for JSON and YAML
    parsing is preserved.

</details>

---

<details>
<summary>ℹ️ Review info</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 774909d4f134df91502dc1476bddf989e78242c4 and 7a54f5f0f2db5d6c8a59ef929accbcaf25b5beb5.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (59)</summary>

* `AGENTS.md`
* `package.json`
* `packages/cli/src/index.ts`
* `packages/cli/src/services/project-config.test.ts`
* `packages/cli/src/services/project-config.ts`
* `packages/network-config/AGENTS.md`
* `packages/network-config/package.json`
* `packages/network-config/src/build-output-v3/get-default-path-map.test.ts`
* `packages/network-config/src/build-output-v3/get-default-path-map.ts`
* `packages/network-config/src/build-output-v3/parse.ts`
* `packages/network-config/src/build-output-v3/read-config-file.test.ts`
* `packages/network-config/src/build-output-v3/read-config-file.ts`
* `packages/network-config/src/detection/ADDING_FRAMEWORKS.md`
* `packages/network-config/src/detection/detect-framework.test.ts`
* `packages/network-config/src/detection/detect-framework.ts`
* `packages/network-config/src/detection/detect-package-manager.test.ts`
* `packages/network-config/src/detection/detect-package-manager.ts`
* `packages/network-config/src/detection/frameworks/astro.ts`
* `packages/network-config/src/detection/frameworks/elysia.ts`
* `packages/network-config/src/detection/frameworks/express.ts`
* `packages/network-config/src/detection/frameworks/fastify.ts`
* `packages/network-config/src/detection/frameworks/hono.ts`
* `packages/network-config/src/detection/frameworks/index.ts`
* `packages/network-config/src/detection/frameworks/laravel.ts`
* `packages/network-config/src/detection/frameworks/nestjs.ts`
* `packages/network-config/src/detection/frameworks/nextjs.ts`
* `packages/network-config/src/detection/frameworks/nuxt.ts`
* `packages/network-config/src/detection/frameworks/remix.ts`
* `packages/network-config/src/detection/frameworks/sveltekit.ts`
* `packages/network-config/src/detection/frameworks/symfony.ts`
* `packages/network-config/src/detection/frameworks/vite.ts`
* `packages/network-config/src/detection/generate-config.test.ts`
* `packages/network-config/src/detection/generate-config.ts`
* `packages/network-config/src/detection/index.ts`
* `packages/network-config/src/detection/merge-config.test.ts`
* `packages/network-config/src/detection/merge-config.ts`
* `packages/network-config/src/detection/read-dependencies.test.ts`
* `packages/network-config/src/detection/read-dependencies.ts`
* `packages/network-config/src/detection/types.ts`
* `packages/network-config/src/errors.ts`
* `packages/network-config/src/find-config.ts`
* `packages/network-config/src/index.ts`
* `packages/network-config/src/parse-config.ts`
* `packages/network-config/src/parse-raw-config.test.ts`
* `packages/network-config/src/parse-raw-config.ts`
* `packages/network-config/src/regions.ts`
* `packages/network-config/src/services/index.ts`
* `packages/network-config/src/services/raw-config-reader.test.ts`
* `packages/network-config/src/services/raw-config-reader.ts`
* `packages/network-config/src/services/schema-validator.test.ts`
* `packages/network-config/src/services/schema-validator.ts`
* `packages/network-config/src/services/v4-config-parser.test.ts`
* `packages/network-config/src/services/v4-config-parser.ts`
* `packages/network-config/src/services/vercel-build-output-parser.test.ts`
* `packages/network-config/src/services/vercel-build-output-parser.ts`
* `packages/network-config/src/test-utils.ts`
* `packages/network-config/src/v4/index.ts`
* `packages/network-config/src/v4/parse.test.ts`
* `packages/network-config/src/v4/parse.ts`

</details>

<details>
<summary>💤 Files with no reviewable changes (8)</summary>

* package.json
* packages/network-config/src/parse-raw-config.ts
* packages/network-config/src/build-output-v3/parse.ts
* packages/network-config/src/parse-raw-config.test.ts
* packages/network-config/src/v4/parse.ts
* packages/network-config/src/build-output-v3/read-config-file.ts
* packages/network-config/src/build-output-v3/read-config-file.test.ts
* packages/network-config/src/find-config.ts

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread packages/cli/src/index.ts Outdated
Comment thread packages/cli/src/services/project-config.ts
Comment thread packages/cli/src/services/project-config.ts Outdated
Comment thread packages/cli/src/services/project-config.ts Outdated
Comment thread packages/network-config/src/detection/frameworks/astro.ts
Comment thread packages/network-config/src/detection/read-dependencies.ts Outdated
Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated
Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated
Comment thread packages/network-config/src/services/vercel-build-output-parser.ts
Comment thread packages/network-config/src/services/vercel-build-output-parser.ts
Zeryther and others added 2 commits February 25, 2026 04:51
…che dependency reads

- Add excludeFiles field to NormalizedConfig for archive-level file exclusion
- Map excludeFiles from FrameworkDefaultConfig through generateConfig
- Merge excludeFiles in mergeWithFrameworkDefaults (user takes precedence)
- Pass excludeFiles to ArchiveService for deployment archive filtering
- Memoize readDependencies via Effect.cachedFunction to avoid redundant I/O
- Add clarifying comment for Case A postProcessConfig asymmetry
- Replace catchAll with catchTags in ProjectConfigService for specific error handling
- Add dependencies: [NetworkConfigLive] to ProjectConfigService and simplify layer composition
- Split ManifestReadError into ManifestNotFoundError, ManifestReadError, ManifestParseError
- Use streaming-aware handler (SERVERLESS_FUNCTION_STREAMING) in generateConfig
- Guard new RegExp(key) with try/catch in getFunctionSettings for glob patterns
- Fix region aggregation in VercelBuildOutputParser to preserve earlier functions' regions
- Sync v4 schema runtime enum with runtime.ts (add php-84, node-22; remove php-82, php-80)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
packages/network-config/src/services/v4-config-parser.ts (2)

100-141: Settings are resolved per pattern key, not per matched file — overlapping patterns apply incorrect settings.

This was flagged in a prior review and still applies. getFunctionSettings(fnPath, config) at Line 102 resolves settings for the pattern string itself, not for each matched file. With overlapping patterns (e.g., api/**/*.ts + api/special.ts), a broad match processes files first and the specific pattern's settings never apply because the file is already in entrypoints (Line 123 skip).

Additionally, Lines 136–137 take schedule/symlinks directly from func rather than from settings, creating an inconsistency: runtime/memory/max_duration come from the merged settings object, but schedule/symlinks bypass the merge entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 100 -
141, The code resolves settings once per pattern (getFunctionSettings(fnPath,
config)) causing broad patterns to claim files before more specific patterns can
apply and also reads schedule/symlinks from the raw func instead of the merged
settings. Fix by resolving settings per matched file: inside the matchedFiles
loop call getFunctionSettings(file, config) (or otherwise merge config for that
specific file) and use those returned settings for runtime/memory/max_duration
as well as schedule and symlinks; keep the existing dedupe check
(entrypoints.find...) but ensure it uses the per-file settings to decide
inclusion so specific-pattern settings can override broad patterns.

25-30: ReDoS risk from dynamic RegExp construction remains.

The try-catch now prevents crashes from invalid regex syntax (addressing the prior feedback), but a valid-yet-pathological pattern (e.g., (a+)+$) can still cause exponential backtracking. Since these patterns come from the project's own gigadrive.yaml, the practical risk is low, but consider adding a timeout or length cap if this ever accepts less-trusted input.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 25 -
30, The dynamic RegExp construction for matching rules (the new RegExp(key) used
to set regexMatch) can still allow catastrophic backtracking for
valid-but-pathological patterns; update v4-config-parser.ts to guard RegExp(key)
by first rejecting or sanitizing unsafe/oversized patterns — e.g., introduce a
MAX_PATTERN_LENGTH constant and skip/ignore patterns longer than that, or run a
safety check using a library like safe-regex before constructing the RegExp, and
only call new RegExp(key).test(path) when the pattern passes the safety/length
check; ensure the fallback behavior remains the same (skip regex matching) when
a pattern is rejected.
packages/network-config/src/services/vercel-build-output-parser.ts (1)

125-160: ⚠️ Potential issue | 🟠 Major

Route destinations should use the same full entrypoint path as the function entrypoint.

Line 146 and Line 159 currently use only handler, which can collide across multiple functions with identical handler filenames.

🐛 Proposed fix
+        const functionEntrypointPath = path.join(functionDirectory, handler);
+
         result.entrypoints.push({
           displayName: functionName,
           runtime,
-          path: path.join(functionDirectory, handler),
+          path: functionEntrypointPath,
           memory: memory ?? 1024,
           maxDuration: maxDuration ?? 15,
           environmentVariables,
           streaming,
           package: {
             filePathMap,
           },
         });
@@
         const defaultRoute: NormalizedConfigRoute = {
           path: `/${functionName}`,
           methods: ['ANY'],
           headers: {},
-          destination: handler,
+          destination: functionEntrypointPath,
           handler: handlerType,
         };
@@
                   path: ('src' in r ? r.src : undefined) ?? `/${functionName}`,
                   methods: ['ANY'] as NormalizedConfigRouteMethod[],
                   headers: {},
-                  destination: handler,
+                  destination: functionEntrypointPath,
                   handler: handlerType,
                 }) as NormalizedConfigRoute
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/vercel-build-output-parser.ts` around
lines 125 - 160, The route destination currently uses only the handler filename
which can collide; compute and reuse the full entrypoint path (the same value
pushed into result.entrypoints via path.join(functionDirectory, handler)) and
replace the destinations that use just handler (in defaultRoute and in the
config.routes.map) with that full entrypoint path; update references near
result.entrypoints, defaultRoute, and the config.routes mapping so destination
points to the full entrypoint path instead of handler, keeping handlerType,
functionName, and other fields unchanged.
🧹 Nitpick comments (6)
packages/network-config/src/services/v4-config-parser.ts (2)

157-162: Naming nit: disallowedExtensions contains filenames, not extensions.

.htaccess and .htpasswd are full filenames (dotfiles), not file extensions. The endsWith check works correctly (e.g., it matches path/to/.htaccess), but the variable name is slightly misleading. A name like disallowedFileSuffixes or disallowedFiles would be more accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 157 -
162, Rename the misleading variable disallowedExtensions to something that
reflects they are full filenames/suffixes (e.g., disallowedFiles or
disallowedFileSuffixes) and update all references in this function (the array
declaration and the check using disallowedExtensions.some((ext) =>
assetName.toLowerCase().endsWith(ext.toLowerCase()))) so the loop over allFiles
and the assetName.endsWith check continue to work unchanged.

189-211: Consider narrowing the as NormalizedConfig / as NormalizedConfigRoute casts.

The two as casts (Lines 209, 211) suppress compile-time checking for missing or mistyped fields. If NormalizedConfig gains a required field later, the cast will silently hide the omission. Consider using satisfies NormalizedConfig (or declaring the return type on parse) so TypeScript structurally validates the object shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 189 -
211, The object and route entries are force-cast with "as NormalizedConfig" and
"as NormalizedConfigRoute", which silences missing/mistyped field errors; remove
those casts and instead make TypeScript validate the shape by using "satisfies
NormalizedConfig" on the top-level object (or declaring the parse function's
return type as NormalizedConfig) and ensure each entry returned from routes.map
satisfies NormalizedConfigRoute (the mapped route object and the handler
variable should be typed accordingly) so the compiler enforces required fields
and catches future shape changes.
packages/network-config/src/detection/generate-config.ts (1)

25-32: Add @example to exported function JSDoc.

The exported generateConfig docs include @param/@returns but miss @example.

As per coding guidelines: "Document exported functions with JSDoc comments including @param, @returns, and @example tags".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/generate-config.ts` around lines 25 -
32, The JSDoc for the exported function generateConfig is missing an `@example`
tag; update the comment block above generateConfig to include a concise `@example`
showing typical usage (e.g., calling generateConfig with a detected framework
object and packageManager string and using the returned NormalizedConfig),
keeping the existing `@param` and `@returns` tags and ensuring the example
demonstrates input shape and expected invocation so readers understand how to
call generateConfig.
packages/network-config/src/detection/read-dependencies.test.ts (1)

61-68: Assert the specific error type, not just generic failure.

Line 67 only checks that the effect failed; it should verify ManifestParseError to lock the API contract.

♻️ Proposed test hardening
-    const result = await Effect.runPromiseExit(readDependencies('/project', 'node').pipe(Effect.provide(fs)));
-    expect(result._tag).toBe('Failure');
+    const error = await Effect.runPromise(
+      readDependencies('/project', 'node').pipe(Effect.provide(fs), Effect.flip)
+    );
+    expect(error._tag).toBe('ManifestParseError');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/read-dependencies.test.ts` around lines
61 - 68, The test currently only asserts the Effect failed; update the assertion
to check for the specific ManifestParseError: call readDependencies('/project',
'node') with the invalid JSON fs (from makeTestFs) and after running
Effect.runPromiseExit assert that result._tag === 'Failure' and that
result.cause (or result.failure) contains an instance or kind of
ManifestParseError (use the same ManifestParseError symbol/type used by
readDependencies) so the test verifies the exact error type rather than a
generic failure.
packages/network-config/src/detection/detect-framework.ts (1)

82-91: Add an @example section to detectFramework JSDoc.

The exported function docs should include a concrete usage example alongside @param/@returns.

As per coding guidelines: packages/network-config/**/*.{ts,tsx} requires exported functions to include JSDoc with @param, @returns, and @example tags.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-framework.ts` around lines 82 -
91, Add an `@example` block to the JSDoc for the exported detectFramework function
in detect-framework.ts showing concrete usage: call
detectFramework(projectFolder) with an example absolute path, await the Promise,
and illustrate the shape of the returned DetectionResult (framework name,
packageManager, generated config object). Keep it brief and realistic (e.g.,
show using async/await, sample path like "/path/to/project", and example
returned object fields) and include it alongside the existing `@param` and
`@returns` tags.
packages/network-config/src/detection/read-dependencies.ts (1)

40-49: Add an @example block to the exported readDependencies docs.

The exported API docs are missing a usage example; please include a short @example snippet.

As per coding guidelines: packages/network-config/**/*.{ts,tsx} requires exported functions to include JSDoc with @param, @returns, and @example tags.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/read-dependencies.ts` around lines 40 -
49, The JSDoc for the exported function readDependencies is missing an `@example`;
update the comment block for readDependencies to include a brief `@example`
snippet showing typical usage (e.g., calling await
readDependencies(projectFolder, language) and demonstrating the returned Set of
dependency names or iterating it), while keeping the existing `@param` and
`@returns` tags intact; ensure the example uses realistic variable names
(projectFolder, language) and is short (one to three lines) to satisfy the
packages/network-config JSDoc requirement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/services/project-config.ts`:
- Around line 67-69: If a configPath is provided, skip running detectFramework
so manifest read/parse failures cannot abort config-only resolution; update the
logic around detectFramework (the detection variable) to check for configPath
first and only call
detectFramework(cwd).pipe(Effect.catchTag('FrameworkNotDetectedError', () =>
Effect.succeed(null))) when configPath is absent, otherwise set detection to
null (or Effect.succeed(null)) so the config-only fallback path remains
non-blocking.

In `@packages/network-config/src/detection/generate-config.ts`:
- Around line 73-79: The assets config is discarding the detected assets
directory by setting paths: [] when defaults.assetsDir is present; update the
block that sets config.assets (the if that checks defaults.assetsDir) to
populate paths with the detected directory (e.g., include defaults.assetsDir)
rather than an empty array, preserving any existing asset paths if present and
keeping prefixToStrip, dynamicRoutes and populateCache logic intact so the
detected assetsDir is actually included in config.assets.paths.

In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 221-247: collectFilesRecursively can recurse infinitely on symlink
loops because it calls fs.stat (which follows symlinks); update the function to
avoid following symlinks by using fs.lstat if the FileSystem API supports it
(replace fs.stat(...) with fs.lstat(...) and treat symlink entries as files or
resolve them carefully), or add a visited tracker: add a Set parameter (e.g.,
visitedPaths or visitedInodes) passed through recursive calls and skip directory
entries whose resolved path/inode (use stat/realpath/inode) is already in the
set before recursing; reference symbols: collectFilesRecursively, fs.stat,
fs.lstat, entryRelative, basePath, and ensure the visited Set is updated when
descending into a Directory.
- Around line 88-91: The fallback for undefined config.regions is currently
returning ['global'] which is not a valid Region and breaks type safety; update
the regions assignment (the const regions variable that inspects config.regions)
so that both the explicit 'global' case and the undefined/falsy fallback resolve
to AVAILABLE_REGIONS instead of ['global'], ensuring the result is a Region[];
use config.regions?.includes('global') to map to AVAILABLE_REGIONS and replace
the nullish fallback (?? ['global']) with (?? AVAILABLE_REGIONS) or equivalent
logic that filters out 'global' when present and returns AVAILABLE_REGIONS for
undefined.

---

Duplicate comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 100-141: The code resolves settings once per pattern
(getFunctionSettings(fnPath, config)) causing broad patterns to claim files
before more specific patterns can apply and also reads schedule/symlinks from
the raw func instead of the merged settings. Fix by resolving settings per
matched file: inside the matchedFiles loop call getFunctionSettings(file,
config) (or otherwise merge config for that specific file) and use those
returned settings for runtime/memory/max_duration as well as schedule and
symlinks; keep the existing dedupe check (entrypoints.find...) but ensure it
uses the per-file settings to decide inclusion so specific-pattern settings can
override broad patterns.
- Around line 25-30: The dynamic RegExp construction for matching rules (the new
RegExp(key) used to set regexMatch) can still allow catastrophic backtracking
for valid-but-pathological patterns; update v4-config-parser.ts to guard
RegExp(key) by first rejecting or sanitizing unsafe/oversized patterns — e.g.,
introduce a MAX_PATTERN_LENGTH constant and skip/ignore patterns longer than
that, or run a safety check using a library like safe-regex before constructing
the RegExp, and only call new RegExp(key).test(path) when the pattern passes the
safety/length check; ensure the fallback behavior remains the same (skip regex
matching) when a pattern is rejected.

In `@packages/network-config/src/services/vercel-build-output-parser.ts`:
- Around line 125-160: The route destination currently uses only the handler
filename which can collide; compute and reuse the full entrypoint path (the same
value pushed into result.entrypoints via path.join(functionDirectory, handler))
and replace the destinations that use just handler (in defaultRoute and in the
config.routes.map) with that full entrypoint path; update references near
result.entrypoints, defaultRoute, and the config.routes mapping so destination
points to the full entrypoint path instead of handler, keeping handlerType,
functionName, and other fields unchanged.

---

Nitpick comments:
In `@packages/network-config/src/detection/detect-framework.ts`:
- Around line 82-91: Add an `@example` block to the JSDoc for the exported
detectFramework function in detect-framework.ts showing concrete usage: call
detectFramework(projectFolder) with an example absolute path, await the Promise,
and illustrate the shape of the returned DetectionResult (framework name,
packageManager, generated config object). Keep it brief and realistic (e.g.,
show using async/await, sample path like "/path/to/project", and example
returned object fields) and include it alongside the existing `@param` and
`@returns` tags.

In `@packages/network-config/src/detection/generate-config.ts`:
- Around line 25-32: The JSDoc for the exported function generateConfig is
missing an `@example` tag; update the comment block above generateConfig to
include a concise `@example` showing typical usage (e.g., calling generateConfig
with a detected framework object and packageManager string and using the
returned NormalizedConfig), keeping the existing `@param` and `@returns` tags and
ensuring the example demonstrates input shape and expected invocation so readers
understand how to call generateConfig.

In `@packages/network-config/src/detection/read-dependencies.test.ts`:
- Around line 61-68: The test currently only asserts the Effect failed; update
the assertion to check for the specific ManifestParseError: call
readDependencies('/project', 'node') with the invalid JSON fs (from makeTestFs)
and after running Effect.runPromiseExit assert that result._tag === 'Failure'
and that result.cause (or result.failure) contains an instance or kind of
ManifestParseError (use the same ManifestParseError symbol/type used by
readDependencies) so the test verifies the exact error type rather than a
generic failure.

In `@packages/network-config/src/detection/read-dependencies.ts`:
- Around line 40-49: The JSDoc for the exported function readDependencies is
missing an `@example`; update the comment block for readDependencies to include a
brief `@example` snippet showing typical usage (e.g., calling await
readDependencies(projectFolder, language) and demonstrating the returned Set of
dependency names or iterating it), while keeping the existing `@param` and
`@returns` tags intact; ensure the example uses realistic variable names
(projectFolder, language) and is short (one to three lines) to satisfy the
packages/network-config JSDoc requirement.

In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 157-162: Rename the misleading variable disallowedExtensions to
something that reflects they are full filenames/suffixes (e.g., disallowedFiles
or disallowedFileSuffixes) and update all references in this function (the array
declaration and the check using disallowedExtensions.some((ext) =>
assetName.toLowerCase().endsWith(ext.toLowerCase()))) so the loop over allFiles
and the assetName.endsWith check continue to work unchanged.
- Around line 189-211: The object and route entries are force-cast with "as
NormalizedConfig" and "as NormalizedConfigRoute", which silences
missing/mistyped field errors; remove those casts and instead make TypeScript
validate the shape by using "satisfies NormalizedConfig" on the top-level object
(or declaring the parse function's return type as NormalizedConfig) and ensure
each entry returned from routes.map satisfies NormalizedConfigRoute (the mapped
route object and the handler variable should be typed accordingly) so the
compiler enforces required fields and catches future shape changes.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a69f89 and d526971.

📒 Files selected for processing (18)
  • packages/cli/src/commands/platform/deploy/index.ts
  • packages/cli/src/index.ts
  • packages/cli/src/services/archive.ts
  • packages/cli/src/services/project-config.test.ts
  • packages/cli/src/services/project-config.ts
  • packages/network-config/src/detection/detect-framework.ts
  • packages/network-config/src/detection/generate-config.test.ts
  • packages/network-config/src/detection/generate-config.ts
  • packages/network-config/src/detection/merge-config.test.ts
  • packages/network-config/src/detection/merge-config.ts
  • packages/network-config/src/detection/read-dependencies.test.ts
  • packages/network-config/src/detection/read-dependencies.ts
  • packages/network-config/src/detection/types.ts
  • packages/network-config/src/normalized-config.ts
  • packages/network-config/src/services/v4-config-parser.ts
  • packages/network-config/src/services/vercel-build-output-parser.ts
  • packages/network-config/src/v4/schema.json
  • packages/network-config/src/v4/schema.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/network-config/src/detection/merge-config.test.ts
  • packages/network-config/src/detection/merge-config.ts
  • packages/cli/src/index.ts
  • packages/network-config/src/detection/generate-config.test.ts

Comment thread packages/cli/src/services/project-config.ts
Comment thread packages/network-config/src/detection/generate-config.ts
Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated
Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated
- Make framework detection non-blocking: catch ManifestReadError and
  ManifestParseError so manifest issues don't abort config-only resolution
- Fix undefined regions fallback: use AVAILABLE_REGIONS instead of ['global']
  which is not a valid Region type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Zeryther Zeryther self-assigned this Feb 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/network-config/src/services/v4-config-parser.ts (3)

221-247: ⚠️ Potential issue | 🟡 Minor

Circular-symlink infinite recursion still unguarded.

fs.stat follows symlinks; a directory containing a symlink that points back to an ancestor will recurse until the stack overflows. The previous comment proposing a visited: Set<string> guard has not been addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 221 -
247, collectFilesRecursively can infinite-recurse on circular symlinks because
fs.stat follows links; fix by adding a visited set of resolved absolute paths
and skip revisiting them: update collectFilesRecursively to accept an optional
visited: Set<string> (or create one at the top call), call fs.realpath (or
equivalent) on fullPath before recursing, check if the resolved path is in
visited and skip if so, otherwise add it to visited and proceed; also handle
symlinks by using lstat/realpath semantics so you only follow links
intentionally and avoid re-traversing ancestors.

100-142: ⚠️ Potential issue | 🟠 Major

Per-pattern settings lookup still not resolved per matched file.

getFunctionSettings(fnPath, config) at Line 102 resolves settings against the pattern string (e.g., api/**/*.ts), not against each resolved file. For overlapping patterns the wrong settings are applied. Additionally, Lines 136-137 consume func.schedule / func.symlinks from the raw pattern value instead of the aggregated settings object.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 100 -
142, The code currently calls getFunctionSettings(fnPath, config) once per
pattern, so overlapping patterns get incorrect settings; move the per-file
settings resolution into the matchedFiles loop by calling
getFunctionSettings(file, config) for each resolved file (and fail with
FunctionConfigError if null, mirroring the existing error shape), then use the
resulting settings object for runtime, memory, maxDuration and also for schedule
and symlinks (replace func.schedule / func.symlinks with settings.schedule /
settings.symlinks); ensure streaming continues to derive from settings.runtime.

26-30: ⚠️ Potential issue | 🟠 Major

ReDoS risk from user-supplied regex patterns remains.

The try/catch prevents a SyntaxError crash, but a syntactically valid yet pathological pattern (e.g., (a+)+) still causes catastrophic backtracking at test time. Because key is user-controlled config, the ReDoS surface is real.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 26 -
30, The current try/catch around new RegExp(key).test(path) only avoids syntax
errors but still allows catastrophic backtracking for malicious or pathological
patterns; update the logic in v4-config-parser.ts so you do not call new
RegExp(key).test(path) on unchecked user input (the key variable). Instead,
validate key first with a safe-regex check (or use a library like safe-regex or
regexpp to detect exponential-backtracking patterns) and only construct the
RegExp if it passes; for unsafe patterns fallback to an escaped-literal match or
a glob matcher (e.g., convert to minimatch) so regexMatch is set without
executing a potentially unsafe RegExp. Ensure you reference the existing
regexMatch assignment and the new RegExp(key) usage when making the change.
🧹 Nitpick comments (3)
packages/network-config/src/services/v4-config-parser.ts (3)

157-161: Hoist disallowedExtensions to module scope as DISALLOWED_ASSET_EXTENSIONS.

It's a stable constant literal array recreated on every parse invocation. Per coding guidelines, constant literal arrays should use UPPER_SNAKE_CASE and be declared at module scope (and prefer as const to derive the union type).

♻️ Proposed refactor
+const DISALLOWED_ASSET_EXTENSIONS = ['.htaccess', '.htpasswd'] as const;
+
 export class V4ConfigParser extends Effect.Service<V4ConfigParser>()('V4ConfigParser', {
   ...
-          const disallowedExtensions = ['.htaccess', '.htpasswd'];
-
-          for (const assetName of allFiles) {
-            if (disallowedExtensions.some((ext) => assetName.toLowerCase().endsWith(ext.toLowerCase()))) {
+          for (const assetName of allFiles) {
+            if (DISALLOWED_ASSET_EXTENSIONS.some((ext) => assetName.toLowerCase().endsWith(ext.toLowerCase()))) {

As per coding guidelines: "Use UPPER_SNAKE_CASE for constants" and "Prefer as const assertions for literal arrays that derive union types."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 157 -
161, Move the local const disallowedExtensions out of the parse function to
module scope and rename it to DISALLOWED_ASSET_EXTENSIONS (use
UPPER_SNAKE_CASE); make it a literal readonly array using "as const" so its
union type is preserved, then update the loop that currently references
disallowedExtensions (inside the parse logic that iterates allFiles) to use
DISALLOWED_ASSET_EXTENSIONS instead. Ensure any type annotations that previously
referred to the local variable are adjusted to use typeof
DISALLOWED_ASSET_EXTENSIONS[number] if needed.

221-225: Use function declaration syntax for collectFilesRecursively.

The function is a non-trivial multi-statement Effect.gen body and should follow the project convention for larger functions.

♻️ Proposed refactor
-const collectFilesRecursively = (
+function collectFilesRecursively(
   fs: FileSystem.FileSystem,
   basePath: string,
-  relativePath: string = ''
-): Effect.Effect<string[], never, never> =>
-  Effect.gen(function* () {
+  relativePath = ''
+): Effect.Effect<string[], never, never> {
+  return Effect.gen(function* () {
     ...
-  });
+  });
+}

Based on learnings: "Use function declaration syntax (not arrow functions) for larger or async functions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 221 -
225, Convert the arrow function collectFilesRecursively to a function
declaration using the same name, parameters (fs: FileSystem.FileSystem,
basePath: string, relativePath: string = ''), and return type
Effect.Effect<string[], never, never>; move the existing Effect.gen
multi-statement body intact into the new function declaration body so behavior
is unchanged, preserving all internal references and exports/usages of
collectFilesRecursively.

209-211: Unsafe as casts may hide missing required fields.

as NormalizedConfigRoute (Line 209) and as NormalizedConfig (Line 211) suppress compiler checks. If either type gains a required property, these casts will silently produce incomplete objects at runtime.

♻️ Proposed refactor
-          } as NormalizedConfigRoute;
+          };
 ...
-      } as NormalizedConfig;
+      };

Let TypeScript verify the shapes structurally — drop the casts and fix any resulting type errors explicitly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 209 -
211, The code is using unsafe "as NormalizedConfigRoute" and "as
NormalizedConfig" casts which hide missing required fields; remove those casts
and let TypeScript enforce the shapes for the mapped route object and the
returned config object. Edit the mapping that currently returns the route (the
object being cast to NormalizedConfigRoute) to explicitly include all required
properties of NormalizedConfigRoute (or transform/validate source fields so they
match), and update the outer object returned (the one cast to NormalizedConfig)
to include all required NormalizedConfig members; run the compiler, fix any
resulting type errors by adding or deriving the missing properties or using
explicit validation/constructors, and prefer returning a properly-typed value
rather than forcing with "as".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 104-111: The null-check branch for "settings" in
v4-config-parser.ts is dead because getFunctionSettings(fnPath, config) always
returns a match for keys from config.functions; remove the unreachable if
(settings == null) { ... } block (including the FunctionConfigError
construction) and rely on the invariant that getFunctionSettings returns a
value, or alternatively replace it with a concise internal assertion (e.g., an
invariant/error with clear message) if you want an explicit guard for future
changes; update references around getFunctionSettings, settings, and
FunctionConfigError accordingly.

---

Duplicate comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 221-247: collectFilesRecursively can infinite-recurse on circular
symlinks because fs.stat follows links; fix by adding a visited set of resolved
absolute paths and skip revisiting them: update collectFilesRecursively to
accept an optional visited: Set<string> (or create one at the top call), call
fs.realpath (or equivalent) on fullPath before recursing, check if the resolved
path is in visited and skip if so, otherwise add it to visited and proceed; also
handle symlinks by using lstat/realpath semantics so you only follow links
intentionally and avoid re-traversing ancestors.
- Around line 100-142: The code currently calls getFunctionSettings(fnPath,
config) once per pattern, so overlapping patterns get incorrect settings; move
the per-file settings resolution into the matchedFiles loop by calling
getFunctionSettings(file, config) for each resolved file (and fail with
FunctionConfigError if null, mirroring the existing error shape), then use the
resulting settings object for runtime, memory, maxDuration and also for schedule
and symlinks (replace func.schedule / func.symlinks with settings.schedule /
settings.symlinks); ensure streaming continues to derive from settings.runtime.
- Around line 26-30: The current try/catch around new RegExp(key).test(path)
only avoids syntax errors but still allows catastrophic backtracking for
malicious or pathological patterns; update the logic in v4-config-parser.ts so
you do not call new RegExp(key).test(path) on unchecked user input (the key
variable). Instead, validate key first with a safe-regex check (or use a library
like safe-regex or regexpp to detect exponential-backtracking patterns) and only
construct the RegExp if it passes; for unsafe patterns fallback to an
escaped-literal match or a glob matcher (e.g., convert to minimatch) so
regexMatch is set without executing a potentially unsafe RegExp. Ensure you
reference the existing regexMatch assignment and the new RegExp(key) usage when
making the change.

---

Nitpick comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 157-161: Move the local const disallowedExtensions out of the
parse function to module scope and rename it to DISALLOWED_ASSET_EXTENSIONS (use
UPPER_SNAKE_CASE); make it a literal readonly array using "as const" so its
union type is preserved, then update the loop that currently references
disallowedExtensions (inside the parse logic that iterates allFiles) to use
DISALLOWED_ASSET_EXTENSIONS instead. Ensure any type annotations that previously
referred to the local variable are adjusted to use typeof
DISALLOWED_ASSET_EXTENSIONS[number] if needed.
- Around line 221-225: Convert the arrow function collectFilesRecursively to a
function declaration using the same name, parameters (fs: FileSystem.FileSystem,
basePath: string, relativePath: string = ''), and return type
Effect.Effect<string[], never, never>; move the existing Effect.gen
multi-statement body intact into the new function declaration body so behavior
is unchanged, preserving all internal references and exports/usages of
collectFilesRecursively.
- Around line 209-211: The code is using unsafe "as NormalizedConfigRoute" and
"as NormalizedConfig" casts which hide missing required fields; remove those
casts and let TypeScript enforce the shapes for the mapped route object and the
returned config object. Edit the mapping that currently returns the route (the
object being cast to NormalizedConfigRoute) to explicitly include all required
properties of NormalizedConfigRoute (or transform/validate source fields so they
match), and update the outer object returned (the one cast to NormalizedConfig)
to include all required NormalizedConfig members; run the compiler, fix any
resulting type errors by adding or deriving the missing properties or using
explicit validation/constructors, and prefer returning a properly-typed value
rather than forcing with "as".

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d526971 and 3d7b00f.

📒 Files selected for processing (2)
  • packages/cli/src/services/project-config.ts
  • packages/network-config/src/services/v4-config-parser.ts

Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated
Replace visited-set approach with a depth limit (MAX_ASSET_DEPTH=100) to
prevent infinite recursion from symlink loops where paths grow monotonically.
Add tests for nested directory asset collection, disallowed extension
filtering, and symlink loop termination.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Zeryther and others added 4 commits February 25, 2026 06:50
Fixes 12 review issues: ReDoS protection with safe-regex2, consistent
Path.Path usage across all modules, depth-limited directory recursion,
Ajv instance reuse, install command deduplication for PHP frameworks,
Vite static-only config, error handling in Case C, mutation fix in
loadFunctions, deduplicated test utilities, and type-safe CLI test mocks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace dependency-injection-via-arguments anti-pattern in
collectFilesRecursively with proper Effect context yielding and
convert it to Effect.fn for tracing. Add comment explaining why
processDirectory uses closure instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract pure helpers (matchesPattern, resolveRouteHandler, resolveRegions,
  mapRoute) as plain functions outside Effect context
- Decompose monolithic parse method into focused Effect.fn helpers
  (parseEntrypoints, collectAssets)
- Use yieldable errors (yield* new FunctionConfigError) instead of
  yield* Effect.fail(new ...)
- Simplify getFunctionSettings with object spread over repetitive
  null-checks
- Move constants above their consumers
- Add accessors: true to service for direct static method access
- Update tests to use accessor pattern (V4ConfigParser.parse directly)
- Replace as any casts with as ConfigV4 in v4/parse.test.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Feb 25, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedsafe-regex2@​5.0.01001008686100

View full report

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
packages/network-config/src/services/v4-config-parser.ts (1)

44-45: ReDoS guard via safe-regex2 — LGTM.

safeRegex(pattern) gates the new RegExp call, and the surrounding try/catch handles any remaining SyntaxError. The ast-grep warning is a false positive here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 44 -
45, False positive ReDoS warning: safeRegex(pattern) already guards the new
RegExp(pattern) call and the surrounding try/catch handles SyntaxError, so no
functional change is needed; retain the safeRegex(pattern) guard and the
try/catch around new RegExp in v4-config-parser (referencing safeRegex and the
RegExp construction) and, if you want to silence the ast-grep alert, add a short
inline comment explaining the guard or an explicit linter suppression next to
the check.
🧹 Nitpick comments (9)
packages/network-config/src/v4/parse.test.ts (1)

18-26: Convert the dynamic yaml import to a static top-level import.

yaml is a declared project dependency — there is no reason to dynamically await import('yaml') inside the helper. The dynamic import is the sole reason readFixture is async, which forces every call site to await and adds unnecessary overhead in tests. A static import allows the function to be synchronous.

♻️ Proposed refactor
+import { parse as parseYaml } from 'yaml';
 ...
-async function readFixture(filePath: string): Promise<Record<string, unknown>> {
+function readFixture(filePath: string): Record<string, unknown> {
   const content = fs.readFileSync(filePath, 'utf8');
   const ext = path.extname(filePath).toLowerCase();
   if (ext === '.json') {
     return JSON.parse(content) as Record<string, unknown>;
   }
-  const { parse: parseYaml } = await import('yaml');
   return parseYaml(content) as Record<string, unknown>;
 }

Call sites (await readFixture(...)) can then drop the await.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/v4/parse.test.ts` around lines 18 - 26, The
helper readFixture is unnecessarily async due to the dynamic await
import('yaml'); replace that with a static top-level import (e.g., import {
parse as parseYaml } from 'yaml'), remove the async keyword from readFixture and
the internal await, and make it return synchronously for non-JSON files; update
call sites that do "await readFixture(...)" to call readFixture(...) without
awaiting. Ensure the function still handles .json and YAML by using parseYaml
for YAML parsing and keep the return type Record<string, unknown>.
packages/network-config/src/build-output-v3/get-default-path-map.ts (2)

28-47: Prefer function declaration for the larger helper.

processDirectory is substantial enough that a function declaration is clearer and matches the project rule.

♻️ Suggested refactor
-  const processDirectory = (currentPath: string, basePath: string = '', depth: number = 0): Effect.Effect<void> =>
-    Effect.gen(function* () {
-      if (depth > MAX_DEPTH) return;
-
-      const entries = yield* fs.readDirectory(currentPath).pipe(Effect.catchAll(() => Effect.succeed([] as string[])));
-
-      for (const name of entries) {
-        const fullPath = pathService.join(currentPath, name);
-        const relativePath = basePath ? pathService.join(basePath, name) : name;
-
-        const stat = yield* fs.stat(fullPath).pipe(Effect.catchAll(() => Effect.succeed(null)));
-        if (!stat) continue;
-
-        if (stat.type === 'Directory') {
-          yield* processDirectory(fullPath, relativePath, depth + 1);
-        } else {
-          result[fullPath] = relativePath;
-        }
-      }
-    });
+  function processDirectory(currentPath: string, basePath: string = '', depth: number = 0): Effect.Effect<void> {
+    return Effect.gen(function* () {
+      if (depth > MAX_DEPTH) return;
+
+      const entries = yield* fs.readDirectory(currentPath).pipe(Effect.catchAll(() => Effect.succeed([] as string[])));
+
+      for (const name of entries) {
+        const fullPath = pathService.join(currentPath, name);
+        const relativePath = basePath ? pathService.join(basePath, name) : name;
+
+        const stat = yield* fs.stat(fullPath).pipe(Effect.catchAll(() => Effect.succeed(null)));
+        if (!stat) continue;
+
+        if (stat.type === 'Directory') {
+          yield* processDirectory(fullPath, relativePath, depth + 1);
+        } else {
+          result[fullPath] = relativePath;
+        }
+      }
+    });
+  }

As per coding guidelines: "Use function declaration syntax for larger/async functions: export async function fn(...) { }".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/build-output-v3/get-default-path-map.ts` around
lines 28 - 47, The helper processDirectory is large and should be converted from
a const-assigned Effect.gen arrow to a named function declaration to follow the
project's style; refactor the const processDirectory (and its Effect.gen body)
into a function declaration (e.g., function processDirectory(currentPath:
string, basePath = '', depth = 0): Effect.Effect<void>) preserving the same
logic and references to MAX_DEPTH, fs.readDirectory, fs.stat, pathService.join
and the result mapping, and ensure callers still invoke processDirectory(...) as
before.

4-13: Add @example to exported function JSDoc.

The exported function docs are missing the required @example section.

✍️ Suggested doc update
 /**
  * Creates a mapping of absolute file paths to their relative paths
  * starting from the provided directory.
  *
  * This is the default path map that will be used for Vercel-based functions,
  * when the function does not specify a filePathMap.
  *
  * `@param` directory - The absolute path of the directory to process
  * `@returns` A record where keys are absolute file paths and values are relative paths
+ * `@example`
+ * const pathMap = yield* getDefaultPathMap('/workspace/.vercel/output/functions');
  */

As per coding guidelines: "Document exported functions with JSDoc comments including @param, @returns, and @example tags".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/build-output-v3/get-default-path-map.ts` around
lines 4 - 13, The JSDoc for the exported function (getDefaultPathMap) is missing
an `@example`; update the JSDoc block above the exported function
(getDefaultPathMap) to include an `@example` that shows typical usage: calling the
function with a sample absolute directory path and the expected shape of the
returned Record<string,string> (absolute -> relative) so readers know
input/output format; keep the example concise and use realistic values and a
short note about asynchronous/sync behavior if applicable.
packages/network-config/src/detection/detect-framework.ts (1)

90-100: Complete detectFramework JSDoc with an @example.

The exported API doc is missing the required example section.

As per coding guidelines: "Document exported functions with JSDoc comments including @param, @returns, and @example tags."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-framework.ts` around lines 90 -
100, The JSDoc for the exported function detectFramework is missing an `@example`;
add a concise example to the existing comment showing typical usage (call
detectFramework with a project root string and handling the returned
DetectionResult), keeping it brief and illustrating input and expected shape of
output; ensure the `@example` appears alongside the existing `@param` and `@returns`
tags in the JSDoc for export detectFramework.
packages/network-config/src/detection/read-dependencies.ts (1)

40-53: Add @example to readDependencies JSDoc.

The exported API docs are missing the @example section.

As per coding guidelines: "Document exported functions with JSDoc comments including @param, @returns, and @example tags."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/read-dependencies.ts` around lines 40 -
53, Add an `@example` section to the JSDoc for the exported function
readDependencies showing a minimal usage example: call readDependencies with a
sample projectFolder path and a language constant (or string) and demonstrate
awaiting/handling the returned Set of dependency names; include both sync/async
usage form appropriate for Effect.fn (e.g., yielding/awaiting the effect) and
show expected return shape (Set<string>) so consumers understand how to call
readDependencies and what it returns. Ensure the new `@example` is placed in the
existing JSDoc block above the readDependencies declaration and follows the same
style as `@param/`@returns tags.
packages/network-config/src/detection/generate-config.ts (1)

25-33: Add an @example block to the exported generateConfig JSDoc.

The exported function docs currently include @param/@returns but miss @example.

✍️ Suggested doc update
 /**
  * Generates a NormalizedConfig from a framework definition and detected package manager.
  * Prepends the install command and builds a complete deployment configuration.
  *
  * `@param` framework - The detected framework definition
  * `@param` packageManager - The detected package manager
  * `@returns` A complete NormalizedConfig with framework defaults
+ * `@example`
+ * const cfg = yield* generateConfig(framework, 'pnpm');
  */

As per coding guidelines: "Document exported functions with JSDoc comments including @param, @returns, and @example tags."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/generate-config.ts` around lines 25 -
33, Add an `@example` JSDoc block to the exported generateConfig function showing
a minimal usage example: call generateConfig with a sample framework object
(matching the framework type used in the file) and a packageManager string
(e.g., "npm" or "yarn"), demonstrating how to yield/await the Effect result and
showing the shape of the returned NormalizedConfig; update the comment above
export const generateConfig = Effect.fn('generateConfig')(function* (...) to
include this `@example` so consumers see how to invoke generateConfig and what the
resolved config looks like.
packages/network-config/src/services/raw-config-reader.test.ts (1)

58-177: Add a regression test for YAML values that parse to null.

Current cases miss the null parse path ('null' or whitespace-only YAML). Adding one protects against uncaught runtime errors in readRawConfig.

✅ Suggested test case
+    it('should fail with ConfigFileParseError when YAML parses to null', async () => {
+      const result = await Effect.runPromise(
+        Effect.gen(function* () {
+          const reader = yield* RawConfigReader;
+          return yield* reader.readRawConfig('/project/config.yaml');
+        }).pipe(
+          Effect.catchTag('ConfigFileParseError', (err) =>
+            Effect.succeed({ _tag: 'caught' as const, filePath: err.filePath })
+          ),
+          Effect.provide(RawConfigReader.Default),
+          Effect.provide(Layer.merge(makeTestFs({ '/project/config.yaml': 'null' }), TestPathLayer))
+        )
+      );
+      expect(result).toMatchObject({ _tag: 'caught', filePath: '/project/config.yaml' });
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.test.ts` around lines
58 - 177, Add a regression test to ensure YAML values that parse to null are
handled: create a new it() in the readRawConfig suite that uses runWithFs (or
Effect.runPromise + Layer.merge(makeTestFs(...), TestPathLayer)) to supply a
file (e.g., '/project/config.yaml') whose contents are 'null' and another case
with whitespace-only content, then call RawConfigReader.readRawConfig and assert
it fails with ConfigFileEmptyError (or the expected error handling path) or
returns an empty object when disableVersionCheck is true; reference
RawConfigReader.readRawConfig, RawConfigReader.Default, runWithFs, makeTestFs
and TestPathLayer to locate where to add the test.
packages/network-config/src/detection/detect-framework.test.ts (1)

206-223: The invalid-regex test doesn't currently exercise an invalid regex path.

This case never injects an invalid matchContent pattern, so it doesn't verify the behavior the test name describes. Either inject a bad detector pattern in the fixture setup or rename the test to match the actual assertion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/detect-framework.test.ts` around lines
206 - 223, The test name claims to exercise an invalid matchContent regex but
never injects one; update the test that calls detectFramework('/project') (which
currently composes fs via Layer.merge, makeTestFs and TestPathLayer) to inject a
framework detector with an invalid matchContent pattern (e.g., a malformed regex
string) into the detection fixtures before calling detectFramework, or
alternatively rename the test to reflect the current behavior; target symbols:
detectFramework, Layer.merge, makeTestFs, and TestPathLayer so the injected bad
detector is picked up by the detection pipeline.
packages/network-config/src/detection/generate-config.test.ts (1)

145-168: Harden install-command assertions to prevent hidden secondary installers.

Lines 156-167 only guard duplicate composer install. They won’t fail if another install step (e.g., bun install) slips into framework commands. Add a check that commands after index 0 do not contain any install command.

♻️ Suggested test hardening
   expect(result.commands[0]).toBe('composer install --prefer-dist --optimize-autoloader --no-dev');
   expect(result.commands).not.toContain('composer install');
+  expect(
+    result.commands.slice(1).some((command) => /\b(?:npm|pnpm|yarn|bun|composer)\s+install\b/.test(command))
+  ).toBe(false);

Based on learnings: framework commands in packages/network-config/src/detection/ are intended to be framework-specific build commands only, while install commands are prepended by generateConfig().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/generate-config.test.ts` around lines
145 - 168, The test allows a hidden secondary installer in later commands; after
calling generateConfig (used in tests referencing generateConfig, symfony, and
customInstallFramework) assert that every command after index 0 does not contain
any install operation (e.g., does not include the word "install" or start with
"composer install" / other installer verbs); specifically check
result.commands[0] remains the intended install entry and then assert
result.commands.slice(1).every(...) is true to ensure no subsequent command is
an install step.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/services/project-config.ts`:
- Around line 90-112: The current Case A calls parseConfig (which runs
postProcessConfig) before mergeWithFrameworkDefaults, causing stale pre-merge
errors to persist; change the flow so you parse the user config without
post-processing, then call mergeWithFrameworkDefaults(userConfig,
detection.config), and only after merging run postProcessConfig on the merged
config (wrapped with wrapParseErrors) to produce the final config used for
validation; update the branch where config is set (the code that currently calls
parseConfig and then mergeWithFrameworkDefaults) to instead obtain a raw parsed
user config, merge framework defaults, then postProcessConfig the merged result
and assign that to config (and keep resolvedConfigPath and framework assignment
as-is).

In `@packages/network-config/src/detection/ADDING_FRAMEWORKS.md`:
- Around line 112-113: Update the "commands" guidance row in
ADDING_FRAMEWORKS.md to clarify that the general rule is to omit the install
command (do not include npm/pnpm/yarn install) but allow explicit install/build
commands in exceptional framework definitions that require platform
compatibility (e.g., Laravel and Symfony entries which intentionally include
commands like `bun install`); reference the `commands` table row and the
Laravel/Symfony examples around the `entrypoint`/framework definitions so the
text states "omit install commands unless a framework's definition requires a
hardcoded install for platform compatibility (see Laravel/Symfony examples)".
- Around line 9-18: The fenced code block that shows the directory tree (the
block starting with ``` and containing "packages/network-config/src/detection/")
is missing a language tag and triggers MD040; fix it by changing the opening
fence to include a language (e.g., replace ``` with ```text) so the block is
treated as plain text and the linter warning is resolved.

In `@packages/network-config/src/services/raw-config-reader.ts`:
- Around line 64-95: The parsed result from JSON/YAML can be null; update the
post-parse guards around the parsed variable (the local parsed in
raw-config-reader.ts produced by parseYaml/JSON.parse) to treat null the same as
undefined and non-object values: if parsed is null or typeof parsed !== 'object'
then return Effect.fail(new ConfigFileParseError(...)); and only check
parsed.version when parsed is a non-null object (i.e. after that guard),
preserving the existing ConfigVersionError logic and honoring
options?.disableVersionCheck.

In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 225-254: The outer Effect.gen wrapper is an unused generator (no
yield*) around the parse Effect; replace it with Effect.succeed to avoid the
lint error: move the const parse = Effect.fn('V4ConfigParser.parse')(function*
(config: ConfigV4, projectFolder: string) { ... }) out of the Effect.gen block
(i.e., export/return parse directly) and wrap the outer return in
Effect.succeed({ parse }) instead of Effect.gen(function* () { ... }), ensuring
the parse definition (and its internal yields like yield* parseEntrypoints)
remains unchanged.
- Line 87: The conditional return uses an unnecessary type assertion which CI
flags; in the function referencing configRegions, replace the asserted return
"AVAILABLE_REGIONS as Region[]" with a plain return of AVAILABLE_REGIONS (i.e.,
remove "as Region[]") since AVAILABLE_REGIONS is already typed as Region[];
update the return in the branch that checks configRegions?.includes('global')
and ensure no other redundant assertions remain for AVAILABLE_REGIONS or Region
in v4-config-parser.

---

Duplicate comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 44-45: False positive ReDoS warning: safeRegex(pattern) already
guards the new RegExp(pattern) call and the surrounding try/catch handles
SyntaxError, so no functional change is needed; retain the safeRegex(pattern)
guard and the try/catch around new RegExp in v4-config-parser (referencing
safeRegex and the RegExp construction) and, if you want to silence the ast-grep
alert, add a short inline comment explaining the guard or an explicit linter
suppression next to the check.

---

Nitpick comments:
In `@packages/network-config/src/build-output-v3/get-default-path-map.ts`:
- Around line 28-47: The helper processDirectory is large and should be
converted from a const-assigned Effect.gen arrow to a named function declaration
to follow the project's style; refactor the const processDirectory (and its
Effect.gen body) into a function declaration (e.g., function
processDirectory(currentPath: string, basePath = '', depth = 0):
Effect.Effect<void>) preserving the same logic and references to MAX_DEPTH,
fs.readDirectory, fs.stat, pathService.join and the result mapping, and ensure
callers still invoke processDirectory(...) as before.
- Around line 4-13: The JSDoc for the exported function (getDefaultPathMap) is
missing an `@example`; update the JSDoc block above the exported function
(getDefaultPathMap) to include an `@example` that shows typical usage: calling the
function with a sample absolute directory path and the expected shape of the
returned Record<string,string> (absolute -> relative) so readers know
input/output format; keep the example concise and use realistic values and a
short note about asynchronous/sync behavior if applicable.

In `@packages/network-config/src/detection/detect-framework.test.ts`:
- Around line 206-223: The test name claims to exercise an invalid matchContent
regex but never injects one; update the test that calls
detectFramework('/project') (which currently composes fs via Layer.merge,
makeTestFs and TestPathLayer) to inject a framework detector with an invalid
matchContent pattern (e.g., a malformed regex string) into the detection
fixtures before calling detectFramework, or alternatively rename the test to
reflect the current behavior; target symbols: detectFramework, Layer.merge,
makeTestFs, and TestPathLayer so the injected bad detector is picked up by the
detection pipeline.

In `@packages/network-config/src/detection/detect-framework.ts`:
- Around line 90-100: The JSDoc for the exported function detectFramework is
missing an `@example`; add a concise example to the existing comment showing
typical usage (call detectFramework with a project root string and handling the
returned DetectionResult), keeping it brief and illustrating input and expected
shape of output; ensure the `@example` appears alongside the existing `@param` and
`@returns` tags in the JSDoc for export detectFramework.

In `@packages/network-config/src/detection/generate-config.test.ts`:
- Around line 145-168: The test allows a hidden secondary installer in later
commands; after calling generateConfig (used in tests referencing
generateConfig, symfony, and customInstallFramework) assert that every command
after index 0 does not contain any install operation (e.g., does not include the
word "install" or start with "composer install" / other installer verbs);
specifically check result.commands[0] remains the intended install entry and
then assert result.commands.slice(1).every(...) is true to ensure no subsequent
command is an install step.

In `@packages/network-config/src/detection/generate-config.ts`:
- Around line 25-33: Add an `@example` JSDoc block to the exported generateConfig
function showing a minimal usage example: call generateConfig with a sample
framework object (matching the framework type used in the file) and a
packageManager string (e.g., "npm" or "yarn"), demonstrating how to yield/await
the Effect result and showing the shape of the returned NormalizedConfig; update
the comment above export const generateConfig =
Effect.fn('generateConfig')(function* (...) to include this `@example` so
consumers see how to invoke generateConfig and what the resolved config looks
like.

In `@packages/network-config/src/detection/read-dependencies.ts`:
- Around line 40-53: Add an `@example` section to the JSDoc for the exported
function readDependencies showing a minimal usage example: call readDependencies
with a sample projectFolder path and a language constant (or string) and
demonstrate awaiting/handling the returned Set of dependency names; include both
sync/async usage form appropriate for Effect.fn (e.g., yielding/awaiting the
effect) and show expected return shape (Set<string>) so consumers understand how
to call readDependencies and what it returns. Ensure the new `@example` is placed
in the existing JSDoc block above the readDependencies declaration and follows
the same style as `@param/`@returns tags.

In `@packages/network-config/src/services/raw-config-reader.test.ts`:
- Around line 58-177: Add a regression test to ensure YAML values that parse to
null are handled: create a new it() in the readRawConfig suite that uses
runWithFs (or Effect.runPromise + Layer.merge(makeTestFs(...), TestPathLayer))
to supply a file (e.g., '/project/config.yaml') whose contents are 'null' and
another case with whitespace-only content, then call
RawConfigReader.readRawConfig and assert it fails with ConfigFileEmptyError (or
the expected error handling path) or returns an empty object when
disableVersionCheck is true; reference RawConfigReader.readRawConfig,
RawConfigReader.Default, runWithFs, makeTestFs and TestPathLayer to locate where
to add the test.

In `@packages/network-config/src/v4/parse.test.ts`:
- Around line 18-26: The helper readFixture is unnecessarily async due to the
dynamic await import('yaml'); replace that with a static top-level import (e.g.,
import { parse as parseYaml } from 'yaml'), remove the async keyword from
readFixture and the internal await, and make it return synchronously for
non-JSON files; update call sites that do "await readFixture(...)" to call
readFixture(...) without awaiting. Ensure the function still handles .json and
YAML by using parseYaml for YAML parsing and keep the return type Record<string,
unknown>.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d7b00f and ab342f3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • packages/cli/CLAUDE.md
  • packages/cli/src/services/project-config.test.ts
  • packages/cli/src/services/project-config.ts
  • packages/network-config/package.json
  • packages/network-config/src/build-output-v3/get-default-path-map.test.ts
  • packages/network-config/src/build-output-v3/get-default-path-map.ts
  • packages/network-config/src/detection/ADDING_FRAMEWORKS.md
  • packages/network-config/src/detection/detect-framework.test.ts
  • packages/network-config/src/detection/detect-framework.ts
  • packages/network-config/src/detection/detect-package-manager.test.ts
  • packages/network-config/src/detection/detect-package-manager.ts
  • packages/network-config/src/detection/frameworks/symfony.ts
  • packages/network-config/src/detection/frameworks/vite.ts
  • packages/network-config/src/detection/generate-config.test.ts
  • packages/network-config/src/detection/generate-config.ts
  • packages/network-config/src/detection/read-dependencies.test.ts
  • packages/network-config/src/detection/read-dependencies.ts
  • packages/network-config/src/detection/types.ts
  • packages/network-config/src/services/raw-config-reader.test.ts
  • packages/network-config/src/services/raw-config-reader.ts
  • packages/network-config/src/services/schema-validator.ts
  • packages/network-config/src/services/v4-config-parser.test.ts
  • packages/network-config/src/services/v4-config-parser.ts
  • packages/network-config/src/services/vercel-build-output-parser.test.ts
  • packages/network-config/src/services/vercel-build-output-parser.ts
  • packages/network-config/src/test-utils.ts
  • packages/network-config/src/v4/parse.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cli/CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/network-config/src/build-output-v3/get-default-path-map.test.ts
  • packages/network-config/src/services/vercel-build-output-parser.test.ts
  • packages/network-config/package.json
  • packages/network-config/src/services/schema-validator.ts
  • packages/network-config/src/detection/detect-package-manager.test.ts
  • packages/network-config/src/detection/detect-package-manager.ts
  • packages/network-config/src/services/v4-config-parser.test.ts
  • packages/network-config/src/detection/frameworks/vite.ts
  • packages/network-config/src/detection/frameworks/symfony.ts
  • packages/network-config/src/detection/types.ts
  • packages/network-config/src/detection/read-dependencies.test.ts

Comment thread packages/cli/src/services/project-config.ts
Comment thread packages/network-config/src/detection/ADDING_FRAMEWORKS.md Outdated
Comment thread packages/network-config/src/detection/ADDING_FRAMEWORKS.md Outdated
Comment thread packages/network-config/src/services/raw-config-reader.ts
Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated
Comment thread packages/network-config/src/services/v4-config-parser.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/network-config/src/services/v4-config-parser.ts (1)

225-254: ⚠️ Potential issue | 🟡 Minor

Yield-less Effect.gen — Biome CI still failing.

The outer Effect.gen(function* () { ... }) at Line 225 never calls yield*, so Biome reports lint/correctness/useYield. Replace with Effect.succeed.

🐛 Proposed fix
-  effect: Effect.gen(function* () {
-    const parse = Effect.fn('V4ConfigParser.parse')(function* (config: ConfigV4, projectFolder: string) {
-      const entrypoints = yield* parseEntrypoints(config, projectFolder);
-      const assets = yield* collectAssets(config, projectFolder);
-
-      return {
-        regions: resolveRegions(config.regions),
-        assets: {
-          paths: assets.sort(),
-          prefixToStrip: (config.assets ?? '') + '/',
-          dynamicRoutes: true,
-          populateCache: config.populateAssetCache ?? false,
-        },
-        environmentVariables: config.env ?? {},
-        commands: config.build_commands ?? [],
-        entrypoints,
-        errors: [],
-        warnings: [],
-        routes: (config.routes ?? []).map(mapRoute),
-      } as NormalizedConfig;
-    });
-
-    return { parse };
-  }),
+  effect: Effect.succeed({
+    parse: Effect.fn('V4ConfigParser.parse')(function* (config: ConfigV4, projectFolder: string) {
+      const entrypoints = yield* parseEntrypoints(config, projectFolder);
+      const assets = yield* collectAssets(config, projectFolder);
+
+      return {
+        regions: resolveRegions(config.regions),
+        assets: {
+          paths: assets.sort(),
+          prefixToStrip: (config.assets ?? '') + '/',
+          dynamicRoutes: true,
+          populateCache: config.populateAssetCache ?? false,
+        },
+        environmentVariables: config.env ?? {},
+        commands: config.build_commands ?? [],
+        entrypoints,
+        errors: [],
+        warnings: [],
+        routes: (config.routes ?? []).map(mapRoute),
+      } as NormalizedConfig;
+    }),
+  }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 225 -
254, The outer wrapper uses Effect.gen(function* () { ... }) but never yields,
causing the lint error; replace the generator wrapper with a simple
Effect.succeed returning the parse effect. Concretely, keep the inner const
parse = Effect.fn('V4ConfigParser.parse')(function* (...) { ... }) as-is, then
return Effect.succeed({ parse }) instead of using Effect.gen; ensure the final
exported object is the Effect created by Effect.succeed and that
parseEntrypoints, collectAssets and mapRoute usages remain unchanged.
🧹 Nitpick comments (1)
packages/network-config/src/services/v4-config-parser.ts (1)

117-145: Non-null assertions (settings!) are avoidable with return yield*.

Because yield* new FunctionConfigError(...) (Line 118) lacks a return, TypeScript's control-flow analysis doesn't treat it as a terminator, leaving settings as ConfigV4FunctionSettings | undefined in the rest of the loop body. Adding return makes TypeScript narrow correctly and eliminates the five ! assertions.

♻️ Proposed refactor
     if (settings == null) {
-      yield* new FunctionConfigError({
+      return yield* new FunctionConfigError({
         message: `Settings invalid for function at path '${fnPath}'`,
         functionPath: fnPath,
       });
     }
 
     ...
 
         entrypoints.push({
           path: file,
-          runtime: settings!.runtime ?? 'node-20',
-          memory: settings!.memory ?? 128,
-          maxDuration: settings!.max_duration ?? 30,
+          runtime: settings.runtime ?? 'node-20',
+          memory: settings.memory ?? 128,
+          maxDuration: settings.max_duration ?? 30,
           schedule: func.schedule,
           symlinks: func.symlinks,
           streaming:
-            settings!.runtime == null || settings!.runtime.startsWith('node-') || settings!.runtime.startsWith('bun-'),
+            settings.runtime == null || settings.runtime.startsWith('node-') || settings.runtime.startsWith('bun-'),
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 117 -
145, The null-check branch that currently does "yield* new
FunctionConfigError({...})" doesn't terminate control flow so TypeScript can't
narrow "settings" and the code uses several non-null assertions ("settings!");
change that line to "return yield* new FunctionConfigError({...})" so the
function exits on error and TypeScript will treat "settings" as defined, then
remove the five "settings!" assertions in the entrypoint push (references:
settings, FunctionConfigError, entrypoints, matchedFiles, fnPath) so
runtime/defaults are used without non-null bangs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 225-254: The outer wrapper uses Effect.gen(function* () { ... })
but never yields, causing the lint error; replace the generator wrapper with a
simple Effect.succeed returning the parse effect. Concretely, keep the inner
const parse = Effect.fn('V4ConfigParser.parse')(function* (...) { ... }) as-is,
then return Effect.succeed({ parse }) instead of using Effect.gen; ensure the
final exported object is the Effect created by Effect.succeed and that
parseEntrypoints, collectAssets and mapRoute usages remain unchanged.

---

Nitpick comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 117-145: The null-check branch that currently does "yield* new
FunctionConfigError({...})" doesn't terminate control flow so TypeScript can't
narrow "settings" and the code uses several non-null assertions ("settings!");
change that line to "return yield* new FunctionConfigError({...})" so the
function exits on error and TypeScript will treat "settings" as defined, then
remove the five "settings!" assertions in the entrypoint push (references:
settings, FunctionConfigError, entrypoints, matchedFiles, fnPath) so
runtime/defaults are used without non-null bangs.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab342f3 and 0b6bf1e.

📒 Files selected for processing (1)
  • packages/network-config/src/services/v4-config-parser.ts

- Guard parseYaml result against null (loose equality check)
- Use full entrypoint path for route destinations in Vercel parser
- Replace yield-less Effect.gen with Effect.succeed in V4ConfigParser
- Add language tag to fenced code block in ADDING_FRAMEWORKS.md
- Clarify install-command exception for platform-specific frameworks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/network-config/src/services/raw-config-reader.ts (1)

64-95: ⚠️ Potential issue | 🟠 Major

parsed still typed as Record<string, unknown>, making the null guard on Line 83 invisible to the type-checker.

Both JSON.parse and parseYaml can return non-object primitives (e.g. null, a bare string), but all three branches cast the result as Record<string, unknown>. Under strict: true / strictNullChecks, TypeScript then treats parsed as never-null, so the if (parsed == null) guard on Line 83 is dead code from the compiler's perspective — defeating the safety it provides at runtime. The previous review already recommended changing the declared type to unknown.

🐛 Proposed fix
-      let parsed: Record<string, unknown>;
+      let parsed: unknown;
       try {
         if (fileExtension === '.json') {
-          parsed = JSON.parse(fileContents) as Record<string, unknown>;
+          parsed = JSON.parse(fileContents);
         } else {
-          parsed = parseYaml(fileContents) as Record<string, unknown>;
+          parsed = parseYaml(fileContents);
         }
       } catch (error) { ... }
 
-      if (parsed == null) {
+      if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
         return yield* Effect.fail(
           new ConfigFileParseError({ message: `Config file could not be parsed at ${filePath}`, filePath })
         );
       }
 
+      const parsedRecord = parsed as Record<string, unknown>;
-      if (!options?.disableVersionCheck && typeof parsed.version !== 'number') {
+      if (!options?.disableVersionCheck && typeof parsedRecord.version !== 'number') {
         ...
       }
 
-      return parsed;
+      return parsedRecord;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.ts` around lines 64 -
95, Change the declared type of parsed from Record<string, unknown> to unknown
and stop force-casting JSON.parse/parseYaml results; after parsing, narrow
parsed with proper runtime checks (e.g., ensure parsed is non-null and typeof
parsed === 'object' before treating it as a record), then use a type-guard or
local const typed as Record<string, unknown> to check parsed.version (used by
the existing ConfigVersionError branch) and to satisfy the subsequent null and
version checks; keep the existing error handling using ConfigFileParseError and
ConfigVersionError but rely on these runtime narrows instead of the initial cast
so the if (parsed == null) guard is effective to the TypeScript compiler.
🧹 Nitpick comments (5)
packages/network-config/src/services/raw-config-reader.ts (2)

24-27: Redundant type assertions on return values.

filePath is already string and null is already null; the as string | null casts convey no additional information.

♻️ Proposed fix
-          return filePath as string | null;
+          return filePath;
       }
     }
-    return null as string | null;
+    return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.ts` around lines 24 -
27, Remove the redundant type assertions on the return statements: simply return
filePath and return null instead of using "as string | null". Locate the return
statements that currently cast filePath and null (references to the filePath
variable and the return of null in raw-config-reader) and delete the "as string
| null" casts so the values are returned with their natural types.

66-72: Collapse redundant else if / else branches — both invoke parseYaml.

The else if (['.yml', '.yaml'].includes(fileExtension)) and the final else block are identical. A single else is sufficient (JSON path explicit, everything else YAML).

♻️ Proposed refactor
       if (fileExtension === '.json') {
         parsed = JSON.parse(fileContents);
-      } else if (['.yml', '.yaml'].includes(fileExtension)) {
-        parsed = parseYaml(fileContents);
       } else {
         parsed = parseYaml(fileContents);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.ts` around lines 66 -
72, The branch in raw-config-reader.ts that checks fileExtension has redundant
clauses; simplify the logic in the function where parsed is assigned by
replacing the chain "if (fileExtension === '.json') { parsed = JSON.parse(...) }
else if (['.yml', '.yaml'].includes(fileExtension)) { parsed = parseYaml(...) }
else { parsed = parseYaml(...) }" with a clear two-way branch: keep the explicit
JSON check using JSON.parse, and use a single else to call
parseYaml(fileContents). Ensure references to fileExtension, parsed, JSON.parse
and parseYaml remain intact and behavior unchanged for non-JSON inputs.
packages/network-config/src/services/vercel-build-output-parser.ts (1)

125-127: Redundant null guards — entrypoints and routes are always initialized.

Both result.entrypoints (Line 67) and result.routes (Line 68) are initialized as empty arrays at the top of loadFunctions. The guards on Lines 125-127 and 168-170 are unreachable dead code.

♻️ Suggested cleanup
-        if (!result.entrypoints) {
-          result.entrypoints = [];
-        }
-
         result.entrypoints.push({
-        if (!result.routes) {
-          result.routes = [];
-        }
-
         result.routes.push(defaultRoute, ...configRoutes);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/vercel-build-output-parser.ts` around
lines 125 - 127, The null guards for result.entrypoints and result.routes are
redundant because loadFunctions initializes them as empty arrays; remove the if
(!result.entrypoints) { result.entrypoints = []; } and the equivalent guard for
result.routes to clean up unreachable dead code in the loadFunctions function
while leaving the initial assignments intact.
packages/network-config/src/services/v4-config-parser.ts (2)

248-250: Prefer satisfies NormalizedConfig over as NormalizedConfig to retain structural type checking.

The as assertion suppresses TypeScript errors if any field is missing, renamed, or has an incompatible type. Using satisfies (or an explicit return type on the Effect.fn callback) keeps the cast-free structural check.

♻️ Proposed refactor
-      return {
+      return ({
         regions: resolveRegions(config.regions),
         assets: {
           paths: assets.sort(),
           prefixToStrip: (config.assets ?? '') + '/',
           dynamicRoutes: true,
           populateCache: config.populateAssetCache ?? false,
         },
         environmentVariables: config.env ?? {},
         commands: config.build_commands ?? [],
         entrypoints,
         errors: [],
         warnings: [],
         routes: (config.routes ?? []).map(mapRoute),
-      } as NormalizedConfig;
+      }) satisfies NormalizedConfig;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 248 -
250, The object literal currently being forced with "as NormalizedConfig" should
use a type-preserving check: replace the "as NormalizedConfig" assertion with
"satisfies NormalizedConfig" (or alternatively give the enclosing Effect.fn
callback an explicit return type of NormalizedConfig) so missing/incorrect
fields like warnings, routes or the mapped entries from mapRoute/config.routes
are caught by the compiler; update the expression that builds { warnings: [],
routes: (config.routes ?? []).map(mapRoute) } to use the satisfies form or
ensure the function return type is NormalizedConfig.

225-252: Yield-less Effect.gen correctly replaced with Effect.succeed — LGTM.

The prior Biome lint/correctness/useYield error is resolved.

Prefer satisfies NormalizedConfig over as NormalizedConfig to preserve structural type checking.

The as assertion on Line 250 suppresses TypeScript errors if any required field of NormalizedConfig is missing or type-incompatible. satisfies provides the same hint to readers while keeping the compiler check intact.

♻️ Proposed refactor
-      return {
+      return ({
         regions: resolveRegions(config.regions),
         assets: {
           paths: assets.sort(),
           prefixToStrip: (config.assets ?? '') + '/',
           dynamicRoutes: true,
           populateCache: config.populateAssetCache ?? false,
         },
         environmentVariables: config.env ?? {},
         commands: config.build_commands ?? [],
         entrypoints,
         errors: [],
         warnings: [],
         routes: (config.routes ?? []).map(mapRoute),
-      } as NormalizedConfig;
+      }) satisfies NormalizedConfig;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 225 -
252, Replace the unsafe type assertion that suppresses structural checks: in the
Effect.fn('V4ConfigParser.parse') implementation return object, change the
trailing "as NormalizedConfig" to use TypeScript's "satisfies NormalizedConfig"
so the returned object retains full compiler structural checking while keeping
the intended type hint for the parser function (refer to the parse function
inside the V4ConfigParser.parse Effect and the NormalizedConfig type).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/network-config/src/services/raw-config-reader.ts`:
- Line 6: The exported constant ALLOWED_CONFIG_NAMES should be a readonly tuple
of string literals so callers can derive a literal union; update the declaration
of ALLOWED_CONFIG_NAMES (the exported const in raw-config-reader.ts) to use an
"as const" assertion on the array (making it readonly and preserving each
element's literal type) so downstream code can infer the union type
`'gigadrive.yaml' | 'gigadrive.yml' | 'nebula.yaml' | 'nebula.yml' |
'nebula.json'`.

In `@packages/network-config/src/services/vercel-build-output-parser.ts`:
- Line 49: deepMerge is currently concatenating arrays from parseResult and the
object returned by loadFunctions, causing duplicate entries (notably
parseResult.regions) after the merge; update the merge logic so array fields are
deduplicated: either remove regions/entrypoints/routes from the object returned
by loadFunctions and merge them separately with a deduplication step, or run
deduplication immediately after deepMerge (e.g., produce result =
deepMerge(parseResult, functionData, assetOverrides) and then set
result.regions/entrypoints/routes to unique arrays built from their values);
focus changes around the deepMerge call, loadFunctions output, and ensure you
reference parseResult.regions and result.regions when implementing
deduplication.

---

Duplicate comments:
In `@packages/network-config/src/services/raw-config-reader.ts`:
- Around line 64-95: Change the declared type of parsed from Record<string,
unknown> to unknown and stop force-casting JSON.parse/parseYaml results; after
parsing, narrow parsed with proper runtime checks (e.g., ensure parsed is
non-null and typeof parsed === 'object' before treating it as a record), then
use a type-guard or local const typed as Record<string, unknown> to check
parsed.version (used by the existing ConfigVersionError branch) and to satisfy
the subsequent null and version checks; keep the existing error handling using
ConfigFileParseError and ConfigVersionError but rely on these runtime narrows
instead of the initial cast so the if (parsed == null) guard is effective to the
TypeScript compiler.

---

Nitpick comments:
In `@packages/network-config/src/services/raw-config-reader.ts`:
- Around line 24-27: Remove the redundant type assertions on the return
statements: simply return filePath and return null instead of using "as string |
null". Locate the return statements that currently cast filePath and null
(references to the filePath variable and the return of null in
raw-config-reader) and delete the "as string | null" casts so the values are
returned with their natural types.
- Around line 66-72: The branch in raw-config-reader.ts that checks
fileExtension has redundant clauses; simplify the logic in the function where
parsed is assigned by replacing the chain "if (fileExtension === '.json') {
parsed = JSON.parse(...) } else if (['.yml', '.yaml'].includes(fileExtension)) {
parsed = parseYaml(...) } else { parsed = parseYaml(...) }" with a clear two-way
branch: keep the explicit JSON check using JSON.parse, and use a single else to
call parseYaml(fileContents). Ensure references to fileExtension, parsed,
JSON.parse and parseYaml remain intact and behavior unchanged for non-JSON
inputs.

In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 248-250: The object literal currently being forced with "as
NormalizedConfig" should use a type-preserving check: replace the "as
NormalizedConfig" assertion with "satisfies NormalizedConfig" (or alternatively
give the enclosing Effect.fn callback an explicit return type of
NormalizedConfig) so missing/incorrect fields like warnings, routes or the
mapped entries from mapRoute/config.routes are caught by the compiler; update
the expression that builds { warnings: [], routes: (config.routes ??
[]).map(mapRoute) } to use the satisfies form or ensure the function return type
is NormalizedConfig.
- Around line 225-252: Replace the unsafe type assertion that suppresses
structural checks: in the Effect.fn('V4ConfigParser.parse') implementation
return object, change the trailing "as NormalizedConfig" to use TypeScript's
"satisfies NormalizedConfig" so the returned object retains full compiler
structural checking while keeping the intended type hint for the parser function
(refer to the parse function inside the V4ConfigParser.parse Effect and the
NormalizedConfig type).

In `@packages/network-config/src/services/vercel-build-output-parser.ts`:
- Around line 125-127: The null guards for result.entrypoints and result.routes
are redundant because loadFunctions initializes them as empty arrays; remove the
if (!result.entrypoints) { result.entrypoints = []; } and the equivalent guard
for result.routes to clean up unreachable dead code in the loadFunctions
function while leaving the initial assignments intact.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab342f3 and 36841b3.

📒 Files selected for processing (4)
  • packages/network-config/src/detection/ADDING_FRAMEWORKS.md
  • packages/network-config/src/services/raw-config-reader.ts
  • packages/network-config/src/services/v4-config-parser.ts
  • packages/network-config/src/services/vercel-build-output-parser.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/network-config/src/detection/ADDING_FRAMEWORKS.md

Comment thread packages/network-config/src/services/raw-config-reader.ts Outdated
Comment thread packages/network-config/src/services/vercel-build-output-parser.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/network-config/src/services/vercel-build-output-parser.ts (1)

49-49: ⚠️ Potential issue | 🟠 Major

Region merge currently re-introduces duplicates through deepMerge.

At Line 69, result.regions starts from parseResult.regions, then Line 49 merges back into parseResult using deepMerge (array concatenation), so existing regions are duplicated.

🔧 Proposed fix
-      const result: Partial<NormalizedConfig> = {
+      const result: Partial<NormalizedConfig> = {
         entrypoints: [],
         routes: [],
-        regions: [...parseResult.regions],
+        regions: [],
         errors: [],
       };
@@
-      return deepMerge(parseResult, functionData, assetOverrides);
+      const merged = deepMerge(parseResult, functionData, assetOverrides);
+      merged.regions = Array.from(new Set(merged.regions ?? [])) as Region[];
+      return merged;

Use this script to verify the behavior from source:

#!/bin/bash
set -euo pipefail

# Confirm seeded regions and deepMerge call site in parser
rg -n -C2 'regions:\s*\[\.\.\.parseResult\.regions\]|deepMerge\(parseResult' packages/network-config/src/services/vercel-build-output-parser.ts

# Confirm deepMerge concatenates arrays (pushes source into target)
rg -n -C3 'Array\.isArray\(targetValue\)\s*&&\s*Array\.isArray\(sourceValue\)|push\(\.\.\.' packages/commons/src/deep-merge.ts

Also applies to: 66-70, 105-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/vercel-build-output-parser.ts` at line
49, The merge reintroduces duplicate regions because deepMerge concatenates
arrays when merging parseResult back into result; to fix, avoid merging the
regions array—either remove/omit parseResult.regions before calling deepMerge or
perform the deepMerge into an object that already has result.regions so
deepMerge doesn’t append parseResult.regions; update the call site that uses
deepMerge(parseResult, functionData, assetOverrides) to exclude the regions key
(or use a replace-array merge strategy) so result.regions remains the seeded
unique array (referencing deepMerge, parseResult, and result.regions in
vercel-build-output-parser.ts).
packages/network-config/src/services/raw-config-reader.ts (1)

64-95: ⚠️ Potential issue | 🟠 Major

Reject non-object parse results before version checks/return.

At Line 64 and Line 83, parsed is assumed to be a record, but only null is rejected. Arrays/scalars can still flow through (especially via readConfigFile where version checks are disabled), which breaks the declared return shape.

🔧 Proposed fix
-      let parsed: Record<string, unknown>;
+      let parsed: unknown;
       try {
         if (fileExtension === '.json') {
-          parsed = JSON.parse(fileContents) as Record<string, unknown>;
+          parsed = JSON.parse(fileContents);
         } else if (['.yml', '.yaml'].includes(fileExtension)) {
-          parsed = parseYaml(fileContents) as Record<string, unknown>;
+          parsed = parseYaml(fileContents);
         } else {
-          parsed = parseYaml(fileContents) as Record<string, unknown>;
+          parsed = parseYaml(fileContents);
         }
       } catch (error) {
         return yield* Effect.fail(
@@
-      if (parsed == null) {
+      if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
         return yield* Effect.fail(
           new ConfigFileParseError({ message: `Config file could not be parsed at ${filePath}`, filePath })
         );
       }
+      const parsedRecord = parsed as Record<string, unknown>;
 
-      if (!options?.disableVersionCheck && typeof parsed.version !== 'number') {
+      if (!options?.disableVersionCheck && typeof parsedRecord.version !== 'number') {
         return yield* Effect.fail(
           new ConfigVersionError({ message: `Config file is missing version at ${filePath}`, filePath })
         );
       }
 
-      return parsed;
+      return parsedRecord;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/raw-config-reader.ts` around lines 64 -
95, The parsed result from JSON/YAML may be null, an array, or a scalar so
update the validation after parsing in raw-config-reader.ts to reject any
non-object (i.e., not an object, null, or Array) before running version checks
or returning; specifically change the check around the parsed variable to assert
typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) and, if
that fails, yield* Effect.fail with a ConfigFileParseError (similar to the
existing error cases) so that functions like readConfigFile and the subsequent
ConfigVersionError only see actual record objects.
🧹 Nitpick comments (3)
packages/network-config/src/services/v4-config-parser.ts (3)

54-60: Complete JSDoc tags for exported APIs.

getFunctionSettings and the exported parse method are missing required JSDoc tags (@param, @returns, @example) in their current blocks.

📝 Suggested doc updates
 /**
  * Resolves merged function settings for a given path by iterating all function
  * patterns in the config. Later patterns override earlier ones.
+ *
+ * `@param` path - File path to resolve settings for.
+ * `@param` config - Parsed V4 config object.
  *
  * `@returns` The merged settings, or undefined when no pattern matches the path.
+ *
+ * `@example`
+ * const settings = getFunctionSettings('api/hello.ts', config);
  */
 export const getFunctionSettings = (path: string, config: ConfigV4): ConfigV4FunctionSettings | undefined => {
     /**
      * Parses a ConfigV4 into a NormalizedConfig.
      *
      * `@param` config - The raw V4 config object
      * `@param` projectFolder - Absolute path to the project root
+     * `@returns` A normalized config object for downstream deployment/runtime consumers.
+     *
+     * `@example`
+     * const normalized = yield* parser.parse(config, '/workspace/project');
      */
     parse: Effect.fn('V4ConfigParser.parse')(function* (config: ConfigV4, projectFolder: string) {

As per coding guidelines: Document exported functions with JSDoc comments including @param, @returns, and @example tags.

Also applies to: 226-231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 54 -
60, Update the JSDoc for the exported functions to include complete tags: add
`@param` entries describing the parameters (their names and types) and expected
values for getFunctionSettings(path: string, config: ConfigV4) and the exported
parse function, add `@returns` describing the return type and when undefined is
returned, and add a short `@example` showing typical usage for each function
(calling getFunctionSettings with a sample path/config and calling parse with a
sample input). Make sure the param names match the function signatures (path,
config) and reference the concrete return type ConfigV4FunctionSettings (or
undefined) so tools can pick up the types.

47-49: Avoid bare catch {} in matchesPattern.

Line 47 uses bare catch {} without re-throwing. Switch to catch (_error) (or handle error) to comply with repository error-handling rules.

As per coding guidelines: Use bare catch { } without a variable only when re-throwing with a custom message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 47 -
49, In matchesPattern, replace the bare catch block with a catch that declares
the error (e.g. catch (_error) or catch (error)) so the exception variable is
captured per repo rules; update the catch in the matchesPattern function to use
a named parameter (and optionally ignore or log it) instead of bare `catch {}`
to satisfy error-handling guidelines.

114-136: Precompute function keys once in parseEntrypoints.

Line 135 recomputes/scans function keys for every matched file. A precomputed Set keeps lookup O(1) and simplifies the inner loop.

♻️ Optional micro-optimization
 const parseEntrypoints = Effect.fn('parseEntrypoints')(function* (config: ConfigV4, projectFolder: string) {
   const entrypoints: NormalizedConfigEntrypoint[] = [];
   if (config.functions == null) return entrypoints;
+  const functionKeys = new Set(Object.keys(config.functions));
 
   for (const [fnPath, func] of Object.entries(config.functions)) {
@@
     for (const file of matchedFiles) {
       if (entrypoints.some((ep) => ep.path === file)) continue;
-      if (Object.keys(config.functions).some((f) => f === file && f !== fnPath)) continue;
+      if (functionKeys.has(file) && file !== fnPath) continue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/services/v4-config-parser.ts` around lines 114 -
136, The inner loop in parseEntrypoints currently recalculates
Object.keys(config.functions) for every matched file (seen in the check
Object.keys(config.functions).some((f) => f === file && f !== fnPath)), causing
repeated scans; precompute a Set of function keys (e.g., functionPathsSet or
functionKeys) once before iterating functions in parseEntrypoints and replace
that check with a constant-time lookup (functionKeys.has(file) && file !==
fnPath) to avoid repeated Object.keys scans and simplify the inner loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 86-89: The resolveRegions function currently coerces string[] to
Region[] with an unsafe as Region[] cast; remove that cast and instead validate
input strings against AVAILABLE_REGIONS and return a properly narrowed Region[]
(or the full AVAILABLE_REGIONS default). Implement a type guard like isRegion(r:
string): r is Region that checks AVAILABLE_REGIONS.includes(r as Region), then
use configRegions?.filter(isRegion) to produce a Region[]; if no valid regions
remain, return AVAILABLE_REGIONS. Update resolveRegions to use this filtering
and drop the unsafe assertion.

In `@packages/network-config/src/services/vercel-build-output-parser.ts`:
- Around line 42-44: The version gate in vercel-build-output-parser.ts is too
lax: replace the current check that uses "config.version < 3" with a hardened
guard that also rejects non-numeric or missing versions; specifically update the
conditional around the existing config check (the block that returns
parseResult) to verify typeof config.version === 'number' (or
!isNaN(Number(config.version))) and only allow numeric versions >= 3 to proceed,
so functions that reference config and the parseResult early-return path behave
safely.

---

Duplicate comments:
In `@packages/network-config/src/services/raw-config-reader.ts`:
- Around line 64-95: The parsed result from JSON/YAML may be null, an array, or
a scalar so update the validation after parsing in raw-config-reader.ts to
reject any non-object (i.e., not an object, null, or Array) before running
version checks or returning; specifically change the check around the parsed
variable to assert typeof parsed === 'object' && parsed !== null &&
!Array.isArray(parsed) and, if that fails, yield* Effect.fail with a
ConfigFileParseError (similar to the existing error cases) so that functions
like readConfigFile and the subsequent ConfigVersionError only see actual record
objects.

In `@packages/network-config/src/services/vercel-build-output-parser.ts`:
- Line 49: The merge reintroduces duplicate regions because deepMerge
concatenates arrays when merging parseResult back into result; to fix, avoid
merging the regions array—either remove/omit parseResult.regions before calling
deepMerge or perform the deepMerge into an object that already has
result.regions so deepMerge doesn’t append parseResult.regions; update the call
site that uses deepMerge(parseResult, functionData, assetOverrides) to exclude
the regions key (or use a replace-array merge strategy) so result.regions
remains the seeded unique array (referencing deepMerge, parseResult, and
result.regions in vercel-build-output-parser.ts).

---

Nitpick comments:
In `@packages/network-config/src/services/v4-config-parser.ts`:
- Around line 54-60: Update the JSDoc for the exported functions to include
complete tags: add `@param` entries describing the parameters (their names and
types) and expected values for getFunctionSettings(path: string, config:
ConfigV4) and the exported parse function, add `@returns` describing the return
type and when undefined is returned, and add a short `@example` showing typical
usage for each function (calling getFunctionSettings with a sample path/config
and calling parse with a sample input). Make sure the param names match the
function signatures (path, config) and reference the concrete return type
ConfigV4FunctionSettings (or undefined) so tools can pick up the types.
- Around line 47-49: In matchesPattern, replace the bare catch block with a
catch that declares the error (e.g. catch (_error) or catch (error)) so the
exception variable is captured per repo rules; update the catch in the
matchesPattern function to use a named parameter (and optionally ignore or log
it) instead of bare `catch {}` to satisfy error-handling guidelines.
- Around line 114-136: The inner loop in parseEntrypoints currently recalculates
Object.keys(config.functions) for every matched file (seen in the check
Object.keys(config.functions).some((f) => f === file && f !== fnPath)), causing
repeated scans; precompute a Set of function keys (e.g., functionPathsSet or
functionKeys) once before iterating functions in parseEntrypoints and replace
that check with a constant-time lookup (functionKeys.has(file) && file !==
fnPath) to avoid repeated Object.keys scans and simplify the inner loop.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab342f3 and 36841b3.

📒 Files selected for processing (4)
  • packages/network-config/src/detection/ADDING_FRAMEWORKS.md
  • packages/network-config/src/services/raw-config-reader.ts
  • packages/network-config/src/services/v4-config-parser.ts
  • packages/network-config/src/services/vercel-build-output-parser.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/network-config/src/detection/ADDING_FRAMEWORKS.md

Comment thread packages/network-config/src/services/v4-config-parser.ts
Comment thread packages/network-config/src/services/vercel-build-output-parser.ts Outdated
Zeryther and others added 4 commits February 25, 2026 07:45
…t to ALLOWED_CONFIG_NAMES

The deepMerge call in VercelBuildOutputParser was duplicating regions because
loadFunctions pre-seeded its regions array with parseResult.regions, and
deepMerge concatenates arrays. Initialize with an empty array instead and
deduplicate after the merge. Also add `as const` to ALLOWED_CONFIG_NAMES
so downstream code can derive a string literal union type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract parseConfigRaw (parse + validate without post-processing) so Case A
can parse the user config raw, merge with framework defaults, then run
postProcessConfig on the merged result. This prevents stale pre-merge errors
(e.g. "no functions or assets") from persisting after framework defaults
fill in the gaps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…veRegions

Add isRegion type guard to validate config strings against AVAILABLE_REGIONS,
replacing the unsafe `as Region[]` assertion. Invalid region strings are now
filtered out, falling back to AVAILABLE_REGIONS when no valid regions remain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eric versions

Add typeof check so missing or non-numeric version fields (e.g. undefined,
string "3") are rejected alongside versions below 3. Add tests covering
both cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/network-config/src/detection/ADDING_FRAMEWORKS.md (1)

36-36: Add at least one example showing pm in use.

Every getDefaultConfig example (lines 36, 209, 234, 264, 298) writes () => ({...}) and silently ignores the pm argument. The reference table on Line 101 documents the signature as (pm: PackageManager) => FrameworkDefaultConfig and says the argument "lets you customise install commands per package manager if needed" — but there is no worked example of how to do that. A contributor adding a framework that needs, for example, yarn build vs pnpm build has nothing to copy.

Consider adding a short inline snippet, e.g., in the Step-by-step example or as a dedicated sub-section:

-  getDefaultConfig: () => ({
+  getDefaultConfig: (pm) => ({
     runtime: 'node-22',
     memory: 128,
     maxDuration: 30,
     streaming: true,
-    commands: ['my-framework build'],
+    commands: [`${pm} run build`], // use pm when the command differs per package manager
     entrypoint: 'dist/index.js',

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/network-config/src/detection/ADDING_FRAMEWORKS.md` at line 36, The
examples for getDefaultConfig currently ignore the pm argument; update at least
one getDefaultConfig example (and ideally the others) to accept pm:
PackageManager and show using pm to choose install/build commands (e.g.,
conditionally returning install/build strings based on pm === 'yarn' vs 'pnpm'
or using a helper like pm.run). Modify the function signature in the example to
getDefaultConfig: (pm) => ({...}) and include a short snippet in the
Step-by-step or a new subsection demonstrating how to map pm to
package-manager-specific commands so contributors can copy the pattern;
reference getDefaultConfig, PackageManager, FrameworkDefaultConfig and pm in the
added example.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/services/project-config.ts`:
- Around line 70-78: The current detectFramework pipeline unconditionally
converts ManifestReadError and ManifestParseError to null which masks real
detection failures and later surfaces as ConfigNotFoundError; update the
Effect.catchTag handlers for ManifestReadError and ManifestParseError inside the
detectFramework pipeline to only swallow the error when it clearly indicates a
missing config/manifest (e.g., an underlying ENOENT/file-not-found flag or a
specific "manifest not present" flag on the error), otherwise rethrow the
original error (use Effect.fail(e) or rethrowing within the handler) so genuine
read/parse failures propagate instead of becoming null and turning into
ConfigNotFoundError. Ensure you reference detectFramework, ManifestReadError,
ManifestParseError, and ConfigNotFoundError when making the change.

---

Nitpick comments:
In `@packages/network-config/src/detection/ADDING_FRAMEWORKS.md`:
- Line 36: The examples for getDefaultConfig currently ignore the pm argument;
update at least one getDefaultConfig example (and ideally the others) to accept
pm: PackageManager and show using pm to choose install/build commands (e.g.,
conditionally returning install/build strings based on pm === 'yarn' vs 'pnpm'
or using a helper like pm.run). Modify the function signature in the example to
getDefaultConfig: (pm) => ({...}) and include a short snippet in the
Step-by-step or a new subsection demonstrating how to map pm to
package-manager-specific commands so contributors can copy the pattern;
reference getDefaultConfig, PackageManager, FrameworkDefaultConfig and pm in the
added example.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab342f3 and bd80baf.

📒 Files selected for processing (8)
  • packages/cli/src/services/project-config.test.ts
  • packages/cli/src/services/project-config.ts
  • packages/network-config/src/detection/ADDING_FRAMEWORKS.md
  • packages/network-config/src/parse-config.ts
  • packages/network-config/src/services/raw-config-reader.ts
  • packages/network-config/src/services/v4-config-parser.ts
  • packages/network-config/src/services/vercel-build-output-parser.test.ts
  • packages/network-config/src/services/vercel-build-output-parser.ts

Comment thread packages/cli/src/services/project-config.ts
…g them

ManifestReadError and ManifestParseError always indicate genuine problems
(file exists but is unreadable or malformed JSON). Previously these were
swallowed and converted to null, masking real failures and producing a
misleading ConfigNotFoundError. Now they propagate as ConfigParseError
with an actionable message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Zeryther
Zeryther merged commit b5ee20c into main Feb 25, 2026
12 checks passed
@Zeryther
Zeryther deleted the feature/framework-auto-detection branch February 25, 2026 07:12
@github-actions github-actions Bot mentioned this pull request Feb 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant