ReviewOS

stacks/ts-cloud

The whole operations/ tree (site-move, server-rename, drained-sites, plan) is unreachable from the published package

#191
Closed glennmichael123 opened this 2 months ago ·
2 months ago

dist/deploy/site-ports.d.ts is published, but no JavaScript behind it is. The module's own doc comment tells callers to use it, the package's "./*" wildcard export makes the import resolve for TypeScript, and then it throws at runtime.

Reproduction

Both 0.12.4 (installed) and 0.12.7 (fetched from the registry):

$ ls package/dist/deploy/ | grep site-ports
site-ports.d.ts

$ grep -rl "buildHostSitePortsScript" package/dist/
package/dist/deploy/site-ports.d.ts

$ grep -rl "buildHostSitePortsScript" --include="*.js" package/dist/
(nothing)

The symbols are absent from every emitted chunk, not just from a barrel:

$ grep -rohE '\b(occupiedHostPorts|buildHostSitePortsScript|parseHostSiteFragments|allocateSitePorts|parseUpstreamPort)\b' package/dist/ --include="*.js"
(nothing)

And the runtime resolution fails outright:

await import('@stacksjs/ts-cloud/deploy/site-ports')
// Cannot find module '@stacksjs/ts-cloud/deploy/site-ports'

deploy/index.d.ts re-exports ./site-target but not ./site-ports, so the root and /deploy entries do not carry them either.

Why this is worth fixing rather than working around

The affected exports are exactly the ones a co-tenancy-aware tool needs, and the module's own documentation points at them:

Build it with occupiedHostPorts() from ./site-ports, passing the deploying project's own slug as ignoreSlug

ValidateDeploymentOptions.occupiedPorts documents that call as the supported way to make validation co-tenant-aware. Following it does not work.

The failure mode is the bad kind: because the .d.ts is shipped and the "./*" export pattern maps the subpath, tsc accepts the import. Nothing surfaces until the code runs.

What was blocked

stacksjs/stacks#2378 (buddy cloud:sites) reads each box's rpx registry to list what every project on it serves. buildHostSitePortsScript + parseHostSiteFragments are precisely that read, already written and already tested here. Since they are not loadable, that PR carries a duplicate of both, with a comment to delete it once this is fixed.

That duplication is the cost worth avoiding: site-ports.ts deliberately duplicates HOST_SITES_DIR from rpx-gateway.ts and pins the copy with a test so it cannot drift. A copy in another repository gets no such test.

Suggested fix

Re-export ./site-ports from deploy/index.ts so the runtime lands in the bundle and the symbols are reachable from @stacksjs/ts-cloud/deploy, the same way site-target already is.

Worth a check for other files in the same state: a .d.ts with no reachable JS is silent, and the "./*" wildcard means every one of them looks importable.

9 days ago

Retitled: this is much bigger than site-ports, and one part of the original description was imprecise.

Correction first

I said these modules "ship types with no runtime / no JavaScript behind them". The dist is minified:

$ head -c 120 package/dist/index.js
import{$ as nh,$a as qC,$b as qS,$c as gA,A as Kf,Aa as bC,Ab as bS,B as qf,...

so an identifier missing from the JS proves it is not publicly exported, not that the code is absent from the bundle. The consumer-visible consequence is unchanged - the import type-checks and then throws - but the diagnosis is "not exported / not resolvable", and the fix is correspondingly smaller than "the build drops these files".

The real scope: all of #167 is behind this

dist/operations/ holds a complete, documented implementation of the consolidation operations. None of it is reachable.

ModuleExportsWhat it is
operations/site-move20planSiteMove - pause background units, snapshot the tree and its systemd units, transfer, restore, loopback health gate, DNS cutover, drain the source without deleting it
operations/server-rename3planServerRename - provider record, driver state pin, box hostname, inventory record, ordered so a crash leaves a stale pin rather than a dangling name
operations/drained-sites3scan a box for site trees it still holds but no longer runs, so a teardown cannot silently destroy a completed move's rollback
operations/plan5resolvePlan / formatPlan / applyPlan - plan-then-apply, resumability from satisfied() asking reality rather than a checkpoint file, destructive steps gated on typed confirmation, an audit hook, and no stdin anywhere

Evidence:

$ ls package/dist/operations/*.js          # 0.12.7, and 0.12.4
(none - only .d.ts)

$ await import('@stacksjs/ts-cloud/operations/site-move')
Cannot find module '@stacksjs/ts-cloud/operations/site-move'

Across every public entry point - 1067 names between root (996), /deploy (71) and /drivers (71) - not one operations symbol is exported:

unreachable  planSiteMove       unreachable  resolvePlan     unreachable  parseDrainedSites
unreachable  planServerRename   unreachable  applyPlan       unreachable  occupiedHostPorts
unreachable  planDrainedSites   unreachable  formatPlan      unreachable  addSiteToCloudConfig
REACHABLE    siteInstallBase    REACHABLE    resolveSiteKind REACHABLE    sshExec

deploy/site-config-editor (11 exports, the config/cloud.ts editors) is in the same state, which is how I found this: I went looking for it to avoid hand-rolling a config edit.

Why this matters more than a missing export usually does

stacksjs/stacks#2342 asks for buddy cloud:move|rename|destroy with --dry-run printing "the plan ts-cloud produced", resumable runs, typed confirmation for destructive steps, and honest exit codes. operations/plan is that contract, already designed and already documented. With it exported, buddy's commands are thin callers. Without it, every consumer either reimplements a plan runner or hand-rolls SSH - and I closed #190 (where I had wrongly concluded the move primitive did not exist at all) because it was about to send someone rebuilding work that is already written.

Suggested fix

Add an ./operations barrel re-exporting the four modules and expose it the way ./deploy and ./drivers are, plus deploy/site-ports and deploy/site-config-editor from the ./deploy barrel.

It is also worth a sweep for others in the same state. A .d.ts with no reachable runtime is silent, and the package's "./*" wildcard export makes every one of them look importable to tsc.

9 days ago

Fixed by #192 (merged b06d37a).

Root cause was the one build.ts already documents: operations was in neither the build's entrypoint list nor the exports map, so dist/operations/ shipped .d.ts with no JavaScript, and the "./*" wildcard made importing it type-check before it threw. deploy/site-ports and deploy/site-config-editor were never re-exported from the deploy barrel, so they were never bundled at all.

Root barrel goes from 996 to 1054 exports; planSiteMove, planServerRename, resolvePlan, applyPlan, formatPlan, parseDrainedSites, occupiedHostPorts, buildHostSitePortsScript, parseHostSiteFragments, addSiteToCloudConfig and siteInstallBase all go from unreachable to reachable, verified against the built dist.

Worth noting for next time: publint already runs in bun run lint and it caught the missing ./operations file the moment the entry was declared. It could not catch the original bug because nothing declared those subpaths at all - they were only reachable through the "./*" wildcard, which publint has no way to verify.

stacksjs/stacks#2380 deletes the duplicate implementation this forced, and waits on a release.

Sign in to comment on this issue.