Problem
The Playground Execute sandbox now supports the three root package.json#exports forms reported in #670, but several adjacent package-target decisions still disagree with Node's CommonJS package algorithm:
- an array falls through when its first target is valid but the file is missing;
- an inactive nested condition stops an outer condition map instead of letting it continue;
- bare or escaping export targets are treated as pack keys;
- a manifest that mixes subpath and condition keys is accepted;
- a package installed under an npm alias cannot require itself by its manifest name.
Each difference changes which browser code executes or turns a valid installed dependency into an unavailable-package error.
Evidence
Exact Node 22 controls and the current public sandbox produced:
| Case |
Node |
Playground sandbox |
["./missing.cjs", "./available.cjs"] |
MODULE_NOT_FOUND for the selected missing target |
Loads available.cjs |
["available.cjs", "./available.cjs"] |
Skips the invalid target and loads the second |
Loads the second |
{ "require": { "development": "./dev.cjs" }, "default": "./available.cjs" } |
Loads the outer default |
Reports unavailable |
Export target "available.cjs" |
ERR_INVALID_PACKAGE_TARGET |
Loads the file |
Mixed { ".": "./root.cjs", "require": "./other.cjs" } |
ERR_INVALID_PACKAGE_CONFIG |
Loads a branch |
Package mounted as alias, manifest name actual, internal require("actual/sub") |
Resolves the same package |
Searches for another mounted package and fails |
The alias path is live. installPlaygroundDependencies mounts an npm alias under the requested dependency name while retaining the downloaded manifest's actual name.
Node's package contract also requires export targets to begin with ./ and rejects traversal, node_modules segments, and encoded separator escapes after that prefix. The current resolver removes an optional ./ and concatenates the result with the pack package key.
Consequence surface
The shared reducer owns root, exact subpath, wildcard subpath, nested condition, array, and null targets. The target validator must therefore cover every one of those shapes.
Behavior to preserve:
This issue does not add ESM evaluation, Node builtins, package imports, filesystem access, or every Node resolution error code.
Approach
Replace the flattened string-candidate reducer with a package-target result that distinguishes:
- resolved target;
- blocked target;
- unresolved conditional branch;
- invalid target.
Validate a string target before pack lookup. Arrays may skip invalid or unresolved entries, but once a valid target is selected, a missing file is a missing module rather than a reason to try another target. Conditional objects continue in manifest order when an active nested branch is unresolved.
Reject manifests that mix subpath and condition keys. For a bare request from inside a mounted package, compare the requested package name with that mount's parsed manifest name; matching requests resolve through the current mount so npm aliases retain self-reference.
Acceptance and verification
pnpm --filter @ttsc/playground build
pnpm --filter @ttsc/test-playground start -- --include=create_sandbox_require
pnpm --filter @ttsc/test-playground start
Coordination
#670 is resolved in public releases beginning with 0.19.0. #820 is the direct manifest-boundary predecessor. This issue covers verified cases outside those issues' completed acceptance matrices and should not reopen either one.
Problem
The Playground Execute sandbox now supports the three root
package.json#exportsforms reported in #670, but several adjacent package-target decisions still disagree with Node's CommonJS package algorithm:Each difference changes which browser code executes or turns a valid installed dependency into an unavailable-package error.
Evidence
Exact Node 22 controls and the current public sandbox produced:
["./missing.cjs", "./available.cjs"]MODULE_NOT_FOUNDfor the selected missing targetavailable.cjs["available.cjs", "./available.cjs"]{ "require": { "development": "./dev.cjs" }, "default": "./available.cjs" }"available.cjs"ERR_INVALID_PACKAGE_TARGET{ ".": "./root.cjs", "require": "./other.cjs" }ERR_INVALID_PACKAGE_CONFIGalias, manifest nameactual, internalrequire("actual/sub")The alias path is live.
installPlaygroundDependenciesmounts an npm alias under the requested dependency name while retaining the downloaded manifest's actualname.Node's package contract also requires export targets to begin with
./and rejects traversal,node_modulessegments, and encoded separator escapes after that prefix. The current resolver removes an optional./and concatenates the result with the pack package key.Consequence surface
The shared reducer owns root, exact subpath, wildcard subpath, nested condition, array, and
nulltargets. The target validator must therefore cover every one of those shapes.Behavior to preserve:
requireanddefault, withnodeandimportinactive in the browser CommonJS evaluator;mainand index fallback for packages withoutexports;This issue does not add ESM evaluation, Node builtins, package
imports, filesystem access, or every Node resolution error code.Approach
Replace the flattened string-candidate reducer with a package-target result that distinguishes:
Validate a string target before pack lookup. Arrays may skip invalid or unresolved entries, but once a valid target is selected, a missing file is a missing module rather than a reason to try another target. Conditional objects continue in manifest order when an active nested branch is unresolved.
Reject manifests that mix subpath and condition keys. For a bare request from inside a mounted package, compare the requested package name with that mount's parsed manifest
name; matching requests resolve through the current mount so npm aliases retain self-reference.Acceptance and verification
node_modules, encoded slash, and encoded backslash targets fail.Coordination
#670 is resolved in public releases beginning with 0.19.0. #820 is the direct manifest-boundary predecessor. This issue covers verified cases outside those issues' completed acceptance matrices and should not reopen either one.