feat(plugin): support typed generated module lifecycles - #64
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe 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. ChangesGenerated aliases and materialization
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (10)
packages/ev/src/_internal/build/generated-contributions.ts (1)
1348-1354: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueEmpty
src/.evdirectory is left behind after the last declaration is removed.
syncGeneratedTypesCompanionRootremovessrc/.ev/typesbut not the framework-createdsrc/.evparent, 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 valueBoth ternary branches are identical.
Buffer.from(contents)handlesstringandUint8Arrayalike 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 winConsider registering the discovered PostCSS config with
options.addWatchFile.
createWebpackConfigsalready receivesaddWatchFile, but the resolved PostCSS config path is never watched, so editingpostcss.config.*(or thepostcsskey inpackage.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 winDerive the generated declaration paths from
generated-contributions.jsinstead of re-literalizing them here.
src/.ev/typesandsrc/evjs-env.d.tsare 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/sharedfor 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 winFull module inventory on every dev compile is a notable hot-path cost.
modules/nestedModules/orphanModules/runtimeModuleswith unboundedmodulesSpacemakestoJsonserialize the entire module graph on every dev rebuild, not just production builds. Consider requesting the module sections only whenplan.generatedactually 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
projectRootis only used for a guard that cannot fail.
createEquivalentRootsalways returns at least the resolved root, and the binding is unused afterwards; thecandidatesmapping 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 winNo coverage for
rootPathpropagation into the dev worker.The mock destructures only
{ config, server }, so the newly requiredrootPathoption onstartUtoopackDevWorkeris 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 valuePrefer
it.eachover 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
commitStartedis alwaystruein the catch block.It is assigned before
commitFrameworkState()and nothing between the assignment and thetryboundary can throw, soif (!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 valueConsider 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
📒 Files selected for processing (59)
docs/docs/file-conventions.mddocs/docs/generated-contributions.mddocs/docs/plugins.mddocs/docs/project-structure.mddocs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/file-conventions.mddocs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/generated-contributions.mddocs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/plugins.mddocs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/project-structure.mdexamples/api-routes/.gitignoreexamples/basic/.gitignoreexamples/complex-routing/.gitignoreexamples/custom-ws-transport/.gitignoreexamples/deployment-adapters/.gitignoreexamples/mpa/.gitignoreexamples/plugin-authoring/.gitignoreexamples/qiankun-master/.gitignoreexamples/qiankun-slave/.gitignoreexamples/render-modes/.gitignoreexamples/ssg/.gitignoreexamples/with-sqlite/.gitignoreexamples/with-tailwind/.gitignoreexamples/with-trpc/.gitignorepackages/bundler-utoopack/src/adapter/dev-worker-client.tspackages/bundler-utoopack/src/adapter/dev-worker.tspackages/bundler-utoopack/src/adapter/index.tspackages/bundler-utoopack/src/adapter/runtime.tspackages/bundler-utoopack/src/manifest-generator.tspackages/bundler-utoopack/tests/adapter.test.tspackages/bundler-utoopack/tests/dev-worker-client.test.tspackages/bundler-utoopack/tests/manifest-generator.test.tspackages/bundler-utoopack/tests/runtime.test.tspackages/bundler-webpack/src/adapter/create-config.tspackages/bundler-webpack/src/adapter/index.tspackages/bundler-webpack/src/manifest-generator.tspackages/bundler-webpack/tests/adapter.test.tspackages/bundler-webpack/tests/create-config.test.tspackages/create-app/README.mdpackages/create-app/scripts/deref-templates.jspackages/create-app/src/index.tspackages/create-app/tests/scaffold.test.tspackages/ev/src/_internal/build/analyze-and-materialize.tspackages/ev/src/_internal/build/bundler.tspackages/ev/src/_internal/build/commands.tspackages/ev/src/_internal/build/dev-api-process.tspackages/ev/src/_internal/build/generated-contributions.tspackages/ev/src/_internal/build/index.tspackages/ev/src/_internal/build/owned-file-output.tspackages/ev/src/plugin/index.tspackages/ev/tests/build-tools-generated-declarations.test.tspackages/ev/tests/build-tools-generated-materialization.test.tspackages/ev/tests/build-tools-generated-tsserver.test.tspackages/ev/tests/bundler-capabilities.test.tspackages/ev/tests/commands-api-restart.test.tspackages/ev/tests/commands.test.tspackages/ev/tests/dev-api-process.test.tspackages/ev/tests/package-surface.test.tspackages/ev/tests/resolve-alias-contribution-types.tspackages/shared/src/manifest/index.tspackages/shared/tests/manifest.test.ts
| const UTOOPACK_CLIENT_MODULE_SUFFIX = " [client] (ecmascript)"; | ||
| const UTOOPACK_MODULE_DECORATION = /\s+\[(?:client|server)\]\s+\(.+\)$/u; |
There was a problem hiding this comment.
🔒 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: broadenUTOOPACK_MODULE_DECORATIONto 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.
| 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(); |
There was a problem hiding this comment.
🩺 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.tsRepository: 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])
PYRepository: 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
fiRepository: 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.tsRepository: 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
PYRepository: 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]}")
PYRepository: 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)
PYRepository: 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)
PYRepository: afx-team/evjs
Length of output: 5402
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '884,910p' packages/bundler-webpack/src/adapter/index.tsRepository: 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 () => { |
There was a problem hiding this comment.
🩺 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.
| 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); | ||
| }); |
There was a problem hiding this comment.
📐 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.
ec155c9 to
802bb00
Compare
Summary
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
main/ v0.3.1 CoreGraph architectureValidation
npm run lintnpm run check-types— 31/31 Turbo tasksnpm run build— 24/24 Turbo tasksnpm 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/13npm run test:e2e— 53/53All six commits are GPG-signed.
Summary by CodeRabbit