Skip to content

feat(hooks): defineHook — typed hook authoring with ctx payload/wrap/fail/skip - #6100

Merged
NathanWalker merged 5 commits into
mainfrom
feat/define-hook
Aug 6, 2026
Merged

feat(hooks): defineHook — typed hook authoring with ctx payload/wrap/fail/skip#6100
NathanWalker merged 5 commits into
mainfrom
feat/define-hook

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Based on main#6099 (the DI foundation) is merged.

PR Checklist

What is the current behavior?

Hooks are plain functions whose shape the CLI infers at runtime: services arrive via parameter-name injection (deprecated, runtime-traced), the payload via a magic hookArgs parameter, middleware by returning a function (implicit and undocumented), and aborting by throwing an error carrying stopExecution/errorAsWarning fields.

What is the new behavior?

A typed, explicit hook-authoring API — fully additive; every existing hook keeps working unchanged.

const { defineHook, inject, DoctorService } = require("nativescript/contracts");

module.exports = defineHook({
	name: "before-prepare",
	run: async (ctx) => {
		const doctorService = inject(DoctorService); // services via inject(), as everywhere else
		ctx.payload.args.push("--offline");          // the operation payload, mutable as before
		ctx.wrap(async (args, next) => next(...args)); // explicit middleware (was: return a function)
		ctx.fail("reason");                          // fails the command (was: throw + stopExecution)
		ctx.skip("reason");                          // warns, command continues (was: abort asWarning)
	},
});
// sugar: defineHook("before-prepare", async (ctx) => { ... })
  • lib/common/define-hook.ts is import-free (loading it can never boot a second CLI runtime) and re-exported from nativescript/contracts; definitions carry a plain-assigned Symbol.for marker — spread-safe ({ ...definition } stays recognized) and recognizable across duplicated CLI copies in an extension tree. isHookDefinition is a type predicate.
  • defineHook<TPayload> / HookContext<TPayload> type the payload; ctx.payload is TPayload | undefined because dispatch-fired hook points carry no payload.
  • Definitions validate at define time: missing/invalid name or run, unknown fields, and array exports all throw naming the definition and both accepted forms — no more deep unattributed failures.
  • Strict name matching: a definition whose name differs from the invoking hook point is skipped with a visible warning (so future manifest routing that honors names is not a behavior change).
  • ctx.wrap() never silently no-ops: at hook points that don't consume middlewares (after-hooks, before-<command> dispatch, before-build-task-args, before-watchAction) it throws with a clear error; the wrappable set (the @hook-decorated before-points) is documented.
  • hooks-service runs definition modules on a dedicated path: no parameter-name parsing, no projectData promotion hack, no deprecation report. Plain-function hooks are untouched. .mjs default exports are recognized; a handler that returns a function gets a warning pointing at ctx.wrap().
  • wrap() middlewares feed the exact channel legacy returned-functions use, so they compose with the @hook decorator chain identically. Per-directory hook results are flattened once so multi-middleware hooks surface correctly (returned arrays of functions were silently dropped before and were never in the documented contract).
  • extending-cli.md now leads with defineHook; plain-function hooks and parameter-name injection remain documented as the transitional and legacy tiers, with the stopExecution contract corrected and the watch hook-point names fixed.

Public type names follow the new-API convention (no I prefix): HookContext, HookDefinition, HookMiddleware. Legacy published I* types are untouched.

Full suite: 115 files, 1739 passed / 9 skipped (main baseline 1713 + 26 branch tests); the yok oracle, public-API test, and compat fixtures are untouched.

Summary by CodeRabbit

  • New Features

    • Added a typed defineHook API for creating reusable CLI hooks.
    • Hooks can access payloads, use dependency injection, wrap middleware, and control execution with skip or fail actions.
    • Added support for object and shorthand hook definitions.
    • Exported hook utilities and types through the public contracts API.
    • Before-hooks can now provide middleware to decorated commands.
  • Documentation

    • Updated watch-hook naming and expanded in-process hook guidance.
    • Documented validation rules, compatibility behavior, and hook execution contracts.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a typed defineHook API, payload and middleware controls, hook execution integration, public exports, updated hook documentation, and comprehensive execution and validation tests.

Changes

Typed hook execution

Layer / File(s) Summary
DefineHook contract and public API
lib/common/define-hook.ts, lib/contracts/index.ts, lib/common/declarations.d.ts, extending-cli.md
Adds typed hook definitions, invocation contexts, payload derivation, middleware registration, failure and skip controls, execution options, public exports, and updated hook documentation.
Middleware-aware hook execution
lib/common/services/hooks-service.ts, lib/common/helpers.ts
Updates hook execution to recognize definitions, inject contexts, consume middleware for decorated commands, return middleware arrays, and preserve legacy function behavior.
End-to-end definition validation
test/define-hook.ts
Tests payloads, injection, middleware wrapping, short-circuiting, skip and fail behavior, export forms, hook-name validation, compatibility, and invalid inputs.
Supporting service stubs and formatting
lib/common/test/unit-tests/stubs.ts, test/stubs.ts
Updates hook service stubs to return empty middleware arrays and applies declaration formatting changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DecoratedCommand
  participant HooksService
  participant HookDefinition
  participant HookInvocation
  DecoratedCommand->>HooksService: executeBeforeHooks with middleware consumption
  HooksService->>HookDefinition: validate and execute hook
  HookDefinition->>HookInvocation: create context and register middleware
  HookInvocation-->>HooksService: return middleware and control result
  HooksService-->>DecoratedCommand: return collected middleware
Loading

Possibly related PRs

Poem

A rabbit defines hooks in a row,
With payloads and middleware flow.
“Skip” warns; “fail” stops the flight,
Tests keep each contract tight.
Hop, hop—the hooks now grow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding a typed defineHook API with context payload, middleware wrapping, failure, and skip handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@edusperoni
edusperoni force-pushed the feat/define-hook branch 5 times, most recently from 514c272 to 077c41b Compare July 30, 2026 02:51
Base automatically changed from feat/di-modernization-phase1 to main August 3, 2026 00:31
@edusperoni
edusperoni force-pushed the feat/define-hook branch 2 times, most recently from e932eec to cd153f4 Compare August 5, 2026 19:22
@edusperoni edusperoni changed the title feat(hooks): defineHook — typed hook authoring with ctx payload/wrap/abort feat(hooks): defineHook — typed hook authoring with ctx payload/wrap/fail/skip Aug 5, 2026
@edusperoni
edusperoni marked this pull request as ready for review August 5, 2026 20:22

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/common/services/hooks-service.ts (1)

129-132: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

executeBeforeHooks promises a HookMiddleware[] but can resolve undefined. The declared contract is non-nullable, while executeHooks returns with no value on the disabled-hooks path. decorateMethod masks this today with its if (newMethods && newMethods.length) guard, so any new caller that trusts the type and calls .length or .filter throws a TypeError when DISABLE_HOOKS is set or --no-hooks is used.

  • lib/common/services/hooks-service.ts#L129-L132: return [] instead of a bare return, and change the executeHooks return type to Promise<any[]>.
  • lib/common/declarations.d.ts#L838-L843: keep Promise<HookMiddleware[]> once the service always resolves an array; no change is needed here after the service fix.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/common/services/hooks-service.ts` around lines 129 - 132, Update
executeHooks in lib/common/services/hooks-service.ts to return an empty array,
rather than undefined, when hooks are disabled or unavailable, and change its
return type to Promise<any[]> so executeBeforeHooks always resolves an array.
Keep lib/common/declarations.d.ts lines 838-843 unchanged with
Promise<HookMiddleware[]>; it requires no direct change because the service fix
satisfies that contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/define-hook.ts`:
- Around line 286-294: Make the deprecation test deterministic by capturing the
existing process.env.NS_DEPRECATIONS value, setting the stage to the default
trace-compatible value for the test, and restoring or deleting it in a finally
block. Update the test containing deprecationReports so restoration occurs even
if reportDeprecation or an assertion throws.

---

Outside diff comments:
In `@lib/common/services/hooks-service.ts`:
- Around line 129-132: Update executeHooks in
lib/common/services/hooks-service.ts to return an empty array, rather than
undefined, when hooks are disabled or unavailable, and change its return type to
Promise<any[]> so executeBeforeHooks always resolves an array. Keep
lib/common/declarations.d.ts lines 838-843 unchanged with
Promise<HookMiddleware[]>; it requires no direct change because the service fix
satisfies that contract.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c965e85-3567-444b-972f-af9f3700b028

📥 Commits

Reviewing files that changed from the base of the PR and between 0549b1c and cd153f4.

📒 Files selected for processing (9)
  • extending-cli.md
  • lib/common/declarations.d.ts
  • lib/common/define-hook.ts
  • lib/common/helpers.ts
  • lib/common/services/hooks-service.ts
  • lib/common/test/unit-tests/stubs.ts
  • lib/contracts/index.ts
  • test/define-hook.ts
  • test/stubs.ts

Comment thread test/define-hook.ts
Comment on lines +286 to +294
const deprecationReports = logger()
.traceOutput.split("\n")
.filter((line) => line.indexOf("hooks.param-name-signature") !== -1);
assert.isTrue(
deprecationReports.some((line) => line.indexOf(legacyPath) !== -1),
);
assert.isFalse(
deprecationReports.some((line) => line.indexOf(definitionPath) !== -1),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This assertion depends on the ambient NS_DEPRECATIONS value.

reportDeprecation selects the output channel from getDeprecationStage(). This test reads traceOutput, which only holds the message in the default stage. If a developer or a CI job exports NS_DEPRECATIONS=warn, the message goes to warnOutput and the assertion at line 289 fails. With NS_DEPRECATIONS=error the call throws instead.

Pin the variable for this test so the channel is deterministic.

💚 Proposed fix: pin the deprecation stage for this test
 	it("keeps a legacy param-name hook on the old path, and never reports a definition hook", async () => {
+		const previousStage = process.env.NS_DEPRECATIONS;
+		delete process.env.NS_DEPRECATIONS;
+		try {
 		const legacyPath = writeHookInDirectory(

Restore the value in a finally block at the end of the test:

		} finally {
			if (previousStage === undefined) {
				delete process.env.NS_DEPRECATIONS;
			} else {
				process.env.NS_DEPRECATIONS = previousStage;
			}
		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/define-hook.ts` around lines 286 - 294, Make the deprecation test
deterministic by capturing the existing process.env.NS_DEPRECATIONS value,
setting the stage to the default trace-compatible value for the test, and
restoring or deleting it in a finally block. Update the test containing
deprecationReports so restoration occurs even if reportDeprecation or an
assertion throws.

Hook authors can now export a definition built with defineHook instead
of a plain function whose shape the CLI has to infer. The handler takes
a context object with the operation payload, an explicit wrap() for
middleware around the hooked method, and abort() for stopping the hook
as either a failure or a warning.

Definitions are marked with Symbol.for("nativescript:cli:hookDefinition")
so a duplicated CLI copy in an extension's dependency tree still
recognizes them. The definition path skips parameter-name resolution,
the projectData promotion hack and the deprecation report; plain
function hooks keep running through the existing path unchanged.

lib/common/define-hook.ts is import-free so a hook can load it without
booting a second runtime, and it is re-exported from
nativescript/contracts.
Yok extends Injector on the base branch, so definitions run in
runInInjectionContext(this.$injector, ...) directly and
inject(Injector) inside a hook returns the facade itself.
…-ops

Drops the `I` prefix from the new hook types, makes `run` the handler
field with `defineHook({ name, run })` canonical and the positional call
kept as sugar, and validates the definition at define time: a missing or
non-string name, a non-function run, and unknown fields all throw naming
the definition and both accepted forms.

The definition marker moves from a non-enumerable defineProperty to a
plain assignment so a spread-derived definition stays recognizable, and
`isHookDefinition` becomes a type predicate.

Behaviors that used to fail quietly now say so:

- `ctx.wrap()` only ever ran at the `@hook`-decorated before-points and
  was dropped everywhere else. Call sites now declare whether they
  consume middlewares, and `wrap()` throws elsewhere instead.
- a definition whose name disagrees with its hook point is skipped with
  a warning rather than run at a point it was not written for.
- `ctx.abort()` with no message produced `Error(undefined)`; it now
  falls back to a message naming the hook point.
- a definition whose run returns a function warns, since the legacy
  returned-middleware convention does not apply to definitions.
- an array export is rejected naming the file, reserving the form for a
  possible multi-definition module later.

`defineHook<TPayload>` / `HookContext<TPayload>` type the payload, as
`TPayload | undefined` because dispatch-fired hooks carry none, and
executeBeforeHooks is typed with the middleware array it already returns.
…ntract

Documents the bag form, define-time validation, the strict name match and
the one-definition-per-file rule; splits ctx into payload/wrap/abort
sections; states that dispatch-fired hook points carry no payload and
lists the hook points where wrap() is honored.

Corrects two long-standing errors: a hook named plainly `watch` never
fires (the points are `before-watch`/`after-watch`), and downgrading a
rejection to a warning needs `errorAsWarning === true` together with a
Boolean `stopExecution`, not `stopExecution: false` alone.
`abort(message, { asWarning: true })` did not abort anything — it warned
and the command carried on, so one verb meant opposite things depending
on a flag. The two outcomes now have a verb each: `ctx.fail(message)`
fails the command, `ctx.skip(message)` warns and lets it continue.

Both still stop the handler by throwing, so both are typed `never` and
the "handler ends here" behavior is unchanged; only the command's fate
differs. A missing message falls back to one naming the hook point and
the method.

`abort` and the `asWarning` option are removed outright rather than
shimmed, as the API is unreleased and never exposed either name.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extending-cli.md`:
- Line 82: Update the hook module description around the CommonJS statement to
make the CommonJS export rule conditional rather than universal. Add a separate
description for .mjs hooks that use an export default, while preserving the
existing hook definition and plain-function behavior.
- Around line 194-205: Update the defineHook handler contract near the hook
rejection behavior to allow synchronous run handlers, stating that the CLI
awaits the handler result rather than requiring every handler to return a
Promise. If the Promise requirement is retained, scope it explicitly to the
legacy plain-function hook API and keep defineHook compatible with the
synchronous examples.

In `@test/define-hook.ts`:
- Around line 125-129: Update the test around hooksService().executeAfterHooks
to store the full argument bag in a payload variable, pass that same variable to
executeAfterHooks, and assert capture.payload is strictly identical to payload.
Replace the nested liveSyncResultInfo identity assertion so shallow-copy
implementations no longer satisfy the test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 97cf36b8-cc31-463a-b97e-b992bd7382a3

📥 Commits

Reviewing files that changed from the base of the PR and between f5e4440 and 26e6639.

📒 Files selected for processing (9)
  • extending-cli.md
  • lib/common/declarations.d.ts
  • lib/common/define-hook.ts
  • lib/common/helpers.ts
  • lib/common/services/hooks-service.ts
  • lib/common/test/unit-tests/stubs.ts
  • lib/contracts/index.ts
  • test/define-hook.ts
  • test/stubs.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • lib/common/helpers.ts
  • lib/contracts/index.ts
  • lib/common/declarations.d.ts
  • test/stubs.ts
  • lib/common/test/unit-tests/stubs.ts
  • lib/common/define-hook.ts
  • lib/common/services/hooks-service.ts

Comment thread extending-cli.md
When your hook is a Node.js script, the CLI executes it in-process. This gives you access to the entire internal state of the CLI and all of its functions.

The CLI assumes that this is a CommonJS module and calls its single exported function.
The CLI assumes that this is a CommonJS module and calls the hook it exports — either a hook definition (see below) or a plain function.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Describe CommonJS hooks conditionally.

Line 82 says that every Node.js hook is a CommonJS module. Line 115 documents .mjs hooks with export default. State the CommonJS export rule conditionally, then document .mjs default exports separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extending-cli.md` at line 82, Update the hook module description around the
CommonJS statement to make the CommonJS export rule conditional rather than
universal. Add a separate description for .mjs hooks that use an export default,
while preserving the existing hook definition and plain-function behavior.

Comment thread extending-cli.md
Comment on lines +194 to +205
The hook can also reject the promise with an instance of Error. The returned error can carry two members that together downgrade the rejection to a warning.

Member | Type | Description
---|---|---
`stopExecution` | Boolean | Set this to `false` to let the CLI continue executing this command.
`errorAsWarning` | Boolean | Set this to treat the returned error as warning. The CLI prints the error.message colored as a warning and continues executing the current command.

If these two members are not set, the CLI prints the returned error colored as fatal error and stops executing the current command.
`errorAsWarning` | Boolean | Must be exactly `true`. The CLI prints the error.message colored as a warning and continues executing the current command.
`stopExecution` | Boolean | Must be present and of type Boolean. It only enables the check — setting it alone, with either value, changes nothing.

**Both** members are required: the CLI continues only when `errorAsWarning === true` *and* `stopExecution` is a Boolean. Otherwise it prints the returned error colored as a fatal error and stops executing the current command.

A plain-function hook can also return a function, which the CLI folds into a middleware chain around the hooked method.

With `defineHook` neither convention is needed, and neither applies: `ctx.fail`/`ctx.skip` replace throwing an error carrying `stopExecution`/`errorAsWarning`, and `ctx.wrap` replaces returning a function. A definition whose `run` returns a function is warned about — the returned function is not used as a middleware.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not require a Promise from every defineHook handler.

Line 193 requires every hook to return a Promise. The new examples use synchronous run handlers. Change the contract to state that the CLI awaits the handler result, or limit the Promise requirement to the legacy API if that is the intended contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extending-cli.md` around lines 194 - 205, Update the defineHook handler
contract near the hook rejection behavior to allow synchronous run handlers,
stating that the CLI awaits the handler result rather than requiring every
handler to return a Promise. If the Promise requirement is retained, scope it
explicitly to the legacy plain-function hook API and keep defineHook compatible
with the synchronous examples.

Comment thread test/define-hook.ts
Comment on lines +125 to +129
const liveSyncResultInfo = { fake: true };
await hooksService().executeAfterHooks("case2", { liveSyncResultInfo });

assert.strictEqual(capture.payload.liveSyncResultInfo, liveSyncResultInfo);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Assert identity for the fallback payload.

The assertion only verifies that liveSyncResultInfo retains its identity. An implementation that shallow-copies the top-level argument still passes, but mutations to ctx.payload then do not affect the CLI object. Store the argument bag and assert capture.payload === payload.

Proposed test update
-		const liveSyncResultInfo = { fake: true };
-		await hooksService().executeAfterHooks("case2", { liveSyncResultInfo });
+		const liveSyncResultInfo = { fake: true };
+		const payload = { liveSyncResultInfo };
+		await hooksService().executeAfterHooks("case2", payload);
 
+		assert.strictEqual(capture.payload, payload);
 		assert.strictEqual(capture.payload.liveSyncResultInfo, liveSyncResultInfo);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const liveSyncResultInfo = { fake: true };
await hooksService().executeAfterHooks("case2", { liveSyncResultInfo });
assert.strictEqual(capture.payload.liveSyncResultInfo, liveSyncResultInfo);
});
const liveSyncResultInfo = { fake: true };
const payload = { liveSyncResultInfo };
await hooksService().executeAfterHooks("case2", payload);
assert.strictEqual(capture.payload, payload);
assert.strictEqual(capture.payload.liveSyncResultInfo, liveSyncResultInfo);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/define-hook.ts` around lines 125 - 129, Update the test around
hooksService().executeAfterHooks to store the full argument bag in a payload
variable, pass that same variable to executeAfterHooks, and assert
capture.payload is strictly identical to payload. Replace the nested
liveSyncResultInfo identity assertion so shallow-copy implementations no longer
satisfy the test.

@NathanWalker
NathanWalker merged commit ef68d67 into main Aug 6, 2026
12 checks passed
@NathanWalker
NathanWalker deleted the feat/define-hook branch August 6, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants