@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.
- package/README.md +77 -281
- package/dist/define.d.ts +68 -4
- package/dist/define.js +121 -51
- package/dist/define.js.map +1 -1
- package/dist/globals/globalMiddleware.d.ts +3 -3
- package/dist/globals/middleware/requireContext.middleware.d.ts +1 -1
- package/dist/globals/middleware/retry.middleware.d.ts +1 -1
- package/dist/globals/middleware/timeout.middleware.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/models/StoreConstants.d.ts +1 -1
- package/dist/tools/getCallerFile.js +16 -6
- package/dist/tools/getCallerFile.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/globalEvents.test.ts +1 -1
- package/src/__tests__/run.anonymous.test.ts +27 -0
- package/src/__tests__/run.middleware.test.ts +71 -0
- package/src/__tests__/tools/getCallerFile.test.ts +20 -5
- package/src/define.ts +145 -57
- package/src/tools/getCallerFile.ts +18 -7
|
@@ -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
|
-
*
|
|
122
|
-
* -
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
241
|
-
*
|
|
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
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
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
|
|
51
|
+
const parts = normalizedPath.split("/");
|
|
52
|
+
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
53
53
|
|
|
54
54
|
let relevantParts: string[];
|
|
55
55
|
|
|
56
|
-
if (
|
|
57
|
-
|
|
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
|
|