@bluelibs/runner 3.4.1 → 3.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,7 +57,7 @@ describe("generateCallerIdFromFile", () => {
57
57
  it("should generate a symbol from a path containing src", () => {
58
58
  const filePath =
59
59
  "/Users/theodordiaconu/Projects/runner/src/globals/resources/queue.resource.ts";
60
- const expectedDescription = "globals.resources.queue.resource";
60
+ const expectedDescription = "src.globals.resources.queue.resource";
61
61
  expect(generateCallerIdFromFile(filePath).description).toEqual(
62
62
  expectedDescription
63
63
  );
@@ -83,7 +83,7 @@ describe("generateCallerIdFromFile", () => {
83
83
  it("should handle paths with backslashes", () => {
84
84
  const filePath =
85
85
  "C:\\Users\\theodordiaconu\\Projects\\runner\\src\\globals\\resources\\queue.resource.ts";
86
- const expectedDescription = "globals.resources.queue.resource";
86
+ const expectedDescription = "src.globals.resources.queue.resource";
87
87
  expect(generateCallerIdFromFile(filePath).description).toEqual(
88
88
  expectedDescription
89
89
  );
@@ -92,7 +92,7 @@ describe("generateCallerIdFromFile", () => {
92
92
  it("should handle file names without extensions", () => {
93
93
  const filePath =
94
94
  "/Users/theodordiaconu/Projects/runner/src/globals/resources/queue.resource";
95
- const expectedDescription = "globals.resources.queue.resource";
95
+ const expectedDescription = "src.globals.resources.queue.resource";
96
96
  expect(generateCallerIdFromFile(filePath).description).toEqual(
97
97
  expectedDescription
98
98
  );
@@ -101,7 +101,7 @@ describe("generateCallerIdFromFile", () => {
101
101
  it("should handle file names with multiple dots", () => {
102
102
  const filePath =
103
103
  "/Users/theodordiaconu/Projects/runner/src/globals/resources/queue.resource.test.ts";
104
- const expectedDescription = "globals.resources.queue.resource.test";
104
+ const expectedDescription = "src.globals.resources.queue.resource.test";
105
105
  expect(generateCallerIdFromFile(filePath).description).toEqual(
106
106
  expectedDescription
107
107
  );
@@ -150,7 +150,7 @@ describe("generateCallerIdFromFile", () => {
150
150
 
151
151
  // Test case where 'src' is at the end, making relevantParts empty (triggers line 77 else branch)
152
152
  const result = generateCallerIdFromFile("/some/path/src", "suffix");
153
- expect(result.description).toEqual(".suffix");
153
+ expect(result.description).toEqual(".some.path.src.suffix");
154
154
  });
155
155
 
156
156
  it("should use fallback parts when no src or node_modules found", () => {
@@ -161,4 +161,19 @@ describe("generateCallerIdFromFile", () => {
161
161
  expectedDescription
162
162
  );
163
163
  });
164
+
165
+ it("should handle path equal to process.cwd() (relative root)", () => {
166
+ const expectedDescription = ".suffix";
167
+ expect(
168
+ generateCallerIdFromFile(process.cwd(), "suffix").description
169
+ ).toEqual(expectedDescription);
170
+ });
171
+
172
+ it("should handle path ending with node_modules (no relevant parts)", () => {
173
+ const path = `${process.cwd()}/node_modules`;
174
+ const expectedDescription = ".suffix";
175
+ expect(generateCallerIdFromFile(path, "suffix").description).toEqual(
176
+ expectedDescription
177
+ );
178
+ });
164
179
  });
package/src/define.ts CHANGED
@@ -41,6 +41,23 @@ import { generateCallerIdFromFile, getCallerFile } from "./tools/getCallerFile";
41
41
 
42
42
  // Helper function to get the caller file
43
43
 
44
+ /**
45
+ * Define a task.
46
+ * Generates a strongly-typed task object with id, lifecycle events, dependencies,
47
+ * middleware, and metadata.
48
+ *
49
+ * - If `id` is omitted, an anonymous, file-based id is generated.
50
+ * - Wires lifecycle events: `beforeRun`, `afterRun`, `onError`.
51
+ * - Carries through dependencies, middleware, input schema, and metadata.
52
+ *
53
+ * @typeParam Input - Input type accepted by the task's `run` function.
54
+ * @typeParam Output - Promise type returned by the `run` function.
55
+ * @typeParam Deps - Dependency map type this task requires.
56
+ * @typeParam TOn - Event type or "*" this task listens to.
57
+ * @typeParam TMeta - Arbitrary metadata type carried by the task.
58
+ * @param taskConfig - The task definition config.
59
+ * @returns A branded task definition usable by the runner.
60
+ */
44
61
  export function defineTask<
45
62
  Input = undefined,
46
63
  Output extends Promise<any> = any,
@@ -50,12 +67,6 @@ export function defineTask<
50
67
  >(
51
68
  taskConfig: ITaskDefinition<Input, Output, Deps, TOn, TMeta>
52
69
  ): ITask<Input, Output, Deps, TOn, TMeta> {
53
- /**
54
- * Creates a task definition.
55
- * - Generates an anonymous id based on file path when `id` is omitted
56
- * - Wires lifecycle events: beforeRun, afterRun, onError
57
- * - Carries through dependencies and middleware as declared
58
- */
59
70
  const filePath = getCallerFile();
60
71
  const isAnonymous = !Boolean(taskConfig.id);
61
72
  const id = taskConfig.id || generateCallerIdFromFile(filePath, "task");
@@ -118,10 +129,21 @@ export function defineResource<
118
129
  >
119
130
  ): IResource<TConfig, TValue, TDeps, TPrivate, TMeta> {
120
131
  /**
121
- * Creates a resource definition.
122
- * - Generates anonymous id when omitted (resource or index flavor)
123
- * - Wires lifecycle events: beforeInit, afterInit, onError
124
- * - Exposes `.with(config)` for config‑bound registration
132
+ * Define a resource.
133
+ * Produces a strongly-typed resource with id, lifecycle events, registration hooks,
134
+ * and optional config schema.
135
+ *
136
+ * - If `id` is omitted, an anonymous, file-based id is generated (resource or index flavored).
137
+ * - Wires lifecycle events: `beforeInit`, `afterInit`, `onError`.
138
+ * - Provides `.with(config)` for config-bound registration with optional runtime validation.
139
+ *
140
+ * @typeParam TConfig - Configuration type accepted by the resource.
141
+ * @typeParam TValue - Promise type resolved by the resource `init`.
142
+ * @typeParam TDeps - Dependency map type this resource requires.
143
+ * @typeParam TPrivate - Private context type exposed to middleware during init.
144
+ * @typeParam TMeta - Arbitrary metadata type carried by the resource.
145
+ * @param constConfig - The resource definition config.
146
+ * @returns A branded resource definition usable by the runner.
125
147
  */
126
148
  // The symbolFilePath might already come from defineIndex() for example
127
149
  const filePath: string = constConfig[symbolFilePath] || getCallerFile();
@@ -149,10 +171,14 @@ export function defineResource<
149
171
  try {
150
172
  config = this.configSchema.parse(config);
151
173
  } catch (error) {
152
- throw new ValidationError("Resource config", this.id, error instanceof Error ? error : new Error(String(error)));
174
+ throw new ValidationError(
175
+ "Resource config",
176
+ this.id,
177
+ error instanceof Error ? error : new Error(String(error))
178
+ );
153
179
  }
154
180
  }
155
-
181
+
156
182
  return {
157
183
  [symbolResourceWithConfig]: true,
158
184
  id: this.id,
@@ -237,8 +263,13 @@ export function defineEvent<TPayload = void>(
237
263
  config?: IEventDefinition<TPayload>
238
264
  ): IEvent<TPayload> {
239
265
  /**
240
- * Creates an event definition. Anonymous ids are generated from file path
241
- * when omitted. The returned object is branded for runtime checks.
266
+ * Define an event.
267
+ * Generates a branded event definition with a stable id (anonymous if omitted)
268
+ * and file path metadata for better debugging.
269
+ *
270
+ * @typeParam TPayload - Payload type carried by the event.
271
+ * @param config - Optional event definition (id, etc.).
272
+ * @returns A branded event definition.
242
273
  */
243
274
  const callerFilePath = getCallerFile();
244
275
  const eventConfig = config || {};
@@ -261,20 +292,27 @@ export type MiddlewareEverywhereOptions = {
261
292
  resources?: boolean;
262
293
  };
263
294
 
295
+ /**
296
+ * Define a middleware.
297
+ * Creates a middleware definition with anonymous id generation, `.with(config)`,
298
+ * and `.everywhere()` helpers.
299
+ *
300
+ * - `.with(config)` merges config (optionally validated via `configSchema`).
301
+ * - `.everywhere()` marks the middleware global (optionally scoping to tasks/resources).
302
+ *
303
+ * @typeParam TConfig - Configuration type accepted by the middleware.
304
+ * @typeParam TDependencies - Dependency map type required by the middleware.
305
+ * @param middlewareDef - The middleware definition config.
306
+ * @returns A branded middleware definition usable by the runner.
307
+ */
264
308
  export function defineMiddleware<
265
- TConfig extends Record<string, any>,
266
- TDependencies extends DependencyMapType
309
+ TConfig extends Record<string, any> = any,
310
+ TDependencies extends DependencyMapType = any
267
311
  >(
268
312
  middlewareDef: IMiddlewareDefinition<TConfig, TDependencies>
269
313
  ): IMiddleware<TConfig, TDependencies> {
270
- /**
271
- * Creates a middleware definition with:
272
- * - Anonymous id generation when omitted
273
- * - `.with(config)` to create configured instances
274
- * - `.everywhere()` to mark as global (optionally scoping to tasks/resources)
275
- */
276
314
  const filePath = getCallerFile();
277
- const object = {
315
+ const base = {
278
316
  [symbolFilePath]: filePath,
279
317
  [symbolMiddleware]: true,
280
318
  config: {} as TConfig,
@@ -283,67 +321,112 @@ export function defineMiddleware<
283
321
  dependencies: middlewareDef.dependencies || ({} as TDependencies),
284
322
  } as IMiddleware<TConfig, TDependencies>;
285
323
 
286
- return {
287
- ...object,
288
- with: (config: TConfig) => {
289
- // Validate config with schema if provided (fail fast)
290
- if (object.configSchema) {
291
- try {
292
- config = object.configSchema.parse(config);
293
- } catch (error) {
294
- throw new ValidationError("Middleware config", object.id, error instanceof Error ? error : new Error(String(error)));
324
+ // Wrap an object to ensure we always return chainable helpers
325
+ const wrap = (
326
+ obj: IMiddleware<TConfig, TDependencies>
327
+ ): IMiddleware<TConfig, TDependencies> => {
328
+ return {
329
+ ...obj,
330
+ with: (config: TConfig) => {
331
+ // Validate config with schema if provided (fail fast)
332
+ if (obj.configSchema) {
333
+ try {
334
+ config = obj.configSchema.parse(config);
335
+ } catch (error) {
336
+ throw new ValidationError(
337
+ "Middleware config",
338
+ obj.id,
339
+ error instanceof Error ? error : new Error(String(error))
340
+ );
341
+ }
295
342
  }
296
- }
297
-
298
- return {
299
- ...object,
300
- [symbolMiddlewareConfigured]: true,
301
- config: {
302
- ...object.config,
303
- ...config,
304
- },
305
- };
306
- },
307
- everywhere(options: MiddlewareEverywhereOptions = {}) {
308
- const { tasks = true, resources = true } = options;
309
343
 
310
- return {
311
- ...object,
312
- [symbolMiddlewareEverywhereTasks]: tasks,
313
- [symbolMiddlewareEverywhereResources]: resources,
314
- everywhere() {
315
- throw new MiddlewareAlreadyGlobalError(object.id);
316
- },
317
- };
318
- },
344
+ return wrap({
345
+ ...obj,
346
+ [symbolMiddlewareConfigured]: true,
347
+ config: {
348
+ ...(obj.config as TConfig),
349
+ ...config,
350
+ },
351
+ } as IMiddleware<TConfig, TDependencies>);
352
+ },
353
+ everywhere(options: MiddlewareEverywhereOptions = {}) {
354
+ const { tasks = true, resources = true } = options;
355
+
356
+ // If already global, prevent calling again
357
+ if (
358
+ obj[symbolMiddlewareEverywhereTasks] ||
359
+ obj[symbolMiddlewareEverywhereResources]
360
+ ) {
361
+ throw new MiddlewareAlreadyGlobalError(obj.id);
362
+ }
363
+
364
+ return wrap({
365
+ ...obj,
366
+ [symbolMiddlewareEverywhereTasks]: tasks,
367
+ [symbolMiddlewareEverywhereResources]: resources,
368
+ } as IMiddleware<TConfig, TDependencies>);
369
+ },
370
+ } as IMiddleware<TConfig, TDependencies>;
319
371
  };
372
+
373
+ return wrap(base);
320
374
  }
321
375
 
376
+ /**
377
+ * Type guard: checks if a definition is a Task.
378
+ * @param definition - Any value to test.
379
+ * @returns True when `definition` is a branded Task.
380
+ */
322
381
  export function isTask(definition: any): definition is ITask {
323
382
  return definition && definition[symbolTask];
324
383
  }
325
384
 
385
+ /**
386
+ * Type guard: checks if a definition is a Resource.
387
+ * @param definition - Any value to test.
388
+ * @returns True when `definition` is a branded Resource.
389
+ */
326
390
  export function isResource(definition: any): definition is IResource {
327
391
  return definition && definition[symbolResource];
328
392
  }
329
393
 
394
+ /**
395
+ * Type guard: checks if a definition is a Resource that carries config via `.with()`.
396
+ * @param definition - Any value to test.
397
+ * @returns True when `definition` is a branded ResourceWithConfig.
398
+ */
330
399
  export function isResourceWithConfig(
331
400
  definition: any
332
401
  ): definition is IResourceWithConfig {
333
402
  return definition && definition[symbolResourceWithConfig];
334
403
  }
335
404
 
405
+ /**
406
+ * Type guard: checks if a definition is an Event.
407
+ * @param definition - Any value to test.
408
+ * @returns True when `definition` is a branded Event.
409
+ */
336
410
  export function isEvent(definition: any): definition is IEvent {
337
411
  return definition && definition[symbolEvent];
338
412
  }
339
413
 
414
+ /**
415
+ * Type guard: checks if a definition is a Middleware.
416
+ * @param definition - Any value to test.
417
+ * @returns True when `definition` is a branded Middleware.
418
+ */
340
419
  export function isMiddleware(definition: any): definition is IMiddleware {
341
420
  return definition && definition[symbolMiddleware];
342
421
  }
343
422
 
344
423
  /**
345
424
  * Override helper that preserves the original `id` and returns the same type.
346
- * You can override any property except `id`.
425
+ * You can override any property except `id`. The override is shallow-merged over the base.
426
+ *
427
+ * @param base - The base definition to override.
428
+ * @param patch - Properties to override (except `id`).
429
+ * @returns A definition of the same kind with overrides applied.
347
430
  */
348
431
  export function defineOverride<T extends ITask<any, any, any, any>>(
349
432
  base: T,
@@ -371,9 +454,14 @@ export function defineOverride(
371
454
  }
372
455
 
373
456
  /**
374
- * Creates a tag definition.
457
+ * Create a tag definition.
375
458
  * - `.with(config)` to create configured instances
376
- * - `.extract(tags)` to extract this tag from a list of tags
459
+ * - `.extract(tags)` to extract this tag from a list of tags or a taggable's meta
460
+ *
461
+ * @typeParam TConfig - Configuration type carried by configured tags.
462
+ * @typeParam TEnforceContract - Optional helper type to enforce a contract when tags are used.
463
+ * @param definition - The tag definition (id).
464
+ * @returns A tag object with helpers to configure and extract.
377
465
  */
378
466
  export function defineTag<TConfig = void, TEnforceContract = void>(
379
467
  definition: ITagDefinition<TConfig, TEnforceContract>
@@ -44,18 +44,29 @@ export function generateCallerIdFromFile(
44
44
  suffix: string = "",
45
45
  fallbackParts: number = 4
46
46
  ): symbol {
47
- filePath = filePath.replace(/\\/g, "/"); // Normalize path for consistency.
48
- const parts = filePath.split("/");
49
- const srcIndex = parts.lastIndexOf("src");
50
- const nodeModulesIndex = parts.lastIndexOf("node_modules");
47
+ // Normalize paths for consistency across platforms
48
+ const normalizedPath = filePath.replace(/\\/g, "/");
49
+ const cwdNormalized = process.cwd().replace(/\\/g, "/");
51
50
 
52
- const breakIndex = Math.max(srcIndex, nodeModulesIndex);
51
+ const parts = normalizedPath.split("/");
52
+ const nodeModulesIndex = parts.lastIndexOf("node_modules");
53
53
 
54
54
  let relevantParts: string[];
55
55
 
56
- if (breakIndex !== -1) {
57
- relevantParts = parts.slice(breakIndex + 1);
56
+ if (nodeModulesIndex !== -1) {
57
+ // If inside node_modules, generate id relative to the package path
58
+ relevantParts = parts.slice(nodeModulesIndex + 1);
59
+ } else if (
60
+ normalizedPath === cwdNormalized ||
61
+ normalizedPath.startsWith(cwdNormalized + "/")
62
+ ) {
63
+ // Prefer generating id relative to the workspace root (process.cwd())
64
+ const relativeToCwd = normalizedPath
65
+ .slice(cwdNormalized.length)
66
+ .replace(/^\//, "");
67
+ relevantParts = relativeToCwd.length > 0 ? relativeToCwd.split("/") : [""];
58
68
  } else {
69
+ // Fallback: use the last N parts if path is outside cwd and not in node_modules
59
70
  relevantParts = parts.slice(-fallbackParts);
60
71
  }
61
72