Skip to content

defold entrypoint#34

Open
cloud9c wants to merge 11 commits into
mainfrom
defold-first-class
Open

defold entrypoint#34
cloud9c wants to merge 11 commits into
mainfrom
defold-first-class

Conversation

@cloud9c
Copy link
Copy Markdown
Contributor

@cloud9c cloud9c commented May 29, 2026

Note

Medium Risk
Changes build/dev upload metadata and entrypoint resolution for a new engine; mistakes could ship the wrong HTML export or break Defold local/play flows until entrypoint is set correctly.

Overview
Adds Defold as a first-class engine across wavedash init, wavedash dev, and build push.

Defold HTML5 exports can include both wasm-web/ and js-web/ trees, so the CLI no longer guesses an HTML file for Defold. Developers must set a top-level entrypoint in wavedash.toml (e.g. wasm-web/<game>/index.html). resolve_defold_entrypoint validates that path (relative, .html, exists under upload_dir, no path escape) and uses the normalized relative path when talking to the API.

wavedash init detects game.project, defaults upload_dir to dist, writes a [defold] section, and documents the required entrypoint. Godot/Unity still use locate_html_entrypoint; their fetch_entrypoint_params calls are unchanged aside from an optional htmlPath field sent for Defold.

build push and dev share the same Defold branch: resolved entrypoint + server-derived entrypointParams from /cli/entrypoint-params (HTML content plus htmlPath).

Reviewed by Cursor Bugbot for commit fb68137. Bugbot is set up for automated code reviews on this repo. Configure here.

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @cloud9c's task in 8m 20s —— View job


Code Review

Reviewed at extra-high effort (5 finder angles × 8 candidates → verify → sweep). Findings posted as inline comments on the PR.

  • Gather diff and PR context
  • Run 5 independent finder angles
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

Summary of findings (10 inline)

High severity

  1. src/dev/entrypoint.rs:61-64 — Sort key precedence is inverted: mtime dominates over the wasm-web//js-web/ architecture preference, so the architecture priority only acts as a tiebreaker when two files share an identical mtime.
  2. src/dev/entrypoint.rs:39-40Option<SystemTime> orders None < Some(_), so a file whose metadata().modified() fails sorts before valid-mtime files and wins selection.
  3. src/dev/entrypoint.rs:51-59architecture_score requires wasm-web//js-web/ to be the first path segment after upload_dir. Defold's default output is build/default/<GameTitle>/wasm-web/...; with upload_dir = "build/default" the preference is silently lost.

Medium severity
4. src/init.rs:323-329 & :254 — Default Defold version "1.0" is not a real Defold release (current is 1.10.x); accepting the default produces a wavedash.toml the server can't resolve.
5. src/init.rs:52default_upload_dir of "dist" is not a Defold convention (Defold bundles to build/default/<GameTitle>/).
6. src/dev/entrypoint.rs:17-21 — Unchanged root-index.html early-return short-circuits the new architecture-aware selection; a stale root index.html silently pre-empts wasm-web/index.html.
7. src/builds.rs:175-179upload_dir is not canonicalized here (unlike dev/mod.rs:101); a strip_prefix failure falls back via unwrap_or and silently leaks an absolute filesystem path to the server as htmlPath.
8. src/init.rs:136-145detect_defold has no version-extraction and no fallback heuristic (compare with detect_godot/detect_unity); version_hint is always None, forcing the prompt onto the hard-coded "1.0" default.

Lower severity
9. src/builds.rs:180-183 — Hardcoded "DEFOLD" literal instead of EngineKind::Defold.as_label(); dev/mod.rs uses the latter. Two sources of truth that can drift.
10. src/dev/entrypoint.rs:82-89 — The new htmlPath field is now unconditionally sent for Godot/Unity callers too (via dev/mod.rs:117 adding them to the same arm), which is a silent server-contract change for existing engines — worth confirming the backend handles it.
Branch: defold-first-class

Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/init.rs
Comment thread src/init.rs
Comment thread src/builds.rs
Comment thread src/builds.rs Outdated
Comment on lines +175 to +179
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.unwrap_or(html_path)
.to_string_lossy()
.replace('\\', "/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

upload_dir is not canonicalized here, unlike dev/mod.rs:101. Both call sites compute html_relative_path the same way, but only handle_dev resolves symlinks/.//.. first. The unwrap_or(html_path) fallback means a strip-prefix failure silently sends the absolute filesystem path to the server as htmlPath — leaking the local layout and almost certainly producing a 4xx.

Easiest fix: canonicalize upload_dir here too (mirror the dev/mod.rs block), or replace unwrap_or with an explicit error so a mismatch becomes a clear bug rather than bad data.

Comment thread src/init.rs
Comment thread src/dev/entrypoint.rs Outdated
Comment on lines 82 to 89
@@ -53,6 +85,7 @@ pub async fn fetch_entrypoint_params(engine: &str, engine_version: &str, html_pa
"engine": engine,
"engineVersion": engine_version,
"htmlContent": html_content,
"htmlPath": html_relative_path,
}))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New htmlPath field is always serialized, including for existing Godot/Unity callers that previously sent only engine/engineVersion/htmlContent (dev/mod.rs:117 now passes Some(&html_relative_path) for Godot/Unity too).

If the server validates request shape strictly, or if it uses htmlPath for resolution and didn't before, this is a silent behavior change for established engines. Worth confirming the backend coordinated change covers Godot/Unity, not just Defold.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@cloud9c this one's legit let's not change the shape of Unity or Godot uploads
Keep the new Defold logic isolated to the Defold case

Comment thread src/dev/entrypoint.rs Outdated
return Some(default_index);
}

let mut html_files: Vec<PathBuf> = Vec::new();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The unchanged early-return on lines 17–21 short-circuits the new architecture-aware selection. If upload_dir/index.html exists for any reason — a stale leftover from a previous engine, a redirect a build tool placed at the root, a user-written launcher — it wins immediately, and wasm-web/index.html is never considered.

For a Defold project migrating onto wavedash, this is a real footgun: any pre-existing root index.html silently pre-empts the proper Defold entrypoint, and fetch_entrypoint_params parses the wrong HTML. Consider gating the early-return on engine kind, or only using it when no nested wasm-web/ / js-web/ directory is present.

@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @cloud9c's task in 2m 59s —— View job


Code Review — Follow-up on commit 10f7bb0

Re-reviewed the branch against origin/main. The refactor commit makes two changes:

  • bumps the Defold default version "1.0""1.12.4" in both init.rs:254 and init.rs:325/326
  • adds an explanatory comment to locate_html_entrypoint justifying the root-index.html short-circuit as intentional for Godot/Unity

That addresses 1 finding (#4) and intentionally pushes back on 1 finding (#6). The remaining 8 inline findings from the prior pass are still applicable — the code at those locations is unchanged.

Status of prior findings

# Severity Location Status
1 High src/dev/entrypoint.rs:64-67 — sort key precedence (mtime dominates architecture) Outstanding — comparator order unchanged
2 High src/dev/entrypoint.rs:42-43Option<SystemTime> orders None < Some Outstanding.ok() still produces Option, no UNIX_EPOCH fallback
3 High src/dev/entrypoint.rs:54-62architecture_score requires wasm-web/ as first segment after upload_dir Outstanding — still uses starts_with("wasm-web/")
4 Medium src/init.rs:254 & :325-326 — default Defold version "1.0" Fixed in 10f7bb0 (now "1.12.4")
5 Medium src/init.rs:51default_upload_dir "dist" is not a Defold convention Outstanding
6 Medium src/dev/entrypoint.rs:21-24 — root index.html early-return short-circuits architecture-aware selection Acknowledged as intentional (new comment at lines 17-19 documents it). Risk note: any pre-existing root index.html in a Defold project (stale leftover, build-tool artifact) will still silently pre-empt the proper wasm-web/index.html. Worth gating on engine kind if Defold projects ever ship with a root index.
7 Medium src/builds.rs:175-179upload_dir not canonicalized (unlike dev/mod.rs:101) Outstandingstrip_prefix still has silent unwrap_or fallback
8 Medium src/init.rs:136-145detect_defold has no version-extraction / fallback heuristic Outstandingversion_hint is still always None
9 Low src/builds.rs:181 — hardcoded "DEFOLD" literal instead of EngineKind::Defold.as_label() Outstanding — two sources of truth still
10 Low src/dev/entrypoint.rs:91htmlPath now sent for Godot/Unity callers too Outstandingdev/mod.rs:117 still passes Some(&html_relative_path) for Godot/Unity in addition to Defold

New observations from this pass

  • build push vs wavedash dev asymmetry on entrypoint params (src/builds.rs:167-189 vs src/dev/mod.rs:116-141). handle_dev fetches entrypointParams from /cli/entrypoint-params for Godot | Unity | Defold, but handle_build_push only does so for Defold — Godot/Unity builds fall through the _ => None arm. This asymmetry pre-dated this PR (it was equivalent to the old executable_entrypoint_params() call returning None for Godot/Unity), so it's not a regression — but worth confirming this is intentional (server computes params at build time for Godot/Unity, only Defold needs them sent up front), not an oversight when wiring Defold through both flows.

Suggested fix for findings #1, #2, #3 in one shot

let architecture_score = |relative: &str| {
    if relative.contains("/wasm-web/") || relative.starts_with("wasm-web/") {
        0
    } else if relative.contains("/js-web/") || relative.starts_with("js-web/") {
        1
    } else {
        2
    }
};

html_files.sort_by(|a, b| {
    let relative_a = a.strip_prefix(upload_dir).unwrap_or(a).to_string_lossy().replace('\\', "/");
    let relative_b = b.strip_prefix(upload_dir).unwrap_or(b).to_string_lossy().replace('\\', "/");
    let modified_a = a.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH);
    let modified_b = b.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH);

    architecture_score(&relative_a)
        .cmp(&architecture_score(&relative_b))
        .then_with(|| modified_b.cmp(&modified_a))
        .then_with(|| relative_a.cmp(&relative_b))
});

Fix entrypoint sort + Option ordering + nested path →

Fix builds.rs canonicalize + DEFOLD label →


  • Gather diff and PR context
  • Compare against prior review findings
  • Identify what was fixed vs still outstanding
  • Look for new issues introduced by the refactor
  • Post status summary
    Branch: defold-first-class

Comment thread src/builds.rs Outdated
@claude
Copy link
Copy Markdown

claude Bot commented May 29, 2026

Claude finished @cloud9c's task in 6m 41s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort. Posted 7 inline findings on the latest commit, ranked most-severe first.

  • Gather diff and PR context (git diff origin/main...HEAD)
  • Run 5 independent finder angles
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

Findings posted as inline comments

High severity — entrypoint selection is broken in three independent ways that compound

  1. src/dev/entrypoint.rs:64-67Sort key precedence inverted. mtime is primary, architecture_score is the tiebreaker. The wasm-web preference is effectively dead code: any js-web/index.html with a newer mtime (very common — any partial rebuild, cp -p, or IDE touch produces this) wins over wasm-web/index.html. [discussion_r3327010584]
  2. src/dev/entrypoint.rs:42-43Option<SystemTime> orders None < Some(_), so a file with a failed metadata().modified() (broken symlink, FS without mtime, transient EACCES) sorts to the top under the descending mtime comparator and wins selection. [discussion_r3327011109]
  3. src/dev/entrypoint.rs:54-62architecture_score only matches when wasm-web/ / js-web/ is the first path segment after upload_dir. Defold's natural bob.jar output is build/default/<GameTitle>/wasm-web/…; if the user sets upload_dir = build/default, the preference silently never fires. [discussion_r3327011836]

Medium severity

  1. src/dev/entrypoint.rs:41-68Sort comparator does file I/O on every comparisonO(N log N) stat() syscalls per sort. Switch to sort_by_cached_key so each file's mtime/key is computed once, and the sort stays consistent if a file is unlinked mid-sort. (New observation from this pass.) [discussion_r3327012596]
  2. src/builds.rs:171-175upload_dir not canonicalized (unlike dev/mod.rs:101), and strip_prefix(...).unwrap_or(html_path) silently sends the absolute filesystem path to the server as htmlPath on any normalization mismatch. [discussion_r3327013225]

Low severity

  1. src/builds.rs:177Engine label hardcoded as "DEFOLD" instead of EngineKind::Defold.as_label(). dev/mod.rs:130 uses the latter — two sources of truth that can drift on any future wire-format change. [discussion_r3327013641]
  2. src/builds.rs:184Wildcard _ => None suppresses the compiler's exhaustiveness check. dev/mod.rs:137 uses explicit None => None; the asymmetry means the next new engine variant will silently bypass entrypoint-param fetching in build push. (Also noted by Cursor Bugbot.) [discussion_r3327014042]

Findings from prior passes still applicable but not re-posted

Avoiding inline duplicates — these remain valid in the current code state:

  • src/init.rs:51default_upload_dir = "dist" is not a Defold convention (Defold bundles to build/default/<GameTitle>/).
  • src/init.rs:135-145detect_defold has no version-extraction or fallback heuristic; version_hint is always None.
  • src/dev/entrypoint.rs:17-23 — root-index.html early-return short-circuits the new architecture-aware selection. Acknowledged as intentional via the new comment, but any stale leftover root index.html in a Defold project will silently pre-empt wasm-web/index.html — gating on engine kind would be safer.
  • src/dev/entrypoint.rs:91-94htmlPath is now serialized for Godot/Unity callers too (dev/mod.rs:130 passes Some(&relative) for all three). Confirm the backend handles this for previously-untouched engines.

Suggested combined fix for findings 1–3

let architecture_score = |relative: &str| {
    if relative.contains("/wasm-web/") || relative.starts_with("wasm-web/") { 0 }
    else if relative.contains("/js-web/") || relative.starts_with("js-web/") { 1 }
    else { 2 }
};

html_files.sort_by_cached_key(|p| {
    let relative = p.strip_prefix(upload_dir).unwrap_or(p).to_string_lossy().replace('\\', "/");
    let architecture = architecture_score(&relative);
    let modified = p.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH);
    (architecture, std::cmp::Reverse(modified), relative)
});

Fix entrypoint sort + Option ordering + nested path + cached key →

Fix builds.rs canonicalize + DEFOLD label + exhaustive match →


Branch: defold-first-class

Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/builds.rs Outdated
Comment on lines +171 to +175
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.unwrap_or(html_path)
.to_string_lossy()
.replace('\\', "/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

upload_dir is not canonicalized here (unlike dev/mod.rs:101), and the .unwrap_or(html_path) fallback silently leaks an absolute filesystem path on strip_prefix failure.

handle_dev resolves upload_dir via .canonicalize()? before walking it (src/dev/mod.rs:101). handle_build_push skips that step, so this call site walks a potentially-relative-or-symlinked path. If strip_prefix(&upload_dir) ever fails (mismatch in normalization between the path returned by WalkDir and upload_dir), unwrap_or(html_path) falls back to the full path — that absolute filesystem path gets sent to the server as htmlPath, leaking the local layout and almost certainly producing a 4xx.

Either canonicalize upload_dir first (mirror dev/mod.rs:101), or replace the fallback with an explicit error so a path mismatch surfaces as a clear bug:

let upload_dir = upload_dir.canonicalize().with_context(|| {
    format!("Failed to canonicalize upload_dir: {}", upload_dir.display())
})?;
// ...
let html_relative_path = html_path
    .strip_prefix(&upload_dir)
    .with_context(|| format!("HTML entrypoint {} is not under upload_dir {}", html_path.display(), upload_dir.display()))?
    .to_string_lossy()
    .replace('\\', "/");

Comment thread src/builds.rs Outdated
Comment thread src/builds.rs Outdated
@claude
Copy link
Copy Markdown

claude Bot commented Jun 1, 2026

Claude finished @cloud9c's task in 4m 11s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort against commit 6cf8c12 ("newest export wins"). Posted 8 inline findings on the current code; one prior finding was fixed and one was intentionally reverted in this round.

  • Gather diff and PR context (git diff origin/main...HEAD)
  • Run independent finder angles
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

What changed since the last review

Prior finding Status
Option<SystemTime> ordering (None sorts as newest) Fixed — now unwrap_or(UNIX_EPOCH) at entrypoint.rs:48,52.
Sort key precedence (mtime vs architecture) Intentionally reversed — comment at entrypoint.rs:73-74 declares "newest export wins" as the design. See finding #2 below for the implication.

Findings posted as inline comments

High severity — incorrect entrypoint selection in Defold flow

  1. architecture_score only matches first-segment wasm-web//js-web/ — Defold's bob.jar writes to <output>/<GameTitle>/wasm-web/..., so a user with upload_dir = "build/default" sees the preference silently never fire. [r3336197445]

  2. "Newest wins" makes architecture_score effectively unreachable on modern filesystems — nanosecond mtime resolution means two files from the same bundler run almost never tie, so the wasm-web preference branch is dead code outside coarse-mtime FSes. Worth verifying bob.jar's actual write order favors wasm-web last on your test cases. [r3336198736]

Medium severity

  1. Sort comparator does stat() on every comparisonO(N log N) syscalls per sort. Switch to sort_by_cached_key so each file's mtime is computed once. [r3336199775]

  2. upload_dir not canonicalized in builds.rs (unlike dev/mod.rs:101); strip_prefix(...).unwrap_or(html_path) silently sends the absolute filesystem path as htmlPath. [r3336201272]

  3. Root index.html short-circuit pre-empts wasm-web/index.html for Defold — the new comment at entrypoint.rs:17-19 acknowledges this as intentional for Godot/Unity, but a stale leftover root index.html in a Defold project is a real footgun. Gate on engine kind. [r3336207637]

  4. default_upload_dir = "dist" for Defold — not a Defold convention; bob.jar writes to build/default/<GameTitle>/. Default produces a wavedash.toml whose upload_dir doesn't exist. [r3336204651]

  5. detect_defold has no version-extraction or fallback heuristicversion_hint is always None, forcing the user onto the hardcoded "1.12.4" default. [r3336206273]

Low severity

  1. Hardcoded "DEFOLD" literal in builds.rs:177 instead of EngineKind::Defold.as_label() — two sources of truth that can drift. [r3336201962]

  2. Wildcard _ => None in builds.rs:184 suppresses the compiler's exhaustiveness check. dev/mod.rs:137 uses explicit None => None. Cursor Bugbot also flagged this on the prior commit. [r3336203584]

Earlier findings still applicable but not re-posted

To avoid inline duplicates these still hold:

  • htmlPath is now serialized for Godot/Unity callers too (dev/mod.rs:130 passes Some(&relative) for all three engines). Worth confirming the backend handles the new field gracefully for previously-untouched engines — not just Defold.

Suggested combined fix for findings #1, #2, #3

let architecture_score = |relative: &str| {
    if relative.contains("/wasm-web/") || relative.starts_with("wasm-web/") { 0 }
    else if relative.contains("/js-web/") || relative.starts_with("js-web/") { 1 }
    else { 2 }
};

html_files.sort_by_cached_key(|p| {
    let relative = p.strip_prefix(upload_dir).unwrap_or(p).to_string_lossy().replace('\\', "/");
    let architecture = architecture_score(&relative);
    let modified = p.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
    // Newest-wins design intact; nested-path match restored; one stat per file.
    (std::cmp::Reverse(modified), architecture, relative)
});

Branch: defold-first-class

Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/builds.rs Outdated
Comment on lines +171 to +175
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.unwrap_or(html_path)
.to_string_lossy()
.replace('\\', "/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

upload_dir is not canonicalized here (unlike dev/mod.rs:101), and .unwrap_or(html_path) silently leaks the absolute filesystem path on strip_prefix failure.

handle_dev resolves upload_dir via .canonicalize()? before walking it (src/dev/mod.rs:101). handle_build_push skips that step, so this call site walks a potentially-relative-or-symlinked path. WalkDir returns absolute paths in some cases (e.g. when given an absolute root) and relative in others — if strip_prefix(&upload_dir) mismatches normalization, unwrap_or(html_path) falls back to the full path, sending an absolute filesystem path to the server as htmlPath. That leaks the local layout ($HOME/projects/<game>/...) and almost certainly produces a 4xx on the server.

Mirror the dev/mod.rs canonicalize step, or replace the fallback with an explicit error:

let upload_dir = upload_dir.canonicalize().with_context(|| {
    format!("Failed to canonicalize upload_dir: {}", upload_dir.display())
})?;
// ...
let html_relative_path = html_path
    .strip_prefix(&upload_dir)
    .with_context(|| format!("HTML entrypoint {} is not under upload_dir {}", html_path.display(), upload_dir.display()))?
    .to_string_lossy()
    .replace('\\', "/");

Fix builds.rs canonicalize →

@cloud9c cloud9c marked this pull request as ready for review June 1, 2026 21:08
@claude
Copy link
Copy Markdown

claude Bot commented Jun 1, 2026

Claude finished @cloud9c's task in 9m 16s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort against commit 1aa904f ("remove out of scope changes"). Posted 6 inline findings; the headline is a regression introduced by the latest commit — the canonicalize safety net that commit a291f10 ("path safety") had explicitly added for the new Defold code was removed.

  • Gather diff and PR context (git diff origin/main...HEAD)
  • Run 5 independent finder angles
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

What changed since the prior review (commit 99f8b5d)

Prior state Latest state
builds.rs canonicalized upload_dir before strip_prefix (dev parity) Removed in 1aa904f — described as "out of scope"

The remaining state from prior passes is intact: explicit match arms in builds.rs, EngineKind::Defold.as_label() (no hardcoded "DEFOLD"), architecture_score matches anywhere in the path, unwrap_or(UNIX_EPOCH) on stat failure, and pre-computed sort keys.

Findings posted as inline comments

High severity — regression

  1. src/builds.rs:171-175Canonicalize was removed but strip_prefix(&upload_dir).unwrap_or(html_path) (introduced by this PR) is still here. A strip_prefix mismatch (symlinked path, macOS /var/private/var, .. segments) silently falls through to the absolute path and sends e.g. htmlPath: /home/<dev>/projects/<game>/dist/... to the server. wavedash dev is unaffected (still canonicalizes at dev/mod.rs:101), so the two flows now silently disagree on htmlPath for the same project. [r3337237169]

Medium severity

  1. src/dev/entrypoint.rs:19-23Root index.html short-circuit pre-empts wasm-web/index.html for Defold. The comment now declares the short-circuit is for "single-HTML exports (Godot/Unity)" but it isn't gated on engine kind. A leftover root index.html (previous engine, redirect page, hand-written launcher) silently steals selection for Defold. [r3337232704]

  2. src/dev/entrypoint.rs:71-77"Newest export wins" makes architecture_score effectively unreachable on ns-mtime filesystems. Two HTMLs from the same bob.jar invocation almost never tie on ext4/APFS/NTFS, so the wasm-web preference branch only fires under coarse-mtime FSes. If bob.jar happens to write js-web after wasm-web, every fresh full export inverts the documented preference. Worth empirically verifying bob.jar's write order — or bucketing mtimes by second so the preference actually decides ties. [r3337233479]

  3. src/dev/entrypoint.rs:25-38Unbounded walk + newest-wins lets unrelated HTML steal selection. WalkDir with no max_depth or skip-list traverses node_modules/, asset readmes, vendored library demos. Any of these newer than the bundle (npm install timestamp, IDE coverage report, asset re-extraction) wins under the new comparator. Cap depth or filter common-noise directories. [r3337234316]

Low severity — carried over from prior passes

  1. src/init.rs:51-52default_upload_dir = "dist" is not a Defold convention. New comment explains why not build/ (Defold's cache lives there), but dist/ isn't where bob.jar writes either. Default to build/default or prompt during init. [r3337234853]

  2. src/init.rs:136-146detect_defold has no version extraction or fallback heuristic. version_hint is always None; the prompt always falls through to the hardcoded "1.12.4" default which will go stale. [r3337235314]

Verdict

The PR was converging well at 99f8b5d. The latest commit (1aa904f) shipped a real regression by removing a canonicalize block that was added in the prior commit specifically to protect the new Defold-only strip_prefix call. Restoring it (or replacing the unwrap_or fallback with an explicit error) is the single most important change before merge. The medium-severity items are quality-of-life / robustness for Defold's bundle layout. Findings #5 and #6 are unchanged UX cliffs from prior passes.
Branch: defold-first-class

Comment thread src/dev/entrypoint.rs
Comment on lines 19 to 23
pub fn locate_html_entrypoint(upload_dir: &Path) -> Option<PathBuf> {
let default_index = upload_dir.join("index.html");
if default_index.is_file() {
return Some(default_index);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Root index.html short-circuit still pre-empts wasm-web/index.html for Defold.

The comment at lines 17–18 documents the intent: this short-circuit is for "single-HTML" exports (Godot/Unity) and the ranking below is "for nested multi-HTML Defold dists." But the short-circuit doesn't check engine kind — it runs for everyone. So in a Defold project:

  • Any stale index.html at upload_dir/ (leftover from a previous engine, a redirect placed at the root, a hand-written launcher, a build-tool artifact, even an OS-Indexer-generated file) wins immediately.
  • The properly nested MyGame/wasm-web/index.html is never considered.
  • fetch_entrypoint_params then parses the wrong HTML; the server returns params for a non-existent build, and the playtest URL renders blank / 404s on missing chunks.

For Defold migrations specifically this is a real footgun — a dist/index.html from a previous threejs/phaser setup silently steals the entrypoint. Gating on engine kind closes it cleanly:

pub fn locate_html_entrypoint(upload_dir: &Path, engine: Option<EngineKind>) -> Option<PathBuf> {
    if !matches!(engine, Some(EngineKind::Defold)) {
        let default_index = upload_dir.join("index.html");
        if default_index.is_file() {
            return Some(default_index);
        }
    }
    // ... walk + sort for Defold (and as a fallback)
}

Callers in dev/mod.rs:111 and builds.rs:165 would pass engine_kind through.

Fix root index.html short-circuit for Defold →

Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/entrypoint.rs Outdated
Comment on lines 25 to 38
@@ -28,16 +31,60 @@
if entry.file_type().is_file() {
if let Some(ext) = entry.path().extension() {
if ext.eq_ignore_ascii_case("html") {
return Some(entry.into_path());
html_files.push(entry.into_path());
}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unbounded walk + "newest mtime wins" lets unrelated HTML files steal selection.

WalkDir::new(upload_dir).min_depth(1) with no .max_depth(...) and no skip-list traverses everything under upload_dir, including:

  • node_modules/ (if anyone vendors deps into the upload dir)
  • asset-pack readmes (docs/index.html, examples/foo.html)
  • engine demo HTML inside vendored libraries
  • .git/ (no — WalkDir hits it; you'd need .filter_entry to skip)
  • IDE-generated coverage / report HTML

Under the new "newest wins" comparator (line 73-77), any of these with an mtime newer than the actual wasm-web/index.html wins selection. Concrete scenario:

upload_dir/
  MyGame/wasm-web/MyGame.html    # bob.jar build, mtime T
  MyGame/js-web/MyGame.html      # bob.jar build, mtime T+1ms
  node_modules/foo/example.html  # npm install at T+1h

locate_html_entrypoint returns node_modules/foo/example.html (newest). fetch_entrypoint_params reads that file and sends its contents as htmlContent to the server; the server returns nonsense params and the playtest is broken — with no diagnostic pointing at the cause.

Two ways to harden:

  1. Cap depth: Defold's deepest legitimate path under upload_dir is 2–3 levels (<GameTitle>/wasm-web/<file>.html). .max_depth(4) would prune most accidental candidates.
  2. Filter common noise: skip node_modules, .git, vendor, docs, examples via .filter_entry(|e| !is_noise(e.file_name())).

Either makes selection robust against the most common ways stray HTML lands under upload_dir.

Comment thread src/init.rs
Comment thread src/init.rs
Comment thread src/builds.rs Outdated
Comment on lines +171 to +175
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.unwrap_or(html_path)
.to_string_lossy()
.replace('\\', "/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Regression in 1aa904f ("remove out of scope changes"): the canonicalize safety net for this strip_prefix was deleted.

Commit a291f10 ("path safety") added a canonicalize() block just above the file-staging step with the rationale "so a strip_prefix miss can't leak an absolute path as htmlPath (mirrors wavedash dev)." Commit 1aa904f removed that block, calling it out of scope — but the strip_prefix(&upload_dir).unwrap_or(html_path) right here was introduced by this same PR to produce htmlPath for Defold. So the canonicalize wasn't out of scope; it was the guard for new code this PR added.

What goes wrong now:

  • upload_dir = config_dir.join(&wavedash_config.upload_dir) is unnormalized. With a symlinked path (macOS /var/private/var, /tmp symlinks), .. segments, or platform-specific normalization, WalkDir can emit entries whose textual prefix doesn't match upload_dir exactly.
  • strip_prefix(&upload_dir) returns Err; .unwrap_or(html_path) falls through to the full absolute path.
  • The server receives htmlPath: "/home/<dev>/projects/<game>/dist/MyGame/wasm-web/MyGame.html" (leaks the dev's layout) instead of MyGame/wasm-web/MyGame.html, and almost certainly produces a 4xx because nothing under R2 is keyed that way.
  • wavedash dev is unaffected (it still canonicalizes at dev/mod.rs:101), so the two flows silently disagree on htmlPath for the same project.

Two fixes; either is fine:

Option A — restore the canonicalize block above FileStaging::prepare (mirrors dev):

let upload_dir = upload_dir
    .canonicalize()
    .with_context(|| format!("Failed to canonicalize upload_dir: {}", upload_dir.display()))?;

(also restore use anyhow::{Context, Result}; at the top of the file.)

Option B — replace the unwrap_or fallback here with an explicit error so a mismatch surfaces loudly instead of leaking a path:

Suggested change
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.unwrap_or(html_path)
.to_string_lossy()
.replace('\\', "/");
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.with_context(|| format!("HTML entrypoint {} is not under upload_dir {}", html_path.display(), upload_dir.display()))?
.to_string_lossy()
.replace('\\', "/");

Restore canonicalize in builds.rs →

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished @cloud9c's task in 9m 4s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort against commit c4ec363 ("remove sniffing"). The latest commit deliberately pivoted away from architecture/mtime sniffing in favor of requiring an explicit entrypoint in wavedash.toml for Defold — a much simpler design that moots most prior findings about sort precedence, mtime fallback, and nested-path matching.

Posted 5 inline findings, ranked most-severe first.

  • Gather diff and PR context (git diff origin/main...HEAD)
  • Run finder angles (line-by-line, removed-behavior, cross-file tracer, language pitfalls)
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

What changed since the last review

Prior finding Status
Sort key precedence / mtime / wasm-web preference Moot — entire sort block removed; Defold now requires explicit entrypoint.
Option<SystemTime> ordering Moot — comparator gone.
architecture_score first-segment match Moot — comparator gone.
sort_by_cached_key for stat efficiency Moot — sort gone.
Hardcoded "DEFOLD" label Already fixed.
Wildcard _ => None match arm Already fixed (explicit Some(Godot | Unity) => None).
Root index.html short-circuit for Defold Moot — Defold no longer uses locate_html_entrypoint.
default_upload_dir = "dist" Resolved by maintainerbob.jar -bo has no default, so dist is sound.
detect_defold version extraction Resolved by maintainergame.project doesn't record the engine version; None is the honest answer.

Findings posted

High severity

  1. htmlPath is still sent for Godot/Unity (src/dev/mod.rs:113-132 + src/dev/entrypoint.rs:91). @calvinstudebaker explicitly flagged this on the prior pass — "let's not change the shape of Unity or Godot uploads. Keep the new Defold logic isolated to the Defold case." The Godot/Unity branch still passes Some(&html_relative_path) to fetch_entrypoint_params, which always serializes htmlPath (including as null). Fix is two-part: pass None from the Godot/Unity arm + make fetch_entrypoint_params skip the field when None. [r3343579974]

Medium severity

  1. resolve_defold_entrypoint path-escape (src/dev/entrypoint.rs:49-68). upload_dir.join(user_string) with no normalization and no containment check. entrypoint = "../../foo/index.html" reads above upload_dir; entrypoint = "/abs/path.html" reads anywhere (Rust's Path::join lets an absolute argument replace the receiver). Both pass is_file() and surface as wrong-file uploads with no clear error. Recommend canonicalize + starts_with(canon_upload) check. [r3343581663]

  2. htmlPath always serialized in JSON (src/dev/entrypoint.rs:91). The json! macro emits "htmlPath": null when Option<&str> is None. Tied to finding Chunk uploads to <500mb chunks so we can cache on r2-asset-proxy #1 — even after the dev/mod.rs change, the request body would still carry the field. Build the body conditionally. [r3343583512]

Low severity

  1. FileStaging::prepare skips Defold entrypoint validation (src/builds.rs:153 + src/file_staging.rs:16). wavedash_config.entrypoint() (the method) returns None for any engine, so the early extension + existence check that protects Custom builds never runs for Defold. A typo like entrypoint = "wasm-web/<game>/something.png" slips past, resolve_defold_entrypoint only checks is_file(), and the binary file is read and POSTed as htmlContent. Two access patterns for the same field (.entrypoint() method vs .entrypoint direct) can drift. [r3343584738]

  2. wavedash init for Defold writes a comment-only entrypoint hint (src/init.rs:254-263). The outro promises "Next steps: → Run wavedash dev" but wavedash dev errors immediately on a fresh init because entrypoint is missing. Either prompt during init or surface a cliclack::log::info line that the user needs to edit wavedash.toml. [r3343585991]

Verdict

The pivot to explicit entrypoint for Defold (commit c4ec363) was the right call — it deletes a meaningful surface area of fragile heuristics and replaces it with a clear user contract. The one outstanding high-severity issue is the maintainer-flagged Godot/Unity request-shape regression (#1), which is also the cleanest to fix in one go with #3. The medium and low items are quality-of-life / robustness items rather than blockers.
Branch: defold-first-class

Comment thread src/dev/entrypoint.rs Outdated
Comment thread src/dev/mod.rs
Comment on lines +113 to +132
Some(kind @ (EngineKind::Godot | EngineKind::Unity)) => {
let engine_label = kind.as_label();
let html_path = locate_html_entrypoint(&upload_dir).ok_or_else(|| {
anyhow::anyhow!(
"No HTML file found in upload_dir; required for {} builds",
engine_label
)
})?;
let ver = engine_version
.ok_or_else(|| anyhow::anyhow!("{} engine requires a version", engine_label))?;
Some(fetch_entrypoint_params(engine_label, ver, html_path).await?)
let html_relative_path = html_path
.strip_prefix(&upload_dir)
.unwrap_or(&html_path)
.to_string_lossy()
.replace('\\', "/");
Some(
fetch_entrypoint_params(engine_label, ver, &html_path, Some(&html_relative_path))
.await?,
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

htmlPath is now sent for Godot/Unity uploads — explicitly flagged by @calvinstudebaker on the prior pass: "let's not change the shape of Unity or Godot uploads. Keep the new Defold logic isolated to the Defold case."

The Godot/Unity branch (lines 113–132) computes html_relative_path and passes Some(&html_relative_path) into fetch_entrypoint_params. Combined with fetch_entrypoint_params always serializing "htmlPath": … unconditionally, the JSON request body for established Godot/Unity callers grew a new htmlPath field that wasn't there pre-PR — exactly the contract change the maintainer asked to avoid.

Suggested isolation: keep locate_html_entrypoint use for Godot/Unity, but pass None for html_relative_path (and have fetch_entrypoint_params skip the field when None — see separate comment at entrypoint.rs:91). Concretely:

Some(kind @ (EngineKind::Godot | EngineKind::Unity)) => {
    let engine_label = kind.as_label();
    let html_path = locate_html_entrypoint(&upload_dir).ok_or_else(|| {
        anyhow::anyhow!(
            "No HTML file found in upload_dir; required for {} builds",
            engine_label
        )
    })?;
    let ver = engine_version
        .ok_or_else(|| anyhow::anyhow!("{} engine requires a version", engine_label))?;
    Some(fetch_entrypoint_params(engine_label, ver, &html_path, None).await?)
}

This also makes the strip_prefix(&upload_dir).unwrap_or(&html_path) block at lines 123–127 unnecessary (and removes its silent-leak fallback in the case strip_prefix ever fails).

Fix Godot/Unity request-shape regression →

Comment thread src/dev/entrypoint.rs
Comment on lines +49 to +68
pub fn resolve_defold_entrypoint(
upload_dir: &Path,
entrypoint: Option<&str>,
) -> Result<(PathBuf, String)> {
let entrypoint = entrypoint.ok_or_else(|| {
anyhow::anyhow!(
"Defold builds need an `entrypoint` in wavedash.toml pointing to your HTML5 export, e.g.\n entrypoint = \"wasm-web/<game>/index.html\"\n(a Defold bundle can contain both wasm-web/ and js-web/ — pick one)"
)
})?;
let relative_path = entrypoint.replace('\\', "/");
let html_path = upload_dir.join(&relative_path);
if !html_path.is_file() {
anyhow::bail!(
"Defold entrypoint `{}` not found under {}. Point `entrypoint` in wavedash.toml at your export's index.html.",
entrypoint,
upload_dir.display()
);
}
Ok((html_path, relative_path))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

resolve_defold_entrypoint joins a user-controlled string straight into upload_dir with no normalization or upload-dir containment check — the entrypoint from wavedash.toml can escape the build directory via .. segments or be an absolute path.

Two concrete misconfigurations:

  1. entrypoint = "../../something/index.html" in wavedash.tomlupload_dir.join("../../something/index.html") resolves above upload_dir. If that file happens to exist, is_file() passes; fs::read_to_string(&html_path) reads its content; and html_relative_path = "../../something/index.html" is sent verbatim to the server as htmlPath. The build then references assets via that escape path on the playsite — almost certainly serving a broken bundle.

  2. entrypoint = "/Users/dev/somethingelse/index.html" → Rust's Path::join with an absolute argument replaces the receiver, so upload_dir.join("/Users/dev/…") is just /Users/dev/…. is_file() and read_to_string both succeed on the absolute path; html_relative_path = "/Users/dev/…" leaks the dev's filesystem layout to the server as htmlPath.

The user "owns their own machine" so this isn't a security exploit, but it surfaces typos and copy-paste mistakes as silent wrong-file uploads instead of clear errors. A cheap guard: canonicalize both and check containment:

let html_path = upload_dir.join(&relative_path);
if !html_path.is_file() {
    anyhow::bail!(/* same as today */);
}
// Reject anything that escapes upload_dir.
let canon_html = html_path
    .canonicalize()
    .with_context(|| format!("Failed to canonicalize {}", html_path.display()))?;
let canon_upload = upload_dir
    .canonicalize()
    .with_context(|| format!("Failed to canonicalize {}", upload_dir.display()))?;
if !canon_html.starts_with(&canon_upload) {
    anyhow::bail!(
        "Defold entrypoint `{}` resolves outside upload_dir ({}). The path must be relative to upload_dir and stay inside it.",
        entrypoint,
        upload_dir.display()
    );
}

Bonus: also reject paths whose lowercase extension isn't .html / .htmFileStaging::prepare does this for Custom builds but skips Defold entirely because wavedash_config.entrypoint() returns None for engines.

Comment thread src/dev/entrypoint.rs Outdated
"engine": engine,
"engineVersion": engine_version,
"htmlContent": html_content,
"htmlPath": html_relative_path,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

htmlPath is serialized unconditionallyOption<&str> with None produces "htmlPath": null in the JSON body, not an omitted field. Both halves of the contract change for Godot/Unity (carrying the field at all, and emitting it as null rather than skipping) are encoded here — so even after dev/mod.rs is updated to pass None for Godot/Unity, the request body would still carry "htmlPath": null.

To make htmlPath truly Defold-only, build the JSON body conditionally before posting:

let mut body = serde_json::json!({
    "engine": engine,
    "engineVersion": engine_version,
    "htmlContent": html_content,
});
if let Some(html_path) = html_relative_path {
    body["htmlPath"] = serde_json::json!(html_path);
}

let response = client.post(&endpoint).json(&body).send().await
    .with_context(|| "Failed to call CLI entrypoint params endpoint")?;

Combined with the dev/mod.rs change to pass None for Godot/Unity, Godot/Unity requests go back to the pre-PR three-field shape, and only Defold sends htmlPath.

Comment thread src/builds.rs
Comment on lines +164 to +178
Some(EngineKind::Defold) => {
let (html_path, html_relative_path) =
resolve_defold_entrypoint(&upload_dir, wavedash_config.entrypoint.as_deref())?;
let ver = engine_version
.ok_or_else(|| anyhow::anyhow!("DEFOLD engine requires a version"))?;
Some(
fetch_entrypoint_params(
EngineKind::Defold.as_label(),
ver,
&html_path,
Some(&html_relative_path),
)
.await?,
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FileStaging::prepare at line 153 doesn't validate the Defold entrypoint because wavedash_config.entrypoint() returns None for any engine (see config.rs:292-301: the method only returns the entrypoint when no engine is configured). For Custom builds, FileStaging::prepare enforces .html/.htm/.js extension and file existence. For Defold, both checks are skipped, and validation happens later inside resolve_defold_entrypoint (line 60), which only checks is_file() — extension is never validated.

Result: a user who sets entrypoint = "wasm-web/<game>/something.png" (typo or wrong asset) sails through FileStaging::prepare, gets to resolve_defold_entrypoint which passes is_file(), then fetch_entrypoint_params reads a binary file and POSTs it as htmlContent. The server most likely returns a parse error; the user sees an opaque API failure instead of "Entrypoint must be an HTML file".

Two ways to close this:

  1. Move the Defold validation into FileStaging::prepare so it mirrors the Custom path (check extension + existence early, with a clean error message).
  2. Or have resolve_defold_entrypoint itself validate the extension before reading the file.

Either way, the gap is in FileStaging::preparewavedash_config.entrypoint() (the method) silently filters Defold out of validation, while wavedash_config.entrypoint (the field) is what actually drives Defold behavior at lines 166 and dev/mod.rs:135. Two access patterns for the same field that can drift.

Comment thread src/init.rs
Comment on lines +254 to +263
EngineType::Defold => {
let version = engine_version.unwrap_or("1.12.4");
// `entrypoint` is a top-level key, so the hint must precede the [defold]
// table. Left commented — the dev fills in the export they want to ship;
// `wavedash dev` / `build push` errors clearly until they do.
toml.push_str(
"\n# Defold HTML5 bundles can contain both wasm-web/ and js-web/ folders.\n# Set `entrypoint` (top-level) to the index.html you want to ship, e.g.:\n# entrypoint = \"wasm-web/<game>/index.html\"\n",
);
toml.push_str(&format!("\n[defold]\nversion = \"{}\"\n", version));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

wavedash init for Defold writes a comment-only entrypoint hint, not an actual entrypoint = … line. A user who runs init and then immediately tries wavedash dev / build push hits the error from resolve_defold_entrypoint ("Defold builds need an entrypoint in wavedash.toml…") before anything works. There's no signal during init that more editing is required.

The error message is clear once you trigger it, but the UX cliff is:

  1. wavedash init — completes successfully, prints "Created wavedash.toml! Next steps: → Run wavedash dev to test locally".
  2. User runs wavedash dev — errors out because entrypoint is missing.
  3. User opens wavedash.toml, sees a hint in a comment, fills it in.

Two cheaper alternatives, in order of effort:

  • Tell the user during init ("Defold requires an entrypoint — edit wavedash.toml before running wavedash dev") via cliclack::log::info or a prefilled-but-required prompt.
  • Better: actually prompt for the entrypoint during init so the produced wavedash.toml works end-to-end on a clean run. The path is dynamic (wasm-web/<GameTitle>/index.html) but the user knows it; the same prompt that asks for upload_dir can ask for entrypoint for Defold.

Right now wavedash init for Defold is the only engine where the next step the outro promises doesn't work without manual file editing. Worth at least an info line so the user isn't surprised.

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished @cloud9c's task in 12m 16s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort against commit 64048d1 ("edge cases"). The latest commit fixes the maintainer-flagged Godot/Unity request-shape regression (htmlPath now conditional) and adds path-escape defenses to resolve_defold_entrypoint (component check + extension check + canonicalize containment). The PR has converged — high-severity findings from prior passes are all resolved or mooted by the pivot to explicit entrypoint. Posted 5 inline findings, ranked most-severe first.

  • Gather diff and PR context against origin/main
  • Run independent finder angles
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

What changed since the last review

Prior finding Status
htmlPath sent for Godot/Unity uploads (maintainer-flagged regression) Fixedentrypoint.rs:120-127 now serializes htmlPath only when Some, and dev/mod.rs:126 passes None for Godot/Unity.
resolve_defold_entrypoint path-escape Fixed — component-based escape rejection at lines 60-73 + canonicalize containment at lines 92-104.
FileStaging::prepare skips Defold entrypoint validation (extension/existence) Mootedresolve_defold_entrypoint now does its own extension check (lines 75-81) and existence check (line 84).
Wildcard _ => None exhaustiveness Already fixed in prior commit.
Hardcoded "DEFOLD" literal Already fixed in prior commit.

Findings posted

Medium severity

  1. Defold entrypoint validation runs after the full scan_directory in build push (builds.rs:170). FileStaging::prepare is a no-op for Defold (entrypoint() method returns None for any non-None engine), and the scan walks the entire upload_dir before resolve_defold_entrypoint runs. A user who forgot to set entrypoint waits for the scan before getting a clear error.

  2. upload_dir is still not canonicalized in handle_build_push (builds.rs:161). Moot for escape safety (resolve_defold_entrypoint re-canonicalizes internally), but the two flows still emit different paths in error messages for the same misconfiguration, and the returned html_path is un-canonical.

  3. Slash-normalization drift between entrypoint field and entrypointParams.htmlPath (builds.rs:163). resolve_defold_entrypoint normalizes \\/ for htmlPath, but the top-level entrypoint field uses the raw user string. The narrow case where a user writes backslashes in wavedash.toml was anticipated by the normalize call — this inconsistency negates that effort.

Low severity

  1. wavedash init for Defold still produces a TOML where wavedash dev errors immediately (init.rs:263). The new comment acknowledges the cliff but the outro still promises "Run wavedash dev" as the next step. A cliclack::log::info warning or an actual prompt would close the gap.

  2. Test cleanup leaks temp directories on panic (entrypoint.rs:166-209). fs::remove_dir_all is the last line of each test, so any earlier assert! failure skips cleanup. CI-only impact; tempfile::TempDir would handle this via RAII.

Verdict

The pivot to explicit entrypoint and the latest "edge cases" commit landed the PR in good shape. None of the remaining findings are blockers — #1 and #2 are robustness / symmetry items, #3 is a narrow edge case, #4#5 are UX / test hygiene. The Godot/Unity request-shape regression that @calvinstudebaker flagged is properly fixed.
Branch: defold-first-class

Comment thread src/builds.rs
Comment thread src/builds.rs Outdated
let entrypoint_params = match engine_kind {
Some(EngineKind::Defold) => {
let (html_path, html_relative_path) =
resolve_defold_entrypoint(&upload_dir, wavedash_config.entrypoint.as_deref())?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For Defold, resolve_defold_entrypoint runs after FileStaging::prepare (line 153) and scan_directory (line 156) — both of which are no-ops for catching a missing/invalid Defold entrypoint, so the user pays for a full directory walk before getting a clear error.

Trace for a user who ran wavedash init and hasn't yet edited entrypoint into their wavedash.toml:

  1. Line 153 — FileStaging::prepare(&upload_dir, &wavedash_config) calls wavedash_config.entrypoint() (the method at config.rs:292-301), which returns None for any non-None engine_type. Defold entrypoint is not validated.
  2. Line 156 — scan_directory(&upload_dir) walks the entire upload_dir, hashing each file. On a real Defold bundle with wasm-web/ + js-web/ + assets, this is the slow step.
  3. Line 167-190 — only now does the Some(EngineKind::Defold) arm call resolve_defold_entrypoint(...) and finally surface the "Defold builds need an entrypoint…" error.

For dev/mod.rs (handle_dev), there's no scan_directory step, so the cost is small. For handle_build_push, the scan is meaningful work to throw away.

Moving resolve_defold_entrypoint (or at minimum the entrypoint existence check) above FileStaging::prepare and scan_directory makes the error fast and matches the contract that FileStaging::prepare advertises for Custom engines (validate-before-upload).

Fix Defold entrypoint validation ordering in builds.rs →

Comment thread src/builds.rs Outdated
// Get temporary R2 credentials (includes build size)
let engine_kind = wavedash_config.engine_type()?;
let entrypoint = match engine_kind {
Some(EngineKind::Defold) => wavedash_config.entrypoint.as_deref(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Slash-normalization drift between the entrypoint field and entrypointParams.htmlPath sent to the server.

resolve_defold_entrypoint deliberately normalizes \\/ (entrypoint.rs:58) — the maintainer anticipated users writing Windows-style paths in wavedash.toml. That normalized value lands in entrypointParams.htmlPath.

But at line 163 (and the parallel dev/mod.rs:108), the top-level entrypoint field uses wavedash_config.entrypoint.as_deref() — the raw unmodified string. So if a user writes:

entrypoint = "wasm-web\\game\\index.html"

the server receives "entrypoint": "wasm-web\\game\\index.html" and "entrypointParams": { "htmlPath": "wasm-web/game/index.html" }. Two different shapes for the same file; whichever the server uses for resolution decides which is correct.

It's a narrow case (most users will type forward slashes), but the back-slash handling in resolve_defold_entrypoint exists specifically for users who don't — and the inconsistency negates that effort.

Cheapest fix: reuse the normalized html_relative_path returned by resolve_defold_entrypoint as the top-level entrypoint field, so both fields agree on slashes. Concretely, hoist the resolve call so its html_relative_path is in scope for both the entrypoint binding and the entrypoint_params arm.

Fix slash normalization drift →

Comment thread src/builds.rs Outdated
}

// Get temporary R2 credentials (includes build size)
let engine_kind = wavedash_config.engine_type()?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

upload_dir is still not canonicalized in handle_build_push — only resolve_defold_entrypoint's internal canonicalize-containment check runs, while dev/mod.rs:101 canonicalizes once and the rest of the flow operates on a clean absolute path.

This was raised in a prior review and is moot for path-escape safetyresolve_defold_entrypoint re-canonicalizes both sides before its starts_with check (entrypoint.rs:92-104), so escapes are rejected. But the broader symmetry concern still applies:

  1. The returned html_path at entrypoint.rs:106 is the un-canonicalized upload_dir.join(relative_path). It's read via fs::read_to_string (fine), but it flows into fetch_entrypoint_params and any error messages downstream — the two flows disagree on the textual path for the same project.
  2. Error messages and diagnostic output differ between wavedash dev and wavedash build push for the same misconfigurationdev reports a /private/var/... absolute path; build push reports ./build/default/.... That asymmetry makes user-reported failures harder to triage.
  3. R2Uploader at line 215 walks the non-canonical path; pre-PR behavior, but worth confirming the new Defold flow doesn't introduce sensitivity to symlinks under upload_dir.

Restoring the canonicalize block (the same one added in a291f10 and removed in 1aa904f) is the simplest fix and keeps the two flows symmetric:

let upload_dir = upload_dir
    .canonicalize()
    .with_context(|| format!("Failed to canonicalize upload_dir: {}", upload_dir.display()))?;

(requires restoring use anyhow::Context;.)

Comment thread src/dev/entrypoint.rs
Comment on lines +166 to +209
#[test]
fn resolves_defold_entrypoint_inside_upload_dir() {
let upload_dir = temp_upload_dir("inside");
let html_path = upload_dir.join("wasm-web/example/index.html");
fs::create_dir_all(html_path.parent().expect("html parent")).expect("create dirs");
fs::write(&html_path, "<html></html>").expect("write html");

let (resolved, relative) =
resolve_defold_entrypoint(&upload_dir, Some("wasm-web/example/index.html"))
.expect("resolve entrypoint");

assert_eq!(resolved, html_path);
assert_eq!(relative, "wasm-web/example/index.html");

fs::remove_dir_all(upload_dir).expect("cleanup");
}

#[test]
fn rejects_defold_entrypoint_escape() {
let upload_dir = temp_upload_dir("escape");
fs::create_dir_all(&upload_dir).expect("create upload dir");

let err = resolve_defold_entrypoint(&upload_dir, Some("../index.html"))
.expect_err("escape should fail");

assert!(err.to_string().contains("relative path inside upload_dir"));

fs::remove_dir_all(upload_dir).expect("cleanup");
}

#[test]
fn rejects_defold_entrypoint_non_html() {
let upload_dir = temp_upload_dir("non-html");
let asset_path = upload_dir.join("wasm-web/example/something.png");
fs::create_dir_all(asset_path.parent().expect("asset parent")).expect("create dirs");
fs::write(&asset_path, "not html").expect("write asset");

let err = resolve_defold_entrypoint(&upload_dir, Some("wasm-web/example/something.png"))
.expect_err("non-html entrypoint should fail");

assert!(err.to_string().contains("HTML file"));

fs::remove_dir_all(upload_dir).expect("cleanup");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test cleanup leaks temp directories if any assertion before fs::remove_dir_all panics.

Each test ends with fs::remove_dir_all(upload_dir).expect("cleanup") — but if expect_err, assert_eq!, or assert! panics earlier (e.g., if resolve_defold_entrypoint's behavior regresses), the cleanup line never executes and the temp directory remains under /tmp/wavedash-cli-entrypoint-* indefinitely. On a CI loop that re-runs the test suite, this accumulates.

This won't bite production users — only test infrastructure — but it makes it harder to diagnose flaky-test situations on a long-lived runner.

tempfile::TempDir (already a common Rust testing crate) handles RAII cleanup on Drop so panics don't leak. If pulling in another dev-dependency is heavier than wanted, a manual Drop guard struct around PathBuf works too. Minor / quality-of-life only.

Comment thread src/init.rs
"\n# Defold HTML5 bundles can contain both wasm-web/ and js-web/ folders.\n# Set `entrypoint` (top-level) to the index.html you want to ship, e.g.:\n# entrypoint = \"wasm-web/<game>/index.html\"\n",
);
toml.push_str(&format!("\n[defold]\nversion = \"{}\"\n", version));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

wavedash init for Defold still produces a TOML where the next step (wavedash dev) errors out — the new comment acknowledges it but the cliff is unchanged.

The comment at lines 256–258 documents the design ("the dev fills in the export they want to ship; wavedash dev / build push errors clearly until they do"), and the strict "no inference" rule in resolve_defold_entrypoint is sound. But the resulting UX:

  1. wavedash init completes successfully and the outro at line 472-475 promises "Next steps: → Run wavedash dev to test locally"
  2. The user runs wavedash dev
  3. It errors with the long "Defold builds need an entrypoint…" message from entrypoint.rs:53-57
  4. The user opens wavedash.toml, finds a #-prefixed hint, edits the file

This is the only engine where the next step the outro promises doesn't work without manual file editing. Two cheap mitigations, in order of effort:

  • Surface a cliclack::log::info line during init telling the user to edit wavedash.toml before running wavedash dev. Costs ~3 lines, fully removes surprise.
  • Prompt during init: add a cliclack::input("Defold HTML5 export path") with a placeholder like wasm-web/<game>/index.html and emit the line uncommented. Costs ~10 lines, lets wavedash dev work end-to-end after init.

Either is fine — the goal is just to make the cliff visible before the user hits it. Acknowledged in the previous review pass; flagging once more since the latest commit ("edge cases") had room to address it.

Add init prompt for Defold entrypoint →

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished @cloud9c's task in 6m 38s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort against commit d852111 ("edge cases 2"). The PR has converged — this commit specifically addressed three of the medium-severity items from the prior pass with no new regressions. Posted 1 inline comment for the smallest remaining UX item.

  • Gather diff and PR context (git diff origin/main...HEAD, git diff 64048d1..HEAD)
  • Run independent finder angles (line-by-line, removed-behavior, cross-flow tracer, language pitfalls)
  • Verify candidate findings
  • Sweep for gaps
  • Post inline review comments

What changed since the last review

Prior finding Status
#1 — Defold entrypoint validation ran after scan_directory in build push Fixedresolve_defold_entrypoint is now hoisted above FileStaging::prepare and scan_directory in both builds.rs:153-159 and dev/mod.rs:106-112, so a missing/invalid entrypoint errors fast.
#3 — Slash-normalization drift between top-level entrypoint and entrypointParams.htmlPath Fixedentrypoint now reuses the normalized html_relative_path returned by resolve_defold_entrypoint (builds.rs:171, dev/mod.rs:115), so backslash inputs in wavedash.toml produce consistent forward-slash output in both fields.
#4wavedash init for Defold produced a TOML where wavedash dev errored immediately Partially fixed — new cliclack::log::info at init.rs:471-475 warns the user to set entrypoint first. The outro still says "Next steps → Run wavedash dev" though; flagged inline.
(new) Test coverage for backslash normalization Addedentrypoint.rs:183-198 verifies wasm-web\example\index.htmlwasm-web/example/index.html end-to-end.

New inline finding

Low severity — only one, on the smallest of the remaining items.

  • init.rs:471-475 — The new info line closes most of the Defold init UX cliff, but the unchanged outro at line 478 still says "Next steps: → Run wavedash dev". A user who skims will follow the visually-dominant outro and hit the resolve_defold_entrypoint error. Two cheap follow-ups noted (re-order outro steps for Defold, or prompt during init).

Carried over but not re-posted

To avoid inline duplicates, these stand:

  • upload_dir not canonicalized in handle_build_push (builds.rs:142) — moot for safety since resolve_defold_entrypoint now does internal canonicalize-containment, but the two flows still emit different textual paths in error messages for the same misconfiguration.
  • Test cleanup leaks temp dirs on panic (entrypoint.rs:166-226) — CI-only impact; tempfile::TempDir would handle this via RAII.
  • Hardcoded "DEFOLD" in error message strings at builds.rs:183 and dev/mod.rs:142 — cosmetic. The Godot/Unity arm at dev/mod.rs:134 uses engine_label; only Defold's version-required error hardcodes the label.

New minor observation (not posted inline)

  • Both builds.rs:181 and dev/mod.rs:140 use .expect("defold entrypoint resolved") to unwrap defold_entrypoint inside an arm already guarded by Some(EngineKind::Defold). Defensive and correct (the outer guard ensures defold_entrypoint is Some), but a mild code smell — the double-match across two locations means future maintainers must keep the guards in sync. Could be hoisted into a single conditional block per function, but the current form is clear enough. Not a blocker.

Missing test coverage worth considering

  • Absolute path rejection (entrypoint = "/etc/passwd").
  • Symlink escape (symlinked file inside upload_dir resolving outside it — the canonicalize-containment check should catch this; would be nice to assert).
  • .. in nested path (foo/../../bar/index.html).

Verdict

The PR has reached a good landing spot. High-severity findings from prior passes are resolved or mooted. The pivot to explicit entrypoint for Defold (commit c4ec363) plus the path-escape defenses (64048d1) plus the validation-ordering / slash-normalization / UX-info fixes (d852111) together form a coherent and defensible design. The remaining items are quality-of-life, not blockers.
Branch: defold-first-class

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d852111. Configure here.

Comment thread src/dev/entrypoint.rs
Comment thread src/init.rs Outdated
Comment on lines +471 to +475
if detected.engine_type == EngineType::Defold {
cliclack::log::info(
"Set `entrypoint` in wavedash.toml to your Defold HTML export before running `wavedash dev`.",
)?;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Closes most of the UX cliff for Defold, but the outro that follows still contradicts the info.

The new info (lines 472–474) warns the user to set entrypoint before running wavedash dev — good. But the very next call (line 478, unchanged in this PR) is:

cliclack::outro(format!(
    "Created wavedash.toml! Next steps:\n  → Run `wavedash dev` to test locally\n  ..."
));

So a Defold user sees a small info line, then an outro promising "Next steps: → Run wavedash dev" — which the info has just told them won't work yet. The outro is the visually dominant message, and a user who skims will act on its promise and hit the resolve_defold_entrypoint error.

Two cheap follow-ups, in order of effort (not blockers):

  1. Conditionalize the outro text for Defold so Edit entrypoint in wavedash.toml is step 1 and Run wavedash dev is step 2 — keeps the next-steps list honest.
  2. Prompt during init for the Defold entrypoint (placeholder wasm-web/<game>/index.html) and emit an uncommented entrypoint = … line; then the outro's claim is actually true on a clean run.

The current state is a real improvement over the prior commit; just calling out that the surface area is now small but still present.

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished @cloud9c's task in 6m 54s —— View job


Code Review — Extra-high effort (recall mode)

Reviewed at extra-high effort against commit fb68137 ("edge cases 3"). This commit is a clean, focused improvement on top of the converged PR — no new findings worth posting inline.

  • Gather diff and PR context (git diff origin/main...HEAD, git diff d852111..HEAD)
  • Run independent finder angles (line-by-line, removed-behavior, cross-flow tracer, language pitfalls)
  • Verify candidate findings
  • Sweep for gaps
  • No new inline comments — see explanation below

What changed since the last review

This commit makes two changes:

  1. resolve_defold_entrypoint now rebuilds relative_path from Component::Normal segments (entrypoint.rs:74-82) instead of just slash-replacing the user's raw string. This strictly improves behavior: ./wasm-web/./example/index.html now normalizes to wasm-web/example/index.html end-to-end (server gets the clean path; not ./wasm-web/./...). Verified by the new normalizes_defold_entrypoint_dot_segments test at entrypoint.rs:209-224. Path-escape and extension defenses from 64048d1 are preserved.

  2. Defold init outro now leads with the entrypoint step (init.rs:472-480) instead of an out-of-band cliclack::log::info line that the previous outro contradicted. Closes the prior-pass concern that the outro's "Next steps → Run wavedash dev" promise was visually dominant over the warning.

Why no new inline findings

I went through the diff at five angles (line-by-line, removed-behavior, cross-file tracer, Rust pitfalls, edge-case sweep) and verified each candidate:

  • Component-based path normalization is robust. The line 60-67 check rejects is_absolute / ParentDir / RootDir / Prefix, so the rebuild at 74-82 only ever sees Normal and CurDir (filtered out). Tested escape inputs (../, /abs/, C:\ after backslash-replace on Unix becoming Normal("C:")) all behave correctly.
  • fetch_entrypoint_params body is built conditionally. htmlPath is only inserted into the JSON when Some, so the Godot/Unity request shape is preserved (per @calvinstudebaker's prior pushback).
  • expect("defold entrypoint resolved") at builds.rs:181 and dev/mod.rs:140 is guarded by the outer Some(EngineKind::Defold) arm — sound, not a footgun.
  • Tests cover inside-upload-dir resolution, backslash normalization, dot-segment normalization, escape rejection, non-HTML rejection. Coverage is good.

Carried over from prior passes (not re-posted to avoid duplication)

  • upload_dir not canonicalized in handle_build_push (builds.rs:142) — dev/mod.rs:101 canonicalizes; builds.rs doesn't. Moot for safety because resolve_defold_entrypoint re-canonicalizes both sides internally for the containment check. Remaining concern is purely cosmetic: the two flows emit different textual paths in error messages for the same misconfiguration.
  • Test cleanup leaks temp dirs on panic (entrypoint.rs:166-252) — tempfile::TempDir would handle this via RAII, but the impact is CI-only.
  • Hardcoded "DEFOLD" in version-required error strings at builds.rs:183 and dev/mod.rs:142 — cosmetic; the rest of the file uses as_label(). Note this code path is effectively unreachable since DefoldSection.version is a required field, so toml parsing fails before reaching it.

Verdict

The PR has fully converged. The latest commit was a small, correct, well-tested polish round. I would not block on any remaining item. The Defold pivot from heuristics to explicit entrypoint (c4ec363) followed by the path-escape defenses (64048d1), validation-ordering / slash-normalization / UX fixes (d852111), and now the path-normalization completeness + outro UX cleanup (fb68137) form a coherent and defensible design that's safe to ship.


• • Branch: defold-first-class

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.

2 participants