Skip to content

Module mode: /** @internal */ stops an export being a command - #226

Merged
mmkal merged 1 commit into
mainfrom
module-internal-tag
Oct 1, 2026
Merged

mmkal merged 1 commit into
mainfrom
module-internal-tag

Conversation

@mmkal

@mmkal mmkal commented Oct 1, 2026

Copy link
Copy Markdown
Owner

In module mode, tag an export /** @internal */ to keep it out of the CLI - the standard TSDoc tag for "exported, but not public API".

// commands.ts
/** @internal */
export const loadConfig = (options: {path: string}) => readConfigFile(options.path) // not a command

export class Users {
  invite(options: {email: string}) {
    return `invite ${this.normalizeEmail(options.email)}` // mycli users invite --email ...
  }

  /** @internal */
  normalizeEmail(email: string) {
    return email.toLowerCase() // not a command
  }
}

Works on functions, arrow functions, fn()/z.function() exports, classes (hides the whole group) and class methods. On an overloaded function, @internal on any declaration hides the whole command - simpler than TS's per-signature stripInternal semantics, and it fails closed.

Before this, the documented way to hide a helper was to make its signature unparseable (params: NoInfer<{...}>). That still works, as does nesting the function under an exported object; the README lists both as alternatives.

Change / Purpose

Change Purpose
src/module-commands.ts: parseCliJsdoc reads @internal into CliJsdoc.internal One jsdoc parser for every tag module mode understands (@alias, @param, now @internal).
src/module-commands.ts: groupOverloadDeclarations drops groups with an @internal declaration Shared by function and class-method extraction, so one check covers both, including overloads.
src/module-commands.ts: extractModuleClasses and buildLocalProcedures skip @internal classes and fn()/zod exports The two other places an export becomes a command.
test/typebox-module-commands.test.ts, test/fn-module-commands.test.ts Help output, invocation and "unknown command" for each export kind; the overload rule.
README.md Documents @internal (plus the other ways to hide an export), and drops a stale bullet claiming non-command exports fail at startup - they've been skipped since #211.

🤖 Generated with Claude Code

Session: local_bea2c0e0-1e8b-42e4-a063-77aa028a4139 (@internal annotation for module mode)

Tagging an exported function, `fn()`/`z.function()` export, class or class
method `@internal` keeps it out of the CLI. On an overloaded function, a tag on
any declaration hides the whole command.

README: `@internal` replaces the `NoInfer` trick as the documented way to hide
helpers, and drops the stale claim that non-command exports fail at startup
(they've been skipped since #211). Includes Misha's README adjustments listing
the other ways to hide an export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • src/module-commands.ts — parseCliJsdoc now reads @internal into CliJsdoc.internal; groupOverloadDeclarations drops any group with an @internal declaration (shared by function and class-method extraction, including overloads), extractModuleClasses skips tagged classes, and buildLocalProcedures skips tagged fn()/z.function() exports.
  • test/fn-module-commands.test.ts, test/typebox-module-commands.test.ts — cover fn/zod exports, plain functions and arrow consts, a whole class, a class method, the "any overload declaration hides the command" rule, plus invocation and unknown-command behavior.
  • README.md — documents @internal and the two alternatives, and corrects the stale "other exports fail at startup" bullet to match the skip behavior already tested in test/typebox-module-commands.test.ts:167.

I verified the two touched test files pass (71 passed, no type errors) and eslint --max-warnings=0 is clean on the changed files. The @internal filter is applied at the same choke points that otherwise turn an export into a command, and the tests would fail if any of those checks were absent.

ℹ️ @internal is not consulted on re-export statements

Tagging a re-export (export * as group from './group', export {Users} from './users.ts') has no effect — the re-export machinery in buildRouterFromFileModule only reads the child router, which respects the tag only if it lives on the declaration in the child module. This is consistent with the PR's stated scope (functions, classes, class methods), but since the README now presents @internal as the standard way to hide an export, a reader may reasonably expect it to work on any export statement. Non-blocking; worth a one-line note in the docs if you want to set expectations.

Technical details
# Parent-level `@internal` on re-exports is ignored

## Affected sites
- `src/module-commands.ts:276-299` — `extractModuleReexports`/`buildRouterFromFileModule` add namespace, named, and star re-exports without ever calling `parseCliJsdoc` on the re-export statement's jsdoc.
- `README.md:694-708` — presents `@internal` as the general way to hide an export.

## Required outcome
- Either document that `@internal` applies to declarations in the commands module (not to `export ... from` statements), or extend the check to the re-export path.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@mmkal
mmkal merged commit 2f06099 into main Oct 1, 2026
57 checks passed
@mmkal
mmkal deleted the module-internal-tag branch October 1, 2026 07:33
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

This is included in v0.17.1.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant