88 * `Symbol.for` rather than a module-local symbol: an extension may resolve a
99 * duplicated copy of the CLI from its own node_modules, and the running CLI
1010 * still has to recognize definitions minted by that copy.
11+ *
12+ * Assigned as a plain enumerable property so that `{ ...definition }` keeps the
13+ * marker; symbols stay invisible to Object.keys/for..in/JSON either way.
1114 */
1215export const HOOK_DEFINITION_MARKER = Symbol . for (
1316 "nativescript:cli:hookDefinition" ,
@@ -22,15 +25,20 @@ export type HookMiddleware = (
2225 next : ( ...args : any [ ] ) => any ,
2326) => any ;
2427
25- export interface IHookContext {
28+ export interface HookContext < TPayload = any > {
2629 /**
2730 * The payload of the operation being hooked. Its shape depends on the hook
2831 * point, and it is the caller's own object: mutating it is a supported
29- * channel for influencing the operation.
32+ * channel for influencing the operation. Hook points fired by command
33+ * dispatch carry no payload at all, hence `undefined`.
3034 */
31- payload : any ;
35+ payload : TPayload | undefined ;
3236
33- /** Registers a middleware around the method this hook point decorates. */
37+ /**
38+ * Registers a middleware around the method this hook point decorates.
39+ * Available only to before-hooks of the hook points that fold middlewares
40+ * around a method; elsewhere it throws rather than dropping the middleware.
41+ */
3442 wrap ( middleware : HookMiddleware ) : void ;
3543
3644 /**
@@ -40,52 +48,173 @@ export interface IHookContext {
4048 abort ( message : string , opts ?: { asWarning ?: boolean } ) : never ;
4149}
4250
43- export type HookHandler = ( ctx : IHookContext ) => void | Promise < void > ;
51+ export type HookHandler < TPayload = any > = (
52+ ctx : HookContext < TPayload > ,
53+ ) => void | Promise < void > ;
54+
55+ /** The object bag accepted by `defineHook`. */
56+ export interface HookDefinitionInput < TPayload = any > {
57+ /** Hook point, in the hyphen convention: `before-prepare`, `after-watch`. */
58+ name : string ;
59+ run : HookHandler < TPayload > ;
60+ }
4461
45- export interface IHookDefinition {
62+ export interface HookDefinition < TPayload = any > {
4663 /** Hook point, in the hyphen convention: `before-prepare`, `after-watch`. */
4764 readonly name : string ;
48- readonly handler : HookHandler ;
65+ readonly run : HookHandler < TPayload > ;
4966}
5067
51- export interface IHookInvocation {
52- context : IHookContext ;
68+ export interface HookInvocation < TPayload = any > {
69+ context : HookContext < TPayload > ;
5370 /** Populated by `ctx.wrap()` while the handler runs. */
5471 middlewares : HookMiddleware [ ] ;
5572}
5673
57- export function defineHook (
74+ const DEFINITION_FIELDS = [ "name" , "run" ] ;
75+
76+ const ACCEPTED_FORMS =
77+ 'defineHook({ name: "before-prepare", run: (ctx) => {} }) or ' +
78+ 'defineHook("before-prepare", (ctx) => {})' ;
79+
80+ function describeDefinition ( name : any ) : string {
81+ return typeof name === "string" && name . length
82+ ? JSON . stringify ( name )
83+ : "<unnamed>" ;
84+ }
85+
86+ function failToDefine ( message : string ) : never {
87+ throw new Error ( `${ message } Accepted forms: ${ ACCEPTED_FORMS } .` ) ;
88+ }
89+
90+ export function defineHook < TPayload = any > (
91+ definition : HookDefinitionInput < TPayload > ,
92+ ) : HookDefinition < TPayload > ;
93+ export function defineHook < TPayload = any > (
5894 name : string ,
59- handler : HookHandler ,
60- ) : IHookDefinition {
61- const definition : IHookDefinition = { name, handler } ;
62- Object . defineProperty ( definition , HOOK_DEFINITION_MARKER , { value : true } ) ;
95+ run : HookHandler < TPayload > ,
96+ ) : HookDefinition < TPayload > ;
97+ export function defineHook < TPayload = any > (
98+ nameOrDefinition : string | HookDefinitionInput < TPayload > ,
99+ run ?: HookHandler < TPayload > ,
100+ ) : HookDefinition < TPayload > {
101+ const input = normalizeDefinitionInput ( nameOrDefinition , run ) ;
102+ const definition : any = { name : input . name , run : input . run } ;
103+ definition [ HOOK_DEFINITION_MARKER ] = true ;
104+
63105 return definition ;
64106}
65107
66- export function isHookDefinition ( value : any ) : boolean {
108+ function normalizeDefinitionInput < TPayload > (
109+ nameOrDefinition : string | HookDefinitionInput < TPayload > ,
110+ run ?: HookHandler < TPayload > ,
111+ ) : HookDefinitionInput < TPayload > {
112+ if ( typeof nameOrDefinition === "string" ) {
113+ if ( ! nameOrDefinition . length ) {
114+ failToDefine ( "defineHook() requires a non-empty hook point name." ) ;
115+ }
116+
117+ if ( typeof run !== "function" ) {
118+ failToDefine (
119+ `defineHook(${ describeDefinition ( nameOrDefinition ) } ) requires a handler function as its second argument.` ,
120+ ) ;
121+ }
122+
123+ return { name : nameOrDefinition , run } ;
124+ }
125+
126+ if (
127+ ! nameOrDefinition ||
128+ typeof nameOrDefinition !== "object" ||
129+ Array . isArray ( nameOrDefinition )
130+ ) {
131+ failToDefine ( "defineHook() was called with an unsupported argument." ) ;
132+ }
133+
134+ const unknownFields = Object . keys ( nameOrDefinition ) . filter (
135+ ( field ) => DEFINITION_FIELDS . indexOf ( field ) === - 1 ,
136+ ) ;
137+ if ( unknownFields . length ) {
138+ failToDefine (
139+ `defineHook(${ describeDefinition ( nameOrDefinition . name ) } ) received unknown ` +
140+ `field${ unknownFields . length > 1 ? "s" : "" } ` +
141+ `${ unknownFields . map ( ( field ) => JSON . stringify ( field ) ) . join ( ", " ) } . ` +
142+ `Supported fields: ${ DEFINITION_FIELDS . map ( ( field ) => JSON . stringify ( field ) ) . join ( ", " ) } .` ,
143+ ) ;
144+ }
145+
146+ if ( typeof nameOrDefinition . name !== "string" || ! nameOrDefinition . name ) {
147+ failToDefine (
148+ 'defineHook() requires a non-empty "name" naming the hook point.' ,
149+ ) ;
150+ }
151+
152+ if ( typeof nameOrDefinition . run !== "function" ) {
153+ failToDefine (
154+ `defineHook(${ describeDefinition ( nameOrDefinition . name ) } ) requires "run" to be a function.` ,
155+ ) ;
156+ }
157+
158+ return { name : nameOrDefinition . name , run : nameOrDefinition . run } ;
159+ }
160+
161+ export function isHookDefinition < TPayload = any > (
162+ value : any ,
163+ ) : value is HookDefinition < TPayload > {
67164 return (
68165 ! ! value &&
69166 ( typeof value === "object" || typeof value === "function" ) &&
70167 value [ HOOK_DEFINITION_MARKER ] === true &&
71- typeof value . handler === "function"
168+ typeof value . run === "function" &&
169+ typeof value . name === "string"
72170 ) ;
73171}
74172
173+ export interface HookInvocationOptions {
174+ /** The hook point the definition runs at; used in diagnostics. */
175+ hookName : string ;
176+ /**
177+ * Whether the caller folds the collected middlewares around a method. Only
178+ * the `@hook`-decorated before-points do; everywhere else `ctx.wrap()` has
179+ * nothing to wrap and says so instead of silently dropping the middleware.
180+ */
181+ consumesMiddlewares ?: boolean ;
182+ }
183+
75184/**
76185 * Derives the context from the raw hook argument bag: the `hookArgs` wrapper
77186 * when the hook point supplies one, the bag itself for hook points that pass
78187 * their keys at the top level, and nothing when there is no payload.
79188 */
80- export function createHookInvocation ( hookArguments : any ) : IHookInvocation {
189+ export function createHookInvocation < TPayload = any > (
190+ hookArguments : any ,
191+ options : HookInvocationOptions ,
192+ ) : HookInvocation < TPayload > {
193+ const { hookName, consumesMiddlewares } = options ;
81194 const middlewares : HookMiddleware [ ] = [ ] ;
82- const context : IHookContext = {
195+ const context : HookContext < TPayload > = {
83196 payload : derivePayload ( hookArguments ) ,
84197 wrap ( middleware : HookMiddleware ) : void {
198+ if ( ! consumesMiddlewares ) {
199+ throw new Error (
200+ `ctx.wrap() is not available at the "${ hookName } " hook point: nothing folds the middleware around a method there, so it would never run.` ,
201+ ) ;
202+ }
203+
204+ if ( typeof middleware !== "function" ) {
205+ throw new Error (
206+ `ctx.wrap() expects a function at the "${ hookName } " hook point.` ,
207+ ) ;
208+ }
209+
85210 middlewares . push ( middleware ) ;
86211 } ,
87212 abort ( message : string , opts ?: { asWarning ?: boolean } ) : never {
88- const error : any = new Error ( message ) ;
213+ const text =
214+ typeof message === "string" && message . trim ( ) . length
215+ ? message
216+ : `The "${ hookName } " hook aborted without a message.` ;
217+ const error : any = new Error ( text ) ;
89218 if ( opts && opts . asWarning ) {
90219 // The pair the hooks service checks for to downgrade a rejection.
91220 error . stopExecution = false ;
0 commit comments