@haltcase/run 1.0.0

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.
Files changed (56) hide show
  1. package/dist/cli/bin.d.ts +2 -0
  2. package/dist/cli/bin.js +8 -0
  3. package/dist/cli/bin.js.map +1 -0
  4. package/dist/cli/handler.d.ts +22 -0
  5. package/dist/cli/handler.js +79 -0
  6. package/dist/cli/handler.js.map +1 -0
  7. package/dist/cli/handler.test.d.ts +1 -0
  8. package/dist/cli/handler.test.js +23 -0
  9. package/dist/cli/handler.test.js.map +1 -0
  10. package/dist/cli/main.d.ts +21 -0
  11. package/dist/cli/main.js +57 -0
  12. package/dist/cli/main.js.map +1 -0
  13. package/dist/cli/parseOptions.d.ts +12 -0
  14. package/dist/cli/parseOptions.js +45 -0
  15. package/dist/cli/parseOptions.js.map +1 -0
  16. package/dist/cli/parseOptions.test.d.ts +1 -0
  17. package/dist/cli/parseOptions.test.js +35 -0
  18. package/dist/cli/parseOptions.test.js.map +1 -0
  19. package/dist/cli/util.d.ts +15 -0
  20. package/dist/cli/util.js +68 -0
  21. package/dist/config.d.ts +16 -0
  22. package/dist/config.js +17 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/tasks/executeTask.d.ts +12 -0
  28. package/dist/tasks/executeTask.js +43 -0
  29. package/dist/tasks/executeTask.js.map +1 -0
  30. package/dist/tasks/guards.d.ts +3 -0
  31. package/dist/tasks/guards.js +3 -0
  32. package/dist/tasks/guards.js.map +1 -0
  33. package/dist/tasks/task.d.ts +7 -0
  34. package/dist/tasks/task.js +39 -0
  35. package/dist/tasks/task.js.map +1 -0
  36. package/dist/tasks/task.test.d.ts +1 -0
  37. package/dist/tasks/task.test.js +25 -0
  38. package/dist/tasks/task.test.js.map +1 -0
  39. package/dist/tasks/types.d.ts +57 -0
  40. package/dist/tasks/types.js +2 -0
  41. package/dist/tasks/types.js.map +1 -0
  42. package/dist/tsconfig.build.tsbuildinfo +1 -0
  43. package/dist/util/loadTaskFile.d.ts +8 -0
  44. package/dist/util/loadTaskFile.js +31 -0
  45. package/dist/util/loadTaskFile.js.map +1 -0
  46. package/dist/util/resolveTaskFile.d.ts +2 -0
  47. package/dist/util/resolveTaskFile.js +18 -0
  48. package/dist/util/resolveTaskFile.js.map +1 -0
  49. package/dist/util/result.d.ts +9 -0
  50. package/dist/util/result.js +2 -0
  51. package/dist/util/result.js.map +1 -0
  52. package/dist/util/types.d.ts +7 -0
  53. package/dist/util/types.js +1 -0
  54. package/license +21 -0
  55. package/package.json +80 -0
  56. package/readme.md +352 -0
package/readme.md ADDED
@@ -0,0 +1,352 @@
1
+ # `@haltcase/run` · [![npm version](https://img.shields.io/npm/v/@haltcase/run?style=flat-square)](https://www.npmjs.com/package/@haltcase/run) [![license](https://img.shields.io/npm/l/@haltcase/run?style=flat-square)](https://www.npmjs.com/package/@haltcase/run) [![@haltcase/run](https://img.shields.io/static/v1?label=style&message=haltcase&color=0ca5ed&style=flat-square)](https://haltcase.dev/style)
2
+
3
+ > Flexible, function-based task runner where command line options are props.
4
+
5
+ ## Features
6
+
7
+ - ✅ Write task files in TypeScript or plain JavaScript
8
+ - ✅ [Execa] built in for shell command execution
9
+ - ✅ Tasks are "just functions" exported from your script files
10
+ - ✅ [Zod] schema support for stricter task execution
11
+
12
+ ## Quick start
13
+
14
+ Install `@haltcase/run`:
15
+
16
+ ```shell
17
+ # pnpm
18
+ pnpm add --save-dev @haltcase/run
19
+
20
+ # npm
21
+ npm install --save-dev @haltcase/run
22
+
23
+ # yarn
24
+ yarn add --dev @haltcase/run
25
+ ```
26
+
27
+ Create a file in a scripts folder at the root of your project and export your
28
+ tasks — they're just functions! [¹](#tasks)
29
+
30
+ ```js
31
+ // scripts/tasks.js
32
+
33
+ export const hello = ({ name }) => {
34
+ console.log(`Hello, ${name}!`);
35
+ };
36
+ ```
37
+
38
+ Now you can execute these functions and pass options from the command line:
39
+
40
+ ```shell
41
+ pnpm hr tasks hello --name World
42
+ # → Hello, World!
43
+ ```
44
+
45
+ ## Task files
46
+
47
+ Task files are simple JavaScript or TypeScript files located (by default) in
48
+ the `scripts` folder within your current working directory. Organize them
49
+ however you like.
50
+
51
+ ```
52
+ .
53
+ └── scripts/
54
+ ├── build.js
55
+ ├── database-tasks.ts
56
+ └── dev.ts
57
+ ```
58
+
59
+ To execute a task within one of these files, pass the name of the file
60
+ and the name of the task. For example:
61
+
62
+ ```shell
63
+ pnpm hr database-tasks seed
64
+ ```
65
+
66
+ > [!NOTE]
67
+ > It is allowed (although confusing) to have multiple task files with the
68
+ > same base name, however you must supply a file extension to make it
69
+ > unambiguous which file you are referring to.
70
+ >
71
+ > ```shell
72
+ > ✖ Found multiple task files with the name 'miscellaneous'
73
+ > Rename the ambiguous files or specify an extension and try again
74
+ > C:\dev\sources\js\skrrt\scripts\miscellaneous.js
75
+ > C:\dev\sources\js\skrrt\scripts\miscellaneous.ts
76
+ > C:\dev\sources\js\skrrt\scripts\miscellaneous.mjs
77
+ > ```
78
+
79
+ > [!TIP]
80
+ > If you want `*.js` task files to use a different module type from your root
81
+ > project, add a `package.json` to your scripts folder that specifies
82
+ > `"type": "module"` or `"type": "commonjs"`.
83
+
84
+ ### CommonJS support
85
+
86
+ While TypeScript or ESM are much more highly recommended, you can also write
87
+ task files in CommonJS. Use the `*.cjs` file extension or create a package.json
88
+ file in your scripts folder with `"type": "commonjs"` and use `exports`.
89
+
90
+ ```js
91
+ // scripts/greetings.cjs
92
+
93
+ exports.hello = ({ name }) => {
94
+ console.log(`Hello, ${name}!`);
95
+ };
96
+ ```
97
+
98
+ ## Tasks
99
+
100
+ While tasks can be as simple as an exported function, they're capable of more.
101
+
102
+ ### TypeScript types
103
+
104
+ So far, all tasks have been simple functions. However, you can get type-safety
105
+ by using the `task` function from `@haltcase/run`:
106
+
107
+ ```ts
108
+ // scripts/hello.ts
109
+
110
+ import type { ParsedOptions } from "@haltcase/run";
111
+ import { task } from "@haltcase/run";
112
+
113
+ interface HelloProps extends ParsedOptions {
114
+ name: string;
115
+ }
116
+
117
+ export const hello = task<HelloProps>(({ name }) => {
118
+ console.log(`Hello, ${name}!`);
119
+ });
120
+ ```
121
+
122
+ ### Asynchronous
123
+
124
+ Tasks can return a Promise:
125
+
126
+ ```js
127
+ export const getUser = async ({ id }) => {
128
+ const userResponse = await fetch(
129
+ `https://fakerapi.it/api/v2/custom?_quantity=1&id=${id}&email=email&website=website`
130
+ );
131
+
132
+ const { data } = await userResponse.json();
133
+
134
+ console.log(data[0]);
135
+ };
136
+ ```
137
+
138
+ ### Positional arguments
139
+
140
+ Aside from named options of the form `--option`, task functions also receive
141
+ positional or unnamed arguments as an array of strings in the `_` property.
142
+
143
+ ```js
144
+ // scripts/greetings.js
145
+
146
+ export const hello = ({ name, _ }) => {
147
+ console.log(`Hello, ${name}! ${_.join(" ")}`);
148
+ };
149
+ ```
150
+
151
+ ```shell
152
+ pnpm hr greetings hello --name World Welcome to the cosmos!
153
+ # → Hello, World! Welcome to the cosmos!
154
+ ```
155
+
156
+ Everything following a `--` option terminator will be treated as positional:
157
+
158
+ ```shell
159
+ pnpm hr greetings hello --name World -- --ThisIsNotAnOption
160
+ # → Hello, World! --ThisIsNotAnOption
161
+ ```
162
+
163
+ ### Shell execution
164
+
165
+ Each task additionally receives a second argument, giving you access to shell
166
+ execution powered by [Execa].
167
+
168
+ ```js
169
+ export const build = async ({ mode }, { $ }) => {
170
+ const { stdout } = await $`vite build --mode ${mode}`;
171
+ console.log(`stdout = ${stdout}`);
172
+ };
173
+ ```
174
+
175
+ See [API](#api) for full API details.
176
+
177
+ ### Task option validation using Zod schemas
178
+
179
+ Using [`task.strict`](#taskstrict), you can supply a [Zod] schema to move validations out of
180
+ your task's logic. If you transform the input, your task function's TypeScript
181
+ types will infer the output type.
182
+
183
+ ## Configuration
184
+
185
+ You can configure `@haltcase/run` using a `haltcase.run.{extension}` file in
186
+ your current working directory.
187
+
188
+ Configuration is loaded using [c12] &mdash; see the documentation there for the
189
+ list of supported configuration locations, file types, and other features.
190
+
191
+ ```ts
192
+ // haltcase.run.ts
193
+ import { HaltcaseRunConfig } from "@haltcase/run";
194
+
195
+ export default {
196
+ taskDirectory: "./scripts",
197
+ quiet: false
198
+ } satisfies HaltcaseRunConfig;
199
+ ```
200
+
201
+ You can also set options in the `haltcase.run` property of your package.json:
202
+
203
+ ```jsonc
204
+ {
205
+ // ...
206
+ "haltcase.run": {
207
+ "taskDirectory": "./scripts",
208
+ "quiet": false
209
+ }
210
+ }
211
+ ```
212
+
213
+ ### TypeScript configuration
214
+
215
+ You will likely want to create a `tsconfig.json` file in your scripts folder,
216
+ for example:
217
+
218
+ ```jsonc
219
+ {
220
+ // if you want to extend your root config
221
+ "extends": "../tsconfig.json",
222
+ "compilerOptions": {
223
+ // if you want to allow non-TypeScript task files
224
+ "allowJs": true,
225
+ "noEmit": true
226
+ },
227
+ "include": [
228
+ // feel free to remove extensions here
229
+ "**/*.ts",
230
+ "**/*.mts",
231
+ "**/*.cts",
232
+ "**/*.js",
233
+ "**/*.mjs",
234
+ "**/*.cjs"
235
+ ],
236
+ "exclude": ["node_modules"]
237
+ }
238
+ ```
239
+
240
+ ## API
241
+
242
+ ### Option parsing
243
+
244
+ Command line option parsing is done with Node's [`util.parseArgs`][nodeparseargs],
245
+ but all options are treated as `string` types. In other words, all options
246
+ expect values, meaning the following usage will throw an error:
247
+
248
+ ```shell
249
+ pnpm hr greetings hello --name
250
+ ```
251
+
252
+ If you want to pass a boolean value for an option, pass `true`/`false` and
253
+ parse it yourself or use [`task.strict`](#taskstrict), for example using the
254
+ Zod schema: `z.string().transform(value => value === "true")`.
255
+
256
+ ### `task`
257
+
258
+ `task` is a very light wrapper around a function that provides improved type inference.
259
+
260
+ ```ts
261
+ task<TOptions>(fn: Task<T>): BrandedTask<T>
262
+ ```
263
+
264
+ Usage:
265
+
266
+ ```ts
267
+ import type { ParsedOptions } from "@haltcase/run";
268
+ import { task } from "@haltcase/run";
269
+
270
+ // without providing a type parameter
271
+ export const defaultOptions = task((options) => {
272
+ console.log(options._); // ok, `_: string[]`
273
+ console.log(options.name); // ok, but `name: unknown` and no hints
274
+ });
275
+
276
+ interface CustomOptions extends ParsedOptions {
277
+ name: string;
278
+ }
279
+
280
+ // providing a type parameter
281
+ export const customOptions = task<CustomOptions>((options) => {
282
+ console.log(options.name); // ok, `name: string`
283
+ });
284
+ ```
285
+
286
+ > [!IMPORTANT]
287
+ > Because all command line arguments are strings, you should only use `string`
288
+ > as the value type for options with this method. If you want to be stricter
289
+ > about value types by validating or transforming them, implement the parsing
290
+ > yourself or use [`task.strict`](#taskstrict) with a Zod schema.
291
+
292
+ ### `Task`
293
+
294
+ The type for a task function, which accepts two arguments: the parsed command
295
+ line options and a [`utilities`](#taskutilities) object providing tools for shell execution.
296
+
297
+ ```ts
298
+ <TOptions = ParsedOptions> = (
299
+ options: TOptions,
300
+ utilities: TaskUtilities
301
+ ) => unknown;
302
+ ```
303
+
304
+ ### `TaskUtilities`
305
+
306
+ Properties:
307
+
308
+ - `$` &ndash; Runa command using Execa's [script mode][execascript].
309
+ - `command` &ndash; Run a command using [Execa] (e.g., shell command or script).
310
+ - `exec` &ndash; Same as `command`, but inherits the parent process' stdio streams by default.
311
+
312
+ ### `task.strict`
313
+
314
+ Provide the shape for a Zod schema as the first argument and a task function as
315
+ the second. The task function will receive the safely parsed and validated
316
+ output of the Zod schema, and its types will be inferred automatically.
317
+
318
+ Note that you supply a plain object for the shape rather than a full Zod schema.
319
+
320
+ ```ts
321
+ task.strict<TShape, TSchema?>(shape: TShape, fn: Task<z.infer<TSchema>>): BrandedTaskStrict<z.infer<TSchema>>
322
+ ```
323
+
324
+ Usage:
325
+
326
+ ```ts
327
+ // scripts/strict-tasks.ts
328
+
329
+ import { task } from "@haltcase/run";
330
+ import { z } from "zod";
331
+
332
+ export const printCharacter = task.strict(
333
+ {
334
+ // positional/unnamed arguments
335
+ _: z.array(z.string()),
336
+ name: z.string(),
337
+ armorClass: z.coerce.number()
338
+ },
339
+ async ({ name, armorClass }) => {
340
+ console.log(`🎲 ${name}\n🛡️ ${armorClass}`);
341
+ }
342
+ );
343
+ ```
344
+
345
+ This also allows you to parse, validate, and safely type the `_` property
346
+ beyond an array of strings.
347
+
348
+ [execa]: https://github.com/sindresorhus/execa#readme
349
+ [execascript]: https://github.com/sindresorhus/execa/blob/main/docs/scripts.md
350
+ [zod]: https://zodjs.netlify.app/
351
+ [nodeparseargs]: https://nodejs.org/api/util.html#utilparseargsconfig
352
+ [c12]: https://github.com/unjs/c12?tab=readme-ov-file#-features