Skip to content

feat(plugin): support typed generated module lifecycles - #64

Draft
yuzheng14 wants to merge 5 commits into
afx-team:mainfrom
yuzheng14:feat/generated-module-lifecycle
Draft

feat(plugin): support typed generated module lifecycles#64
yuzheng14 wants to merge 5 commits into
afx-team:mainfrom
yuzheng14:feat/generated-module-lifecycle

Conversation

@yuzheng14

@yuzheng14 yuzheng14 commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • add typed declaration companions for plugin-generated modules and exact aliases
  • materialize framework-owned runtime and declaration files with ownership checks, no-op writes, atomic replacement, and final-byte digests
  • make dev plan updates generation-aware and transactional, including fresh previous-plan recompilation after candidate compile or callback failures
  • support exact custom-scheme aliases and server-only client-graph checks in Webpack
  • anchor Utoopack stats checks to the real workspace root and fail closed on incomplete module inventories
  • ignore generated framework artifacts in scaffolds/examples and document the behavior in English and Chinese

Motivation

Plugins can generate server-only modules whose runtime source, editor declarations, bundle output, and running API must remain on the same schema generation. Previously, a schema edit could update generated files without providing a safe, end-to-end lifecycle for declaration discovery, server compiler replacement, callback publication, API restart, and rollback.

This change gives generated modules an explicit typed contract and stages framework-owned files until the bundler has quarantined the previous server watcher. A candidate is accepted only after fresh build facts and server readiness are published. Candidate compilation, artifact publication, or API-start failures restore the previous files, perform a fresh previous-plan compile, and republish the previous generation before rejecting the update.

Compatibility notes

  • migrated directly onto the current main / v0.3.1 CoreGraph architecture
  • Webpack supports generated server-runtime refresh without restarting the client dev server
  • Utoopack keeps server-runtime topology changes fail-closed; this PR adds generation-aware artifact updates, workspace-root-correct stats inspection, and client-boundary verification
  • declaration metadata remains intentionally narrow: exact named values and explicitly audited non-generic named types

Validation

  • npm run lint
  • npm run check-types — 31/31 Turbo tasks
  • npm run build — 24/24 Turbo tasks
  • npm test — 17/17 Turbo tasks
    • @evjs/ev: 622/622
    • @evjs/bundler-webpack: 85/85
    • @evjs/bundler-utoopack: 81/81
    • @evjs/shared: 254/254
    • @evjs/create-app: 13/13
  • Webpack generated-runtime refresh regression repeated 10/10
  • npm run test:e2e — 53/53
  • English and Chinese documentation production builds

All six commits are GPG-signed.

Summary by CodeRabbit

  • New Features
    • Added support for exact TypeScript types for generated aliases, including automatic declaration companions and type discovery.
    • Improved development updates with transactional commit, rollback, and server-runtime refresh handling.
    • Added PostCSS configuration support and custom generated-alias resolution in Webpack.
    • Added safeguards to prevent server-only generated modules from entering client bundles.
  • Documentation
    • Documented generated type files, alias declaration rules, project conventions, and development limitations.
  • Bug Fixes
    • Improved generated output stability, ownership protection, and workspace-root handling across build adapters.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 90a10877-2451-49ff-8f07-936682918982

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds exact TypeScript declarations for generated aliases, transactional framework materialization and development updates, server/client generated-module isolation checks for Webpack and Utoopack, PostCSS and custom-scheme alias support, and generated-file documentation and scaffolding updates.

Changes

Generated aliases and materialization

Layer / File(s) Summary
Exact declaration contracts and publishing
packages/shared/..., packages/ev/src/_internal/build/..., packages/ev/src/plugin/index.ts
Generated modules can provide declarationSource and audited alias export metadata. evjs validates, renders, and publishes declaration companions plus src/evjs-env.d.ts through prepared, ownership-aware materialization.
Transactional development lifecycle
packages/ev/src/_internal/build/bundler.ts, packages/ev/src/_internal/build/commands.ts, packages/ev/src/_internal/build/dev-api-process.ts
Development updates track plan generations, defer framework writes, snapshot generated outputs, and coordinate commit, rollback, API replacement, and build-facts publication.
Webpack adapter updates
packages/bundler-webpack/src/adapter/*, packages/bundler-webpack/src/manifest-generator.ts
Webpack adds PostCSS and custom-scheme alias handling, server-generated-module isolation checks, transactional server compiler replacement, and publication barriers.
Utoopack adapter updates
packages/bundler-utoopack/src/adapter/*, packages/bundler-utoopack/src/manifest-generator.ts
Utoopack propagates the resolved workspace root and plan generation, validates client stats for server-generated modules, and applies transactional plan updates with recovery.
Validation and generated-file support
packages/ev/tests/*, packages/bundler-*/tests/*, docs/**, examples/*/.gitignore, packages/create-app/*
Tests cover declaration discovery, materialization safety, TypeScript server refreshes, bundler isolation, and dev recovery. Documentation and scaffolding mark generated declaration outputs as managed artifacts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • afx-team/evjs#41: Extends generated-contribution IR and alias plumbing used by this declaration-companion work.
  • afx-team/evjs#42: Shares the build/materialization lifecycle changes used by the generated-contribution updates.

Suggested reviewers: xusd320

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the core change: typed generated module lifecycles.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (10)
packages/ev/src/_internal/build/generated-contributions.ts (1)

1348-1354: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Empty src/.ev directory is left behind after the last declaration is removed.

syncGeneratedTypesCompanionRoot removes src/.ev/types but not the framework-created src/.ev parent, so projects that stop contributing declarations keep an empty generated directory inside their source root.

♻️ Optional cleanup
     await removeStaleGeneratedFiles(companionRoot, companionRoot, new Set());
     await fs.rmdir(companionRoot);
+    await fs.rmdir(path.dirname(companionRoot)).catch((error) => {
+      const code = (error as NodeJS.ErrnoException).code;
+      if (code !== "ENOENT" && code !== "ENOTEMPTY") throw error;
+    });
     return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ev/src/_internal/build/generated-contributions.ts` around lines 1348
- 1354, Update syncGeneratedTypesCompanionRoot’s empty-modules cleanup to remove
the framework-created companionRoot parent after deleting its contents, while
preserving the ownership guard and symlink assertion. Ensure the last
declaration removal cleans up both src/.ev/types and the now-empty src/.ev
directory without affecting non-owned roots.
packages/ev/src/_internal/build/owned-file-output.ts (1)

43-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Both ternary branches are identical.

Buffer.from(contents) handles string and Uint8Array alike here, so the conditional adds no behavior.

♻️ Simplification
-  const nextContents =
-    typeof contents === "string"
-      ? Buffer.from(contents)
-      : Buffer.from(contents);
+  const nextContents = Buffer.from(contents as string | Uint8Array);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ev/src/_internal/build/owned-file-output.ts` around lines 43 - 46,
Update the contents conversion in the surrounding owned-file output logic to
remove the redundant typeof ternary and call Buffer.from(contents) directly.
Preserve the resulting Buffer behavior for both string and Uint8Array inputs.
packages/bundler-webpack/src/adapter/create-config.ts (1)

76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider registering the discovered PostCSS config with options.addWatchFile.

createWebpackConfigs already receives addWatchFile, but the resolved PostCSS config path is never watched, so editing postcss.config.* (or the postcss key in package.json) in dev neither invalidates the framework plan nor surfaces a restart hint. Since discovery result also flips the loader chain on/off, adding/removing the file goes unnoticed until a manual restart.

♻️ Sketch
-  const postcssLoader = resolveProjectPostcssLoader(cwd);
+  const postcssConfigPath = findProjectPostcssConfig(cwd);
+  if (postcssConfigPath) options.addWatchFile?.(postcssConfigPath);
+  const postcssLoader = resolveProjectPostcssLoader(cwd, postcssConfigPath);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-webpack/src/adapter/create-config.ts` at line 76, Update
createWebpackConfigs to register the path returned by
resolveProjectPostcssLoader with options.addWatchFile when a PostCSS config is
discovered, ensuring edits or addition/removal of postcss.config.* and
package.json PostCSS configuration trigger invalidation and restart handling
while preserving the existing loader-chain behavior.
packages/ev/src/_internal/build/commands.ts (1)

269-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the generated declaration paths from generated-contributions.js instead of re-literalizing them here.

src/.ev/types and src/evjs-env.d.ts are duplicated from the materialization module that owns these outputs (you already import its ownership helpers). If the layout changes there, the snapshot/rollback path silently diverges and rollback stops protecting the real files.

♻️ Sketch
 import {
   GENERATED_IR_DIR,
+  getGeneratedTypesCompanionPath,
+  getGeneratedTypesDiscoveryPath,
   type GeneratedOutputOwnership,
   getGeneratedTypesCompanionOwnership,
   getGeneratedTypesDiscoveryOwnership,
 } from "./generated-contributions.js";
-  const generatedTypeCompanionsPath = path.resolve(cwd, "src/.ev/types");
+  const generatedTypeCompanionsPath = getGeneratedTypesCompanionPath(cwd);

As per coding guidelines, "Use shared helpers from @evjs/shared for route, path, build-ID, and server-function-ID conventions; avoid duplicating validation rules across config, build analysis, and runtimes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ev/src/_internal/build/commands.ts` around lines 269 - 274, Update
the path setup around generatedTypeCompanionsPath and generatedTypeDiscoveryFile
to derive both generated declaration paths from the imported
generated-contributions materialization ownership helpers, rather than
hardcoding "src/.ev/types" and "src/evjs-env.d.ts". Keep
generatedTypeCompanionsSnapshot rooted under snapshotRoot, and reuse the shared
helper outputs so snapshot and rollback track the materialized files.

Source: Coding guidelines

packages/bundler-webpack/src/adapter/index.ts (1)

1572-1588: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full module inventory on every dev compile is a notable hot-path cost.

modules/nestedModules/orphanModules/runtimeModules with unbounded modulesSpace makes toJson serialize the entire module graph on every dev rebuild, not just production builds. Consider requesting the module sections only when plan.generated actually contains server-scoped modules (the sole consumer, assertServerGeneratedModulesStayOutOfClient, returns early otherwise).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-webpack/src/adapter/index.ts` around lines 1572 - 1588,
Avoid enabling full module inventory serialization during ordinary dev compiles.
In the stats/toJson options near the module-related fields, request modules,
nestedModules, orphanModules, runtimeModules, and their unbounded space limits
only when plan.generated contains server-scoped modules. Preserve the existing
settings for other report fields and ensure
assertServerGeneratedModulesStayOutOfClient still receives the required module
data.
packages/bundler-utoopack/src/manifest-generator.ts (1)

285-286: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

projectRoot is only used for a guard that cannot fail.

createEquivalentRoots always returns at least the resolved root, and the binding is unused afterwards; the candidates mapping already iterates all roots.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-utoopack/src/manifest-generator.ts` around lines 285 - 286,
Remove the unused projectRoot binding and its guard after createEquivalentRoots
in the manifest generation flow. Preserve the existing candidates mapping over
all returned roots, relying on createEquivalentRoots to provide at least the
resolved root.
packages/bundler-utoopack/tests/adapter.test.ts (1)

39-40: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No coverage for rootPath propagation into the dev worker.

The mock destructures only { config, server }, so the newly required rootPath option on startUtoopackDevWorker is never asserted here; a regression back to an undefined root would pass. Capture and assert it in one dev test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-utoopack/tests/adapter.test.ts` around lines 39 - 40, Update
the startUtoopackDevWorker mock in adapter.test.ts to capture the rootPath
argument alongside config and server, then add an assertion in one dev-worker
test that the expected rootPath is propagated. Preserve the existing config and
server assertions.
packages/bundler-utoopack/tests/manifest-generator.test.ts (1)

182-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer it.each over the in-test loop.

A failure inside the loop does not identify which module-path representation broke, and later variants are skipped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-utoopack/tests/manifest-generator.test.ts` around lines 182
- 207, Replace the loop in the test “resolves client module paths against
Utoopack's workspace stats root” with an it.each table containing each
module-path representation. Keep the shared setup and rejection assertion per
case, so failures identify the specific representation and all variants execute
independently.
packages/bundler-utoopack/src/adapter/index.ts (2)

451-471: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

commitStarted is always true in the catch block.

It is assigned before commitFrameworkState() and nothing between the assignment and the try boundary can throw, so if (!commitStarted) throw error; is unreachable. Either drop the flag or set it after the commit resolves if the intent is "rollback only when a commit actually happened".

♻️ Simplify by removing the dead flag
-    let commitStarted = false;
     try {
-      commitStarted = true;
       await options.commitFrameworkState();
@@
     } catch (error) {
-      if (!commitStarted) throw error;
       try {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-utoopack/src/adapter/index.ts` around lines 451 - 471,
Remove the dead commitStarted flag and its unreachable conditional from the
try/catch around commitFrameworkState, generateDevArtifacts, and the subsequent
plan updates. Preserve the existing error handling behavior by allowing the
catch block to handle errors directly without the always-true state check.

65-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider an options object for generateDevArtifacts.

Seven positional parameters, four of them adjacent primitives/plan values (cwd, rootPath, plan, planGeneration), make call sites easy to mis-order silently at the three call sites in this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-utoopack/src/adapter/index.ts` around lines 65 - 86, The
generateDevArtifacts function has too many positional parameters, making calls
easy to mis-order. Replace its parameter list with a single options object
containing the existing values, update all three call sites in this file to use
named properties, and preserve the current defaults and behavior for options and
facts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/bundler-utoopack/src/manifest-generator.ts`:
- Around line 131-132: The module decoration regex in UTOOPACK_MODULE_DECORATION
only recognizes client and server tags; broaden it to match any bracketed layer
suffix, then ensure the manifest generator treats every decoration other than
UTOOPACK_CLIENT_MODULE_SUFFIX as unsupported and fails closed. In
packages/bundler-utoopack/tests/manifest-generator.test.ts lines 155-180, add an
unrecognized-layer representation case such as [ssr] and assert generation fails
closed.

In `@packages/bundler-webpack/src/adapter/index.ts`:
- Around line 448-467: The server refresh in updatePlanWithFreshServerCompiler
must synchronize plan changes with devWorkQueue before artifact publication
resumes. Keep the plan update and publication re-allowance in one shared async
path, wait for pending dev work before allowing publication, and preserve
handleClientStats’s ability to read a consistent this.plan while resolving
outputs and validating generated modules.

In `@packages/ev/tests/build-tools-generated-declarations.test.ts`:
- Line 44: Add an explicit 120,000 ms timeout to the test named “materializes
exact named exports for strict rootDir projects without paths” and the assertion
block at lines 202–204 that also spawns tsc, using the suite’s existing Vitest
timeout configuration style.

In `@packages/shared/tests/manifest.test.ts`:
- Around line 58-80: Extend the GeneratedModulePlan test with a negative type
fixture annotated with `@ts-expect-error` that constructs a plan without
sourceHash. Keep the existing runtimeOnly and typedModule fixtures unchanged,
ensuring the omission fails type checking and verifies sourceHash remains
required.

---

Nitpick comments:
In `@packages/bundler-utoopack/src/adapter/index.ts`:
- Around line 451-471: Remove the dead commitStarted flag and its unreachable
conditional from the try/catch around commitFrameworkState,
generateDevArtifacts, and the subsequent plan updates. Preserve the existing
error handling behavior by allowing the catch block to handle errors directly
without the always-true state check.
- Around line 65-86: The generateDevArtifacts function has too many positional
parameters, making calls easy to mis-order. Replace its parameter list with a
single options object containing the existing values, update all three call
sites in this file to use named properties, and preserve the current defaults
and behavior for options and facts.

In `@packages/bundler-utoopack/src/manifest-generator.ts`:
- Around line 285-286: Remove the unused projectRoot binding and its guard after
createEquivalentRoots in the manifest generation flow. Preserve the existing
candidates mapping over all returned roots, relying on createEquivalentRoots to
provide at least the resolved root.

In `@packages/bundler-utoopack/tests/adapter.test.ts`:
- Around line 39-40: Update the startUtoopackDevWorker mock in adapter.test.ts
to capture the rootPath argument alongside config and server, then add an
assertion in one dev-worker test that the expected rootPath is propagated.
Preserve the existing config and server assertions.

In `@packages/bundler-utoopack/tests/manifest-generator.test.ts`:
- Around line 182-207: Replace the loop in the test “resolves client module
paths against Utoopack's workspace stats root” with an it.each table containing
each module-path representation. Keep the shared setup and rejection assertion
per case, so failures identify the specific representation and all variants
execute independently.

In `@packages/bundler-webpack/src/adapter/create-config.ts`:
- Line 76: Update createWebpackConfigs to register the path returned by
resolveProjectPostcssLoader with options.addWatchFile when a PostCSS config is
discovered, ensuring edits or addition/removal of postcss.config.* and
package.json PostCSS configuration trigger invalidation and restart handling
while preserving the existing loader-chain behavior.

In `@packages/bundler-webpack/src/adapter/index.ts`:
- Around line 1572-1588: Avoid enabling full module inventory serialization
during ordinary dev compiles. In the stats/toJson options near the
module-related fields, request modules, nestedModules, orphanModules,
runtimeModules, and their unbounded space limits only when plan.generated
contains server-scoped modules. Preserve the existing settings for other report
fields and ensure assertServerGeneratedModulesStayOutOfClient still receives the
required module data.

In `@packages/ev/src/_internal/build/commands.ts`:
- Around line 269-274: Update the path setup around generatedTypeCompanionsPath
and generatedTypeDiscoveryFile to derive both generated declaration paths from
the imported generated-contributions materialization ownership helpers, rather
than hardcoding "src/.ev/types" and "src/evjs-env.d.ts". Keep
generatedTypeCompanionsSnapshot rooted under snapshotRoot, and reuse the shared
helper outputs so snapshot and rollback track the materialized files.

In `@packages/ev/src/_internal/build/generated-contributions.ts`:
- Around line 1348-1354: Update syncGeneratedTypesCompanionRoot’s empty-modules
cleanup to remove the framework-created companionRoot parent after deleting its
contents, while preserving the ownership guard and symlink assertion. Ensure the
last declaration removal cleans up both src/.ev/types and the now-empty src/.ev
directory without affecting non-owned roots.

In `@packages/ev/src/_internal/build/owned-file-output.ts`:
- Around line 43-46: Update the contents conversion in the surrounding
owned-file output logic to remove the redundant typeof ternary and call
Buffer.from(contents) directly. Preserve the resulting Buffer behavior for both
string and Uint8Array inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fde9abf8-983d-4d8b-b9ac-22cd417cc19d

📥 Commits

Reviewing files that changed from the base of the PR and between f2f03fe and ec155c9.

📒 Files selected for processing (59)
  • docs/docs/file-conventions.md
  • docs/docs/generated-contributions.md
  • docs/docs/plugins.md
  • docs/docs/project-structure.md
  • docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/file-conventions.md
  • docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/generated-contributions.md
  • docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/plugins.md
  • docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/project-structure.md
  • examples/api-routes/.gitignore
  • examples/basic/.gitignore
  • examples/complex-routing/.gitignore
  • examples/custom-ws-transport/.gitignore
  • examples/deployment-adapters/.gitignore
  • examples/mpa/.gitignore
  • examples/plugin-authoring/.gitignore
  • examples/qiankun-master/.gitignore
  • examples/qiankun-slave/.gitignore
  • examples/render-modes/.gitignore
  • examples/ssg/.gitignore
  • examples/with-sqlite/.gitignore
  • examples/with-tailwind/.gitignore
  • examples/with-trpc/.gitignore
  • packages/bundler-utoopack/src/adapter/dev-worker-client.ts
  • packages/bundler-utoopack/src/adapter/dev-worker.ts
  • packages/bundler-utoopack/src/adapter/index.ts
  • packages/bundler-utoopack/src/adapter/runtime.ts
  • packages/bundler-utoopack/src/manifest-generator.ts
  • packages/bundler-utoopack/tests/adapter.test.ts
  • packages/bundler-utoopack/tests/dev-worker-client.test.ts
  • packages/bundler-utoopack/tests/manifest-generator.test.ts
  • packages/bundler-utoopack/tests/runtime.test.ts
  • packages/bundler-webpack/src/adapter/create-config.ts
  • packages/bundler-webpack/src/adapter/index.ts
  • packages/bundler-webpack/src/manifest-generator.ts
  • packages/bundler-webpack/tests/adapter.test.ts
  • packages/bundler-webpack/tests/create-config.test.ts
  • packages/create-app/README.md
  • packages/create-app/scripts/deref-templates.js
  • packages/create-app/src/index.ts
  • packages/create-app/tests/scaffold.test.ts
  • packages/ev/src/_internal/build/analyze-and-materialize.ts
  • packages/ev/src/_internal/build/bundler.ts
  • packages/ev/src/_internal/build/commands.ts
  • packages/ev/src/_internal/build/dev-api-process.ts
  • packages/ev/src/_internal/build/generated-contributions.ts
  • packages/ev/src/_internal/build/index.ts
  • packages/ev/src/_internal/build/owned-file-output.ts
  • packages/ev/src/plugin/index.ts
  • packages/ev/tests/build-tools-generated-declarations.test.ts
  • packages/ev/tests/build-tools-generated-materialization.test.ts
  • packages/ev/tests/build-tools-generated-tsserver.test.ts
  • packages/ev/tests/bundler-capabilities.test.ts
  • packages/ev/tests/commands-api-restart.test.ts
  • packages/ev/tests/commands.test.ts
  • packages/ev/tests/dev-api-process.test.ts
  • packages/ev/tests/package-surface.test.ts
  • packages/ev/tests/resolve-alias-contribution-types.ts
  • packages/shared/src/manifest/index.ts
  • packages/shared/tests/manifest.test.ts

Comment on lines +131 to +132
const UTOOPACK_CLIENT_MODULE_SUFFIX = " [client] (ecmascript)";
const UTOOPACK_MODULE_DECORATION = /\s+\[(?:client|server)\]\s+\(.+\)$/u;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Decoration allow-list is limited to [client]/[server], so other layer tags bypass the isolation check. Any module name carrying a different bracketed layer decoration is neither rejected as unsupported nor normalized, so a leaked server-scoped generated module reported under that layer goes undetected.

  • packages/bundler-utoopack/src/manifest-generator.ts#L131-L132: broaden UTOOPACK_MODULE_DECORATION to any \s+\[[^\]]+\]\s+\(.+\)$ suffix and treat every decoration other than the known client suffix as unsupported (fail closed).
  • packages/bundler-utoopack/tests/manifest-generator.test.ts#L155-L180: add a representation case with an unrecognized layer tag (e.g. [project]/.ev/plugins/schema-lifecycle/database.ts [ssr] (ecmascript)) asserting the generator fails closed.
📍 Affects 2 files
  • packages/bundler-utoopack/src/manifest-generator.ts#L131-L132 (this comment)
  • packages/bundler-utoopack/tests/manifest-generator.test.ts#L155-L180
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-utoopack/src/manifest-generator.ts` around lines 131 - 132,
The module decoration regex in UTOOPACK_MODULE_DECORATION only recognizes client
and server tags; broaden it to match any bracketed layer suffix, then ensure the
manifest generator treats every decoration other than
UTOOPACK_CLIENT_MODULE_SUFFIX as unsupported and fails closed. In
packages/bundler-utoopack/tests/manifest-generator.test.ts lines 155-180, add an
unrecognized-layer representation case such as [ssr] and assert generation fails
closed.

Comment on lines +448 to +467
private async updatePlanWithFreshServerCompiler(
update: BuildPlanUpdate,
options: BundlerDevUpdateOptions<WebpackConfig>,
): Promise<void> {
const previousPlan = this.plan;
const previousConfig = this.config;
const previousPlanGeneration = this.planGeneration;
const previousServerPublicAssetOwnership = new Map(
this.serverPublicAssetOwnership,
);
this.blockArtifactPublication();

let frameworkStateCommitted = false;
let cleanupError: unknown;
try {
const previousState = this.invalidateCurrentServerWatch();
if (previousState) {
await this.quarantineServerWatch(previousState);
}
await this.drainArtifactPublication();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect all dev-work/plan-update queue usages in the webpack adapter.
rg -nP -C4 '\b(enqueueDevWork|enqueuePlanUpdate|devWorkQueue|planUpdateQueue)\b' packages/bundler-webpack/src/adapter/index.ts

Repository: afx-team/evjs

Length of output: 2410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== relevant source ==="
sed -n '280,370p' packages/bundler-webpack/src/adapter/index.ts
echo
sed -n '630,705p' packages/bundler-webpack/src/adapter/index.ts

python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/bundler-webpack/src/adapter/index.ts")
s = p.read_text()

def find_class(body, name):
    m = re.search(rf'class\s+{re.escape(name)}[\\s\\S]{{80000}}?\\n{re.escape(name)}', body)
    if m:
        return s.find("class " + name), s.index("\n" + name, m.end()-len(name)), -1
    return -1, -1, -1

classes = [m.start() for m in re.finditer(r'\bclass\s+(\w+)', s)]
for idx, i in enumerate(classes[:-1]):
    print({
        "class": re.search(r'\bclass\s+(\w+)', s[i:]).group(1),
        "line": s.count("\n", 0, i)+1,
        "next_class_line": s.count("\n", 0, classes[idx+1])+1,
    })

print("\nhandleClientStats references:")
for m in re.finditer(r'\bhandleClientStats\b', s):
    line = s.count("\n", 0, m.start())+1
    print(line, s[m.start():m.start()+80].splitlines()[0])
    idx = s.find("private handleClientStats", m.start()-300)
    if idx != -1 and len(s[idx:idx+800]) < 1200:
        print("\n".join(f"{row}: {line}" for row, line in enumerate(s[idx:s.index("\n\n", idx)], idx//s.find("\n")+1)[:80]))

print("\nplan assignments around updatePlanWithFreshServerCompiler:")
for m in re.finditer(r'\b(plan\s*=|config\s*=)\b', s):
    line = s.count("\n", 0, m.start())+1
    if 430 <= line <= 520 or 330 <= line <= 355:
        print(line, s[max(0,s.rfind("\n",0,m.start())):m.start() + 100].splitlines()[-1])
PY

Repository: afx-team/evjs

Length of output: 5113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== handleClientStats outline/source ==="
rg -n -C8 '\bhandleClientStats\b|resolveBuildOutputPaths|assertServerGeneratedModulesStayOutOfClient|latestClientStats|this\.plan' packages/bundler-webpack/src/adapter/index.ts

echo
echo "=== updatePlanWithFreshServerCompiler full body ==="
m=$(grep -n "private async updatePlanWithFreshServerCompiler" packages/bundler-webpack/src/adapter/index.ts | head -1 | cut -d: -f1)
if [ -n "$m" ]; then
  sed -n "$((m-5)),$((m+220))p" packages/bundler-webpack/src/adapter/index.ts
fi

Repository: afx-team/evjs

Length of output: 27756


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== handleClientStats snapshot read/write area ==="
sed -n '846,878p' packages/bundler-webpack/src/adapter/index.ts
echo
echo "=== latestClientStats reads ==="
rg -n -C4 'latestClientStats|emitStats\(' packages/bundler-webpack/src/adapter/index.ts

Repository: afx-team/evjs

Length of output: 4543


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/bundler-webpack/src/adapter/index.ts")
text = p.read_text()

# Extract the class body and call graphs for the two queues/dev-work handlers.
class_start = text.index("class WebpackDevSession")
text = text[class_start:]

class_text = text[:text.index("\n}\n", class_start+1)]
print("callers of updatePlanWithFreshServerCompiler:")
for m in re.finditer(r'\bupdatePlanWithFreshServerCompiler\b', class_text):
    line = class_text.count("\n", 0, m.start()) + class_start.count("\n") + 1
    print(line, class_text[max(0,m.start()-120):m.start()+120].splitlines()[-1])

print("\ncallers of handleClientStats:")
for m in re.finditer(r'\bhandleClientStats\b', class_text):
    line = class_text.count("\n", 0, m.start()) + class_start.count("\n") + 1
    print(line, class_text[max(0,m.start()-100):m.start()+80].splitlines()[-1])

print("\nenqueueDevWork call sites:")
for m in re.finditer(r'\benqueueDevWork\b', class_text):
    line = class_text.count("\n", 0, m.start()) + class_start.count("\n") + 1
    # Print next 10 lines
    rest = class_text[m.start():]
    nl = rest.find("\n")
    print(line, "\n".join(rest.split("\n")[:min(10, rest.count('\n')+1)]))

print("\nmethod signatures with plan/config mutations:")
for name in [
    "applyPlanUpdate",
    "updatePlanWithFreshServerCompiler",
    "recoverPreviousServerCompiler",
    "stop",
    "start",
    "handleClientStats",
]:
    m = re.search(rf'(?m)\b(?:private\s+)?(?:async\s+)?{re.escape(name)}\s*\(', class_text)
    if m:
        start_line = class_text.count("\n", 0, m.start()) + class_start.count("\n") + 1
        end = index_of_next_method_start(class_text, m.end())
        print(f"{name}: {start_line}-{class_text.count(chr(10), 0, end)+class_start.count(chr(10))+1}")
        body = class_text[m.start():end]
        for token in ("this.plan =", "this.config =", "this.planGeneration =", "this.blockArtifactPublication", "await this.drainArtifactPublication"):
            for tm in re.finditer(re.escape(token), body):
                print(f"  {class_text.count(chr(10), 0, m.start()+tm.start())+class_start.count(chr(10))+1}: {token}")

def index_of_next_method_start(class_text, pos):
    # Find the next top-level private/member declaration after pos.
    idx = len(class_text)
    for pat in [r'\n\s*(?:private|async)\s+\w+', r'\n\s*\w+(?:\s*\([^()]*(?:\([^()]*\)[^()]*)*\))?\s*:\s*\w+']:
        for m in re.finditer(pat, class_text[pos:]):
            if m.start() < idx:
                idx = m.start()
    return pos + idx

PY

Repository: afx-team/evjs

Length of output: 325


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/bundler-webpack/src/adapter/index.ts")
text = p.read_text()

# Find class boundaries conservatively.
class_start = text.index("class WebpackDevSession")
next_class = re.search(r"\n\s*class\s+\w+", text[class_start + len("class WebpackDevSession"):])
class_end = class_start + len("class WebpackDevSession") + (next_class.start() if next_class else 0)
class_text = text[class_start:class_end]
base_line = class_start.count("\n") + class_start.count("\r") + 1

print("callers of updatePlanWithFreshServerCompiler:")
for m in re.finditer(r"\bupdatePlanWithFreshServerCompiler\b", class_text):
    line = class_text[:m.start()].count("\n") + 1
    slice_end = text.index("\n", class_start + len("class WebpackDevSession") + 70)
    print(line, text[slice_end-class_start + m.start() - 110:slice_end-class_start + m.start() + 80].splitlines()[-1])

print("\ncallers of handleClientStats:")
for m in re.finditer(r"\bhandleClientStats\b", class_text):
    line = class_text[:m.start()].count("\n") + 1
    print(line, class_text[max(0, m.start()-120):m.start()+80].splitlines()[-1])

print("\nenqueueDevWork call sites:")
for m in re.finditer(r"\benqueueDevWork\<T\>\(", class_text):
    line = class_text[:m.start()].count("\n") + 1
    rest = class_text[m.start():]
    for i, line_txt in enumerate(rest.split("\n")[:8]):
        print(f"{line + i}={line_txt}")

print("\nmutations and publication synchronization in relevant methods:")
for name in [
    "applyPlanUpdate",
    "updatePlanWithFreshServerCompiler",
    "recoverPreviousServerCompiler",
    "stop",
    "start",
    "handleClientStats",
]:
    method = re.search(rf"\b(?:private\s+)?(?:async\s+)?{re.escape(name)}\s*\(", class_text)
    start = method.start() if method else -1
    end = len(class_text)
    for nxt in re.finditer(rf"\b(?:private\s+)?(?:async\s+)?\w+\b", class_text[start+1:], re.S):
        if nxt.end() > start:
            end = start + nxt.start()
            break
    body = class_text[start:end]
    print(f"\n{name}: lines {start.count(chr(10))+1}-{end.count(chr(10))+1}")
    for token in ["this.plan =", "this.config =", "this.planGeneration =", "this.blockArtifactPublication", "await this.drainArtifactPublication", "await this.devWorkQueue"]:
        for tm in re.finditer(re.escape(token), body):
            print(f"  line {1+body[:tm.start()].count(chr(10))}: {token}")
PY

echo
echo "=== deterministic control-flow and queue independence summary ==="
python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/bundler-webpack/src/adapter/index.ts")
s = p.read_text()

def line_before(s, target):
    i = s.index(target)
    nl = s.rfind("\n", 0, i)
    return s[nl+1:i].strip()

for target in [
    "if (hasServerGeneratedRuntimeChange(update.previous, update.next) &&",
    "await this.enqueueDevWork(() =>",
    "void this.enqueueDevWork(() =>",
]:
    print(target)
    print(line_before(s, target))
    print()

for line_no, line in enumerate(s.splitlines(), 1):
    if "private async updatePlanWithFreshServerCompiler" in line:
        start = line_no
        end = i = line_no
        for nl in range(line_no+1, 1200):
            if i >= nl: continue
            if line_no <= nl <= 865:
                if "private async recoverPreviousServerCompiler" in s.splitlines()[nl-1]:
                    break
                if line_no <= nl <= 685:
                    print(f"line {nl}: {s.splitlines()[nl-1]}")
PY

Repository: afx-team/evjs

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path("packages/bundler-webpack/src/adapter/index.ts")
s = p.read_text()
lines = s.splitlines()

def caller_context(target, line_no):
    if line_no < 12:
        start = 0
    else:
        start = line_no - 12
    return "\n".join(f"{i+1}: {lines[i]}" for i in range(start, min(len(lines), line_no+12)))

for target in [
    "updatePlanWithFreshServerCompiler",
    "handleClientStats",
    "enqueueDevWork<T>",
]:
    print(f"== {target} ==")
    for i, line in enumerate(lines, 1):
        if target in line:
            print(f"-- {i} --")
            print(caller_context(target, i))
            print()

print("== targeted queue/context lines ==")
for i, line in enumerate(lines, 1):
    if i in {336, 344, 352, 663, 684, 846}:
        print(f"{i}: {line}")
    elif 340 <= i <= 356 or 660 <= i <= 688:
        print(f"{i}: {line}")
PY

echo
echo "=== static summary ==="
python3 - <<'PY'
from pathlib import Path
s = Path("packages/bundler-webpack/src/adapter/index.ts").read_text()
print("updatePlanWithFreshServerCompiler invoked inside enqueuePlanUpdate lambda before enqueueDevWork? -> ", 
      "await this.updatePlanWithFreshServerCompiler(update, updateOptions);" in s)
print("applyPlanUpdate invoked inside enqueueDevWork? -> ",
      "await this.enqueueDevWork(() =>" in s and "this.applyPlanUpdate(update, updateOptions)" in s)
print("ensureClientServerIsRunning enqueueStat enqueueDevWork? -> ", "void this.enqueueDevWork(() =>" in s and "this.enqueueStats" in s)
PY

Repository: afx-team/evjs

Length of output: 6346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== artifact publication sync code ==="
sed -n '1011,1022p' packages/bundler-webpack/src/adapter/index.ts
echo
echo "=== publishDevArtifacts preconditions ==="
sed -n '962,976p' packages/bundler-webpack/src/adapter/index.ts
echo
echo "=== generateDevArtifacts callers ==="
rg -n -C5 '\bgenerateDevArtifacts\b' packages/bundler-webpack/src/adapter/index.ts

python3 - <<'PY'
from pathlib import Path
s = Path("packages/bundler-webpack/src/adapter/index.ts").read_text()
for target in ["blockArtifactPublication", "allowArtifactPublication", "artifactPublicationQueue"]:
    print(f"-- {target} --")
    for i, line in enumerate(s.splitlines(), 1):
        if target in line:
            print(i, line)
PY

Repository: afx-team/evjs

Length of output: 5402


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '884,910p' packages/bundler-webpack/src/adapter/index.ts

Repository: afx-team/evjs

Length of output: 959


Add the same dev-work/planet-update synchronization around the server-refresh path.

updatePlanWithFreshServerCompiler runs in planUpdateQueue and doesn’t enqueue into devWorkQueue, while handleClientStats runs in devWorkQueue and reads this.plan to resolve output paths and assert generated modules before allowing artifact publication. Since publication is also re-allowed before handleServerStats, this needs a stronger guard such as waiting devWorkQueue before allowing publication and keeping the plan change queued inside a single shared async path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bundler-webpack/src/adapter/index.ts` around lines 448 - 467, The
server refresh in updatePlanWithFreshServerCompiler must synchronize plan
changes with devWorkQueue before artifact publication resumes. Keep the plan
update and publication re-allowance in one shared async path, wait for pending
dev work before allowing publication, and preserve handleClientStats’s ability
to read a consistent this.plan while resolving outputs and validating generated
modules.

await expect(fs.access(path.join(cwd, "src/.ev/types"))).rejects.toThrow();
});

it("materializes exact named exports for strict rootDir projects without paths", async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -H -t f -i 'vitest.config.*' packages/ev | xargs -r cat -n
fd -H -t f -i 'vitest.workspace.*' . | xargs -r cat -n
rg -n 'testTimeout|hookTimeout' --type=ts --type=js -g '!**/node_modules/**'

Repository: afx-team/evjs

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -u

echo "== package vitest/config references =="
rg -n 'vitest|vitest\.config|testTimeout|timeout:|describe\.concurrent|node:test' -g '!**/node_modules/**' -g '!**/.git/**' packages/ev tests 2>/dev/null | head -n 200 || true

echo
echo "== candidate package/test files =="
git ls-files 'packages/ev/**' 'vitest.*' | sed -n '1,200p'

echo
echo "== target test context =="
python3 - <<'PY'
from pathlib import Path
p = Path('packages/ev/tests/build-tools-generated-declarations.test.ts')
if p.exists():
    lines=p.read_text().splitlines()
    for i in [36,43,44,196,202,203,204]:
        if i<=len(lines):
            print(f"{i:4}: {lines[i-1]}")
PY

echo
echo "== all timeout settings =="
rg -n 'testTimeout|hookTimeout|timeout:\s*\d+|timeout:' --type ts --type js --type tsx --type jsx -g '!**/node_modules/**' -g '!**/.git/**' .

Repository: afx-team/evjs

Length of output: 9269


Give the tsc spawning test an explicit timeout.

This case runs a full tsc program in packages/ev; add timeout: 120_000 to this test so cold CI runs don’t hit Vitest’s default per-test timeout.

Also applies to the assertion at lines 202-204 in the same suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ev/tests/build-tools-generated-declarations.test.ts` at line 44, Add
an explicit 120,000 ms timeout to the test named “materializes exact named
exports for strict rootDir projects without paths” and the assertion block at
lines 202–204 that also spawns tsc, using the suite’s existing Vitest timeout
configuration style.

Comment on lines +58 to +80
describe("GeneratedModulePlan", () => {
it("keeps sourceHash required while declaration companions stay additive", () => {
const runtimeOnly: GeneratedModulePlan = {
key: "database:runtime",
id: "database",
pluginName: "database",
scope: { kind: "server" },
file: "./.ev/plugins/database/runtime.ts",
specifier: "evjs:generated/database/runtime",
extension: ".ts",
sourceHash: "a".repeat(64),
};
const typedModule: GeneratedModulePlan = {
...runtimeOnly,
declarationFile: "./src/.ev/types/database/runtime.d.ts",
};

expect(runtimeOnly.declarationFile).toBeUndefined();
expect(typedModule.declarationFile).toBe(
"./src/.ev/types/database/runtime.d.ts",
);
expect(typedModule.sourceHash).toMatch(/^[0-9a-f]{64}$/u);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Actually verify that sourceHash is required.

Both fixtures explicitly include it, so removing sourceHash from GeneratedModulePlan would still pass. Add a negative type fixture with @ts-expect-error that omits the field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/tests/manifest.test.ts` around lines 58 - 80, Extend the
GeneratedModulePlan test with a negative type fixture annotated with
`@ts-expect-error` that constructs a plan without sourceHash. Keep the existing
runtimeOnly and typedModule fixtures unchanged, ensuring the omission fails type
checking and verifies sourceHash remains required.

@xusd320
xusd320 marked this pull request as draft July 30, 2026 08:40
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