Skip to content

Resolve MRT-qualified visual assets in the manifest editor - #199

Open
Chiara Mooney (chiaramooney) wants to merge 9 commits into
mainfrom
chiaramooney-redesigned-robot
Open

Resolve MRT-qualified visual assets in the manifest editor#199
Chiara Mooney (chiaramooney) wants to merge 9 commits into
mainfrom
chiaramooney-redesigned-robot

Conversation

@chiaramooney

Copy link
Copy Markdown
Collaborator

Fixes #191.

Problem

The AppxManifest custom editor warned Image not found in package directory for visual assets that are actually present. MRT projects reference the unqualified asset name in the manifest (Assets\Square150x150Logo.png) while only qualifier-suffixed files ship on disk (Square150x150Logo.scale-200.png). The checkImagePath handler did a literal fs.existsSync chain, found nothing, and posted status: 'notFound'.

Fix

Ported the WinApp CLI's MrtAssetHelper qualifier grammar to TypeScript and made image-path resolution MRT-aware:

  • If the literal file exists, use it (unchanged behavior).
  • Otherwise enumerate sibling files in the asset folder, match them as qualifier variants of the requested base name, and pick the best candidate (preferring scale-*, then targetsize-*, then the lowest-ordered qualifier).
  • A variant match reports found with an mrtNote explaining which file satisfied the reference, so the user still sees that the literal path isn't on disk.
  • All probing is contained to the manifest folder and the workspace roots via isPathWithin, so a crafted manifest path can't drive filesystem reads outside the project.

Everything lives in a single new module, src/manifest-editor/image-utils.ts, which also absorbed the pre-existing getImageDimensions and aspect-ratio helpers that were previously inline in the provider.

Also in this branch:

  • Non-exact resolutions outside the manifest folder now render a preview via webview.asWebviewUri instead of going blank.
  • getImageDimensions hardening found while writing its tests: the fd is now always closed, truncated PNG headers return null instead of 0×0, and a zero-length JPEG segment no longer causes an infinite loop.
  • The E2E harness now launches with a dedicated --user-data-dir and dismisses VS Code's first-run onboarding wizard. Without this, a second VS Code instance handed its command line to the already-running one and exited 0, so app.firstWindow() failed — the E2E suite could not launch at all on a developer machine with VS Code open.

Test coverage

  • src/test/image-utils.test.ts — qualifier grammar, variant matching, candidate ranking, path containment, image header parsing, and end-to-end path resolution including the out-of-workspace containment case.
  • src/test/manifest-validator.test.ts — qualifier-token cases for the validator.
  • src/test/e2e/mrt-asset-resolution.spec.ts — 7 tests driving the real editor against a fixture whose assets exist only as .scale-200 variants, including the logo preview falling back to the resolved variant.

1135/1135 unit tests and the E2E suite pass; eslint is clean on all changed files.

Review

This branch went through three rounds of the repo's pr-review skill. Deliberate decisions that reviewers flagged and I kept:

  • manifest-validator.ts keeps its own narrow qualifier list rather than delegating to isQualifierToken — the language-qualifier regex accepts any 2–3 letter token, so .bmp/.svg/.gif would be misread as qualifiers.
  • isMrtVariantName compares only the first dot-separated segment, so dotted base names never match their variants. This is exact parity with the C# CLI and is pinned by a test.

An MSIX manifest references the unqualified asset name (Assets\Logo.png)
while the files that ship are qualifier-suffixed (Logo.scale-200.png,
Logo.targetsize-24_altform-unplated.png). The manifest editor checked the
literal path only, so every visual asset field on a standard WinUI 3 project
warned 'Image not found in package directory' and the logo preview stayed
blank.

Ports the WinApp CLI's MrtAssetHelper qualifier grammar and variant matching
to TypeScript and uses it for the existence check: the literal file wins,
then qualifier-suffixed siblings, then qualifier-folder layouts. Only a
reference with no literal file and no variants still warns. A resolved
variant shows an informational 'Resolved via MRT to ...' note and drives the
thumbnail preview. Aspect-ratio checking now skips targetsize-N variants,
which are square by definition regardless of the field.

Fixes #191

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
- Restrict MRT variant enumeration to the package and workspace roots so a
  manifest value can't drive readdirSync of arbitrary local or UNC directories.
- Extract checkImagePath's branch selection into image-path-resolver.ts and
  cover it with unit tests; the logic previously lived inline in the webview
  message switch with no direct coverage.
- Only offer 'Copy to Assets folder?' for a literal out-of-package file, so the
  prompt can no longer copy a variant and rewrite the manifest to a qualified
  name the user never typed.
- Restore the '..' containment guard on the preview path.
- Clear a stale image-path status synchronously on edit instead of leaving a
  resolved-MRT note under a path the user just retyped.
- Pin dotted-base-name parity with the CLI, add compound qualifier-folder and
  sibling-vs-folder precedence tests, and note why manifest-validator.ts keeps
  its narrower qualifier list.
- Delete only this spec's files in the shared E2E workspace.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
Fold mrt-asset-helper.ts, image-path-resolver.ts and asset-dimensions.ts into a
single image-utils.ts, and move getImageDimensions there from the editor provider
so all image handling lives in one module.

Drop the MrtResolution.variants field, which was built in production but only ever
read by tests, and collapse the three-function variant ranking into one comparator
with identical ordering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
- mrtNote now reports the resolved relative path instead of just the file
  name, so qualifier-folder hits read differently from the typed value
- Cover the widened manifest-validator qualifier list with accepted and
  near-miss spellings
- Assert the application logo preview falls back to the resolved variant
- Fix E2E launch: give the test instance its own user-data dir so it does
  not hand off to a running VS Code, and dismiss the first-run dialog

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
- Contain the workspace fallback in resolveManifestImagePath so a literal
  file resolved outside every workspace root is no longer reported as found.
- Harden getImageDimensions: always close the fd, return null for truncated
  PNG headers instead of 0x0, and stop the JPEG marker walk on a zero-length
  segment instead of looping forever.
- Preview non-exact resolutions that live outside the manifest folder via
  webview.asWebviewUri instead of leaving the preview blank.
- Add unit coverage for isPathWithin, getImageDimensions, and the workspace
  containment case.
- Clean up the E2E temp user-data dir in global teardown.
- Correct the E2E README test counts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

VSIX Build

Artifact Baseline Current Delta
VS Code Extension (VSIX) 27.88 MB 27.88 MB 📈 +1.4 KB (+0.00%)

Updated 2026-08-26 21:31:00 UTC · commit bbf835e · workflow run

Replace the multi-key candidate ranking with a single-pass pick: prefer the
scale-200 variant when one exists, otherwise the first match alphabetically.
Drop the now-unused getVariantScale helper.

Cut over-long explanatory comments down to two lines.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
The fallback re-resolved a leading-".." reference against each workspace root,
which by construction lands outside that root, so the containment guard always
rejected it. Every reference it was meant to serve is already handled by the
in-workspace branch above. Also drops an isAbsolute check that a leading ".."
already rules out.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
A reference resolving to a workspace folder itself is a directory, so
resolveMrtAsset always returns null for it and the branch that reads
inWorkspace can never fire. Containment alone is sufficient.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
Resolving through an MRT variant is normal authoring, so the editor now just
stops warning rather than explaining itself. The preview still repoints at the
resolved variant and its caption still shows that file name.

Removes the note text, the .validation-msg.info style it needed, and the
MrtResolution.relativePath field that existed only to build it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 607fbdd4-8e6c-4c50-a5d7-2c9d0556fb50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Visual asset fields warn "Image not found in package directory" for MRT-qualified assets (.scale-200, .targetsize-24_altform-unplated, etc.)

1 participant