Skip to content

17.3.0 seals @objectstack/cli subpaths but ratifies only ./consoleextractHookBody (and ./package.json) have no public entry, and an app's hook-body fidelity harness breaks with no replacement #15325

Description

@hotlong

Found while upgrading objectstack-ai/hotcrm from 17.2.0 to 17.3.0 (hotcrm#1576). Filing rather than working around, per that card.

What changed

@objectstack/cli@17.2.0 published no exports field at all — every deep path was reachable. 17.3.0 introduces one, and it admits exactly two subpaths:

{
  ".":        { "types": "./dist/index.d.ts",         "default": "./dist/index.js" },
  "./console":{ "types": "./dist/utils/console.d.ts", "default": "./dist/utils/console.js" }
}

Measured, same tree, Node v22.22.2:

npm view @objectstack/cli@17.2.0 --json  ->  has exports field: false   (main: dist/index.js)
npm view @objectstack/cli@17.3.0 --json  ->  has exports field: true

The sealing itself is deliberate and I am not asking for it to be reverted — @objectstack/cli's own CHANGELOG.md for 17.3.0 explains it (entry 8c82289), and states the intended remedy for a consumer outside the repo:

Ratify ./console as a public subpath export — resolveConsolePath, hasConsoleDist, createConsoleStaticPlugin and the drift-guard helpers were reachable as a deep dist/ import until #13123 sealed the surface, and cloud's objectos-runtime node server consumes them to mount the Console SPA. The #13123 body names exactly this remedy for an out-of-repo consumer: ratify the subpath as public surface rather than read dist/ paths.

That remedy was applied for cloud's consumer. It was not applied for the other out-of-repo consumer of this package's internals.

What broke

hotcrm/test/helpers/action-sandbox.ts — the harness that runs hook and action bodies through the real QuickJSScriptRunner + hookBodyRunnerFactory/actionBodyRunnerFactory, so that a test sees the same body-only lowering the CLI build ships:

test/helpers/action-sandbox.ts(4,33): error TS2307: Cannot find module
  '@objectstack/cli/dist/utils/extract-hook-body.js' or its corresponding type declarations.

It is not only a type error — the specifier is dead at runtime too:

require.resolve('@objectstack/cli/dist/utils/extract-hook-body.js')  ->  ERR_PACKAGE_PATH_NOT_EXPORTED
require.resolve('@objectstack/cli/package.json')                     ->  ERR_PACKAGE_PATH_NOT_EXPORTED
require.resolve('@objectstack/cli')                                  ->  .../@objectstack/cli/dist/index.js

The file is still shipped in the tarball (dist/utils/extract-hook-body.js, 382 lines, with its .d.ts). Only the door is gone.

Why a local reimplementation is the wrong answer

extractHookBody is what decides whether a hook is still shippable body-only: it peels the function to its statements, rejects the forbidden tokens, infers capabilities, and throws HookBodyExtractionError with kind / freeIdentifiers / nodeOnlyIdentifiers. hotcrm has 15+ source and test sites whose comments are written against that exact refusal behaviour, and the harness's own header states the contract it was built to:

extractHookBody is reached by a deep import because the CLI publishes no export map for it. Both packages are pinned to an exact version in package.json, and if a platform upgrade moves the file the import fails at load — loudly, on every test in this harness — rather than degrading to a lookalike check.

A hand-rolled copy of the extractor is precisely the lookalike that comment exists to prevent: it would pass while diverging from the rule the build actually applies, which is the failure mode #13651 was filed about in the first place.

What I ask for

Ratify a public entry for the hook-body extractor, the same way ./console was ratified — e.g. @objectstack/cli/hook-body exposing extractHookBody, HookBodyExtractionError, HookBodyRefusalKind and ExtractedBody. Any app that wants to assert "my hooks are still metadata-only" needs the platform's own extractor to do it; os lint's new hook-body/not-lowerable rule answers the pass/fail question but does not hand a test the lowered source to run.

Secondary, and cheap: "./package.json": "./package.json" in the exports map. Sealing it breaks the ordinary tooling idiom of reading a dependency's own manifest, and there is no upside to withholding it.

Interim measure in hotcrm

Documented, not silent: the harness now resolves the package root (require.resolve('@objectstack/cli'), which the exports map allows) and reaches the extractor by file URL relative to it, so the platform's own implementation is still what runs. It is a stopgap that points at this issue, and it is exactly the "read dist/ paths" shape the changelog entry above says a consumer should not have to adopt — which is the argument for ratifying the subpath.

Also worth a look while here

os migrate meta --from 17 on a 17.3.0 runtime reports Chain: protocol 17 → 17 (runtime 17.0.0). The runtime 17.0.0 there is the protocol major padded to a semver, but printed next to real package versions it reads as "your runtime is 17.0.0" on a 17.3.0 install.

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions