@idlesummer/tasker 0.1.2 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @idlesummer/tasker
2
2
 
3
- A simple, lightweight task pipeline runner with CLI spinners and formatters for Node.js build tools.
3
+ A simple, lightweight declarative task pipeline runner with CLI spinners and formatters for Node.js build tools.
4
4
 
5
5
  > **⚠️ Learning Project Notice**
6
6
  > Hey! Just so you know, this is a learning project I built to understand task pipelines and build tools better. It works and I use it for my own stuff, but there might be bugs or rough edges. Feel free to use it, but maybe don't bet your production deploy on it just yet. Contributions and bug reports are super welcome though!
@@ -12,6 +12,7 @@ Basically, it's a simple way to run tasks in sequence with nice terminal spinner
12
12
  ## Features
13
13
 
14
14
  - **Task Pipeline** - Run tasks one after another, each task gets the results from the previous ones
15
+ - **Conditional Tasks** - Use boolean expressions to conditionally include/exclude tasks
15
16
  - **CLI Spinners** - Those satisfying loading spinners powered by ora
16
17
  - **Formatters** - Helper functions to make bytes, durations, and file lists look nice
17
18
  - **TypeScript** - Full type safety so you don't shoot yourself in the foot
@@ -81,6 +82,7 @@ The `examples/` folder has working projects you can run:
81
82
 
82
83
  - **[basic-pipeline](./examples/basic-pipeline)** - Super simple example to get started
83
84
  - **[build-tool](./examples/build-tool)** - More realistic build tool with file operations
85
+ - **[conditional-tasks](./examples/conditional-tasks)** - Demonstrates conditional task execution
84
86
  - **[formatters](./examples/formatters)** - Shows off all the formatting utilities
85
87
 
86
88
  Each example is a standalone npm package. To run one:
@@ -125,6 +127,22 @@ const result = await pipeline.run({})
125
127
  // result.duration is total time in ms
126
128
  ```
127
129
 
130
+ **Conditional Tasks:**
131
+
132
+ You can conditionally include tasks using boolean expressions:
133
+
134
+ ```typescript
135
+ const isProduction = process.env.NODE_ENV === 'production'
136
+
137
+ const pipeline = pipe([
138
+ { name: 'Build', run: async () => ({ built: true }) },
139
+ isProduction && { name: 'Minify', run: async () => ({ minified: true }) },
140
+ { name: 'Deploy', run: async () => ({ deployed: true }) },
141
+ ])
142
+ ```
143
+
144
+ When the condition is `false`, the task is automatically skipped. See the [conditional-tasks example](./examples/conditional-tasks) for a complete demo.
145
+
128
146
  ### Formatters
129
147
 
130
148
  Make numbers pretty:
package/dist/index.cjs CHANGED
@@ -83,12 +83,14 @@ function fileList(baseDir, pattern = "**/*", width = 45) {
83
83
  /**
84
84
  * Creates a task pipeline that runs tasks sequentially with spinners
85
85
  * @template TContext - The context type for the pipeline
86
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
86
87
  */
87
88
  function pipe(tasks) {
88
89
  return { run: async (initialContext) => {
89
90
  const startTime = Date.now();
90
91
  let context = initialContext;
91
92
  for (const task of tasks) {
93
+ if (!task) continue;
92
94
  const taskStart = Date.now();
93
95
  const spinner = (0, ora.default)(task.name).start();
94
96
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["prettyBytes","prettyMs","path","pc","duration"],"sources":["../src/format.ts","../src/pipeline.ts"],"sourcesContent":["import { statSync } from 'fs'\r\nimport { join } from 'path'\r\nimport { fdir } from 'fdir'\r\nimport pc from 'picocolors'\r\nimport pm from 'picomatch'\r\nimport prettyBytes from 'pretty-bytes'\r\nimport prettyMs from 'pretty-ms'\r\n\r\n/**\r\n * Formats byte sizes into human-readable strings\r\n * @param size - The size in bytes\r\n * @returns Formatted string (e.g., \"1.23 kB\", \"4.56 MB\")\r\n */\r\nexport const bytes = prettyBytes\r\n\r\n/**\r\n * Formats milliseconds into human-readable durations\r\n * @param ms - The duration in milliseconds\r\n * @returns Formatted string (e.g., \"42ms\", \"1.2s\", \"2m 3.5s\")\r\n */\r\nexport const duration = prettyMs\r\n\r\n/**\r\n * Display a formatted list of files in a directory with their sizes\r\n * @param baseDir - The base directory to search\r\n * @param pattern - Glob pattern to match files\r\n * @param width - Row width for file paths\r\n * @returns Formatted file list with sizes and total\r\n */\r\nexport function fileList(baseDir: string, pattern = '**/*', width = 45) {\r\n // Find all files matching the pattern\r\n // Use windows: true option to handle both / and \\ as path separators\r\n const matcher = pm(pattern, { windows: true })\r\n const files = new fdir()\r\n .withRelativePaths()\r\n .filter(path => matcher(path))\r\n .crawl(baseDir)\r\n .sync()\r\n\r\n const stats = files.map(path => {\r\n const fullPath = join(baseDir, path)\r\n const displayPath = path.replace(/\\\\/g, '/')\r\n return {\r\n path: displayPath,\r\n size: statSync(fullPath).size,\r\n }\r\n })\r\n\r\n const total = stats.reduce((sum, stat) => sum + stat.size, 0)\r\n const lines = stats.map(({ path, size }) => {\r\n const paddedPath = pc.cyan(path.padEnd(width))\r\n const formattedSize = pc.dim(bytes(size))\r\n return ` ${paddedPath} ${formattedSize}`\r\n })\r\n\r\n const footerLabel = `${files.length} files, total:`.padEnd(width)\r\n const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`\r\n return [...lines, footer].join('\\n')\r\n}\r\n","import ora from 'ora'\r\n\r\n/** Base context type - extend this to add your own properties */\r\nexport type Context = Record<string, unknown>\r\n\r\n/**\r\n * A task that runs in a pipeline\r\n * @template TContext - The context type for this task\r\n * @property name - Task name shown in spinner\r\n * @property run - Task implementation - return updates to merge into context\r\n * @property onSuccess - Optional success message (default: task name)\r\n * @property onError - Optional error message (default: task name)\r\n */\r\nexport interface Task<TContext extends Context> {\r\n name: string\r\n run: (ctx: TContext) => Promise<Partial<TContext> | void>\r\n onSuccess?: (ctx: TContext, duration: number) => string\r\n onError?: (error: Error) => string\r\n}\r\n\r\n/**\r\n * Result after running a pipeline\r\n * @template TContext - The context type\r\n * @property context - Final context after all tasks\r\n * @property duration - Total duration in milliseconds\r\n */\r\nexport type PipeResult<TContext extends Context> = {\r\n context: TContext\r\n duration: number\r\n}\r\n\r\n/**\r\n * Creates a task pipeline that runs tasks sequentially with spinners\r\n * @template TContext - The context type for the pipeline\r\n */\r\nexport function pipe<TContext extends Context>(tasks: Task<TContext>[]) {\r\n return {\r\n run: async (initialContext: TContext) => {\r\n const startTime = Date.now()\r\n let context = initialContext\r\n\r\n for (const task of tasks) {\r\n const taskStart = Date.now()\r\n const spinner = ora(task.name).start()\r\n\r\n try {\r\n const updates = await task.run(context) ?? {}\r\n const duration = Date.now() - taskStart\r\n const message = task.onSuccess?.(context, duration) ?? task.name\r\n spinner.succeed(message)\r\n context = { ...context, ...updates }\r\n }\r\n catch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error))\r\n const message = task.onError?.(err) ?? task.name\r\n spinner.fail(message)\r\n throw new Error(`Task \"${task.name}\" failed: ${err.message}`, { cause: err })\r\n }\r\n }\r\n\r\n const duration = Date.now() - startTime\r\n return { context, duration } as PipeResult<TContext>\r\n },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaA,MAAa,QAAQA;;;;;;AAOrB,MAAa,WAAWC;;;;;;;;AASxB,SAAgB,SAAS,SAAiB,UAAU,QAAQ,QAAQ,IAAI;CAGtE,MAAM,iCAAa,SAAS,EAAE,SAAS,MAAM,CAAC;CAC9C,MAAM,QAAQ,IAAI,WAAM,CACrB,mBAAmB,CACnB,QAAO,WAAQ,QAAQC,OAAK,CAAC,CAC7B,MAAM,QAAQ,CACd,MAAM;CAET,MAAM,QAAQ,MAAM,KAAI,WAAQ;EAC9B,MAAM,0BAAgB,SAASA,OAAK;AAEpC,SAAO;GACL,MAFkBA,OAAK,QAAQ,OAAO,IAAI;GAG1C,uBAAe,SAAS,CAAC;GAC1B;GACD;CAEF,MAAM,QAAQ,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,MAAM,EAAE;CAC7D,MAAM,QAAQ,MAAM,KAAK,EAAE,cAAM,WAAW;AAG1C,SAAO,KAFYC,mBAAG,KAAKD,OAAK,OAAO,MAAM,CAAC,CAEvB,IADDC,mBAAG,IAAI,MAAM,KAAK,CAAC;GAEzC;CAEF,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,OAAO,MAAM;CACjE,MAAM,SAAS,KAAKA,mBAAG,IAAI,YAAY,CAAC,IAAIA,mBAAG,IAAI,MAAM,MAAM,CAAC;AAChE,QAAO,CAAC,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK;;;;;;;;;ACtBtC,SAAgB,KAA+B,OAAyB;AACtE,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,2BAAc,KAAK,KAAK,CAAC,OAAO;AAEtC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE;IAC7C,MAAMC,aAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,UAAU,KAAK,YAAY,SAASA,WAAS,IAAI,KAAK;AAC5D,YAAQ,QAAQ,QAAQ;AACxB,cAAU;KAAE,GAAG;KAAS,GAAG;KAAS;YAE/B,OAAO;IACZ,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACrE,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,KAAK;AAC5C,YAAQ,KAAK,QAAQ;AACrB,UAAM,IAAI,MAAM,SAAS,KAAK,KAAK,YAAY,IAAI,WAAW,EAAE,OAAO,KAAK,CAAC;;;EAIjF,MAAMA,aAAW,KAAK,KAAK,GAAG;AAC9B,SAAO;GAAE;GAAS;GAAU;IAE/B"}
1
+ {"version":3,"file":"index.cjs","names":["prettyBytes","prettyMs","path","pc","duration"],"sources":["../src/format.ts","../src/pipeline.ts"],"sourcesContent":["import { statSync } from 'fs'\r\nimport { join } from 'path'\r\nimport { fdir } from 'fdir'\r\nimport pc from 'picocolors'\r\nimport pm from 'picomatch'\r\nimport prettyBytes from 'pretty-bytes'\r\nimport prettyMs from 'pretty-ms'\r\n\r\n/**\r\n * Formats byte sizes into human-readable strings\r\n * @param size - The size in bytes\r\n * @returns Formatted string (e.g., \"1.23 kB\", \"4.56 MB\")\r\n */\r\nexport const bytes = prettyBytes\r\n\r\n/**\r\n * Formats milliseconds into human-readable durations\r\n * @param ms - The duration in milliseconds\r\n * @returns Formatted string (e.g., \"42ms\", \"1.2s\", \"2m 3.5s\")\r\n */\r\nexport const duration = prettyMs\r\n\r\n/**\r\n * Display a formatted list of files in a directory with their sizes\r\n * @param baseDir - The base directory to search\r\n * @param pattern - Glob pattern to match files\r\n * @param width - Row width for file paths\r\n * @returns Formatted file list with sizes and total\r\n */\r\nexport function fileList(baseDir: string, pattern = '**/*', width = 45) {\r\n // Find all files matching the pattern\r\n // Use windows: true option to handle both / and \\ as path separators\r\n const matcher = pm(pattern, { windows: true })\r\n const files = new fdir()\r\n .withRelativePaths()\r\n .filter(path => matcher(path))\r\n .crawl(baseDir)\r\n .sync()\r\n\r\n const stats = files.map(path => {\r\n const fullPath = join(baseDir, path)\r\n const displayPath = path.replace(/\\\\/g, '/')\r\n return {\r\n path: displayPath,\r\n size: statSync(fullPath).size,\r\n }\r\n })\r\n\r\n const total = stats.reduce((sum, stat) => sum + stat.size, 0)\r\n const lines = stats.map(({ path, size }) => {\r\n const paddedPath = pc.cyan(path.padEnd(width))\r\n const formattedSize = pc.dim(bytes(size))\r\n return ` ${paddedPath} ${formattedSize}`\r\n })\r\n\r\n const footerLabel = `${files.length} files, total:`.padEnd(width)\r\n const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`\r\n return [...lines, footer].join('\\n')\r\n}\r\n","import ora from 'ora'\r\n\r\n/** Base context type - extend this to add your own properties */\r\nexport type Context = Record<string, unknown>\r\n\r\n/**\r\n * A task that runs in a pipeline\r\n * @template TContext - The context type for this task\r\n * @property name - Task name shown in spinner\r\n * @property run - Task implementation - return updates to merge into context\r\n * @property onSuccess - Optional success message (default: task name)\r\n * @property onError - Optional error message (default: task name)\r\n */\r\nexport interface Task<TContext extends Context> {\r\n name: string\r\n run: (ctx: TContext) => Promise<Partial<TContext> | void>\r\n onSuccess?: (ctx: TContext, duration: number) => string\r\n onError?: (error: Error) => string\r\n}\r\n\r\n/**\r\n * Result after running a pipeline\r\n * @template TContext - The context type\r\n * @property context - Final context after all tasks\r\n * @property duration - Total duration in milliseconds\r\n */\r\nexport type PipeResult<TContext extends Context> = {\r\n context: TContext\r\n duration: number\r\n}\r\n\r\n/**\r\n * Creates a task pipeline that runs tasks sequentially with spinners\r\n * @template TContext - The context type for the pipeline\r\n * @param tasks - Array of tasks (supports conditional spreading with falsy values)\r\n */\r\nexport function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]) {\r\n return {\r\n run: async (initialContext: TContext) => {\r\n const startTime = Date.now()\r\n let context = initialContext\r\n\r\n for (const task of tasks) {\r\n // Skip falsy values (supports conditional spreading)\r\n if (!task) continue\r\n const taskStart = Date.now()\r\n const spinner = ora(task.name).start()\r\n\r\n try {\r\n const updates = await task.run(context) ?? {}\r\n const duration = Date.now() - taskStart\r\n const message = task.onSuccess?.(context, duration) ?? task.name\r\n spinner.succeed(message)\r\n context = { ...context, ...updates }\r\n }\r\n catch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error))\r\n const message = task.onError?.(err) ?? task.name\r\n spinner.fail(message)\r\n throw new Error(`Task \"${task.name}\" failed: ${err.message}`, { cause: err })\r\n }\r\n }\r\n\r\n const duration = Date.now() - startTime\r\n return { context, duration } as PipeResult<TContext>\r\n },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAaA,MAAa,QAAQA;;;;;;AAOrB,MAAa,WAAWC;;;;;;;;AASxB,SAAgB,SAAS,SAAiB,UAAU,QAAQ,QAAQ,IAAI;CAGtE,MAAM,iCAAa,SAAS,EAAE,SAAS,MAAM,CAAC;CAC9C,MAAM,QAAQ,IAAI,WAAM,CACrB,mBAAmB,CACnB,QAAO,WAAQ,QAAQC,OAAK,CAAC,CAC7B,MAAM,QAAQ,CACd,MAAM;CAET,MAAM,QAAQ,MAAM,KAAI,WAAQ;EAC9B,MAAM,0BAAgB,SAASA,OAAK;AAEpC,SAAO;GACL,MAFkBA,OAAK,QAAQ,OAAO,IAAI;GAG1C,uBAAe,SAAS,CAAC;GAC1B;GACD;CAEF,MAAM,QAAQ,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,MAAM,EAAE;CAC7D,MAAM,QAAQ,MAAM,KAAK,EAAE,cAAM,WAAW;AAG1C,SAAO,KAFYC,mBAAG,KAAKD,OAAK,OAAO,MAAM,CAAC,CAEvB,IADDC,mBAAG,IAAI,MAAM,KAAK,CAAC;GAEzC;CAEF,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,OAAO,MAAM;CACjE,MAAM,SAAS,KAAKA,mBAAG,IAAI,YAAY,CAAC,IAAIA,mBAAG,IAAI,MAAM,MAAM,CAAC;AAChE,QAAO,CAAC,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK;;;;;;;;;;ACrBtC,SAAgB,KAA+B,OAAsD;AACnG,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;AAExB,OAAI,CAAC,KAAM;GACX,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,2BAAc,KAAK,KAAK,CAAC,OAAO;AAEtC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE;IAC7C,MAAMC,aAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,UAAU,KAAK,YAAY,SAASA,WAAS,IAAI,KAAK;AAC5D,YAAQ,QAAQ,QAAQ;AACxB,cAAU;KAAE,GAAG;KAAS,GAAG;KAAS;YAE/B,OAAO;IACZ,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACrE,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,KAAK;AAC5C,YAAQ,KAAK,QAAQ;AACrB,UAAM,IAAI,MAAM,SAAS,KAAK,KAAK,YAAY,IAAI,WAAW,EAAE,OAAO,KAAK,CAAC;;;EAIjF,MAAMA,aAAW,KAAK,KAAK,GAAG;AAC9B,SAAO;GAAE;GAAS;GAAU;IAE/B"}
package/dist/index.d.cts CHANGED
@@ -53,8 +53,9 @@ type PipeResult<TContext extends Context> = {
53
53
  /**
54
54
  * Creates a task pipeline that runs tasks sequentially with spinners
55
55
  * @template TContext - The context type for the pipeline
56
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
56
57
  */
57
- declare function pipe<TContext extends Context>(tasks: Task<TContext>[]): {
58
+ declare function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]): {
58
59
  run: (initialContext: TContext) => Promise<PipeResult<TContext>>;
59
60
  };
60
61
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/format.ts","../src/pipeline.ts"],"mappings":";;;;;AAaA;AAOA;AASA;;cAhBa,KAAA,SAAK,WAAA;AAAA;AAOlB;AASA;;;AAhBkB,cAOL,QAAA,SAAQ,QAAA;AAAA;AASrB;;;;AC1BA;AAUA;ADOqB,iBASL,QAAA,CAAA,OAAA,UAAA,OAAA,WAAA,KAAA;;;;KC1BJ,OAAA,GAAU,MAAA;AAAA;AAUtB;;;;;;;AAVsB,UAUL,IAAA,kBAAsB,OAAA;EAAA,IAAA;EAAA,GAAA,GAAA,GAAA,EAE1B,QAAA,KAAa,OAAA,CAAQ,OAAA,CAAQ,QAAA;EAAA,SAAA,IAAA,GAAA,EACtB,QAAA,EAAA,QAAA;EAAA,OAAA,IAAA,KAAA,EACA,KAAA;AAAA;AAAA;;AASpB;AASA;;;AAlBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AAQX;;;AARW,iBAQK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,EAAgB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE3B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/format.ts","../src/pipeline.ts"],"mappings":";;;;;AAaA;AAOA;AASA;;cAhBa,KAAA,SAAK,WAAA;AAAA;AAOlB;AASA;;;AAhBkB,cAOL,QAAA,SAAQ,QAAA;AAAA;AASrB;;;;AC1BA;AAUA;ADOqB,iBASL,QAAA,CAAA,OAAA,UAAA,OAAA,WAAA,KAAA;;;;KC1BJ,OAAA,GAAU,MAAA;AAAA;AAUtB;;;;;;;AAVsB,UAUL,IAAA,kBAAsB,OAAA;EAAA,IAAA;EAAA,GAAA,GAAA,GAAA,EAE1B,QAAA,KAAa,OAAA,CAAQ,OAAA,CAAQ,QAAA;EAAA,SAAA,IAAA,GAAA,EACtB,QAAA,EAAA,QAAA;EAAA,OAAA,IAAA,KAAA,EACA,KAAA;AAAA;AAAA;;AASpB;AAUA;;;AAnBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AASX;;;;AATW,iBASK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,GAAiB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE5B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
package/dist/index.d.mts CHANGED
@@ -53,8 +53,9 @@ type PipeResult<TContext extends Context> = {
53
53
  /**
54
54
  * Creates a task pipeline that runs tasks sequentially with spinners
55
55
  * @template TContext - The context type for the pipeline
56
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
56
57
  */
57
- declare function pipe<TContext extends Context>(tasks: Task<TContext>[]): {
58
+ declare function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]): {
58
59
  run: (initialContext: TContext) => Promise<PipeResult<TContext>>;
59
60
  };
60
61
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/format.ts","../src/pipeline.ts"],"mappings":";;;;;AAaA;AAOA;AASA;;cAhBa,KAAA,SAAK,WAAA;AAAA;AAOlB;AASA;;;AAhBkB,cAOL,QAAA,SAAQ,QAAA;AAAA;AASrB;;;;AC1BA;AAUA;ADOqB,iBASL,QAAA,CAAA,OAAA,UAAA,OAAA,WAAA,KAAA;;;;KC1BJ,OAAA,GAAU,MAAA;AAAA;AAUtB;;;;;;;AAVsB,UAUL,IAAA,kBAAsB,OAAA;EAAA,IAAA;EAAA,GAAA,GAAA,GAAA,EAE1B,QAAA,KAAa,OAAA,CAAQ,OAAA,CAAQ,QAAA;EAAA,SAAA,IAAA,GAAA,EACtB,QAAA,EAAA,QAAA;EAAA,OAAA,IAAA,KAAA,EACA,KAAA;AAAA;AAAA;;AASpB;AASA;;;AAlBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AAQX;;;AARW,iBAQK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,EAAgB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE3B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/format.ts","../src/pipeline.ts"],"mappings":";;;;;AAaA;AAOA;AASA;;cAhBa,KAAA,SAAK,WAAA;AAAA;AAOlB;AASA;;;AAhBkB,cAOL,QAAA,SAAQ,QAAA;AAAA;AASrB;;;;AC1BA;AAUA;ADOqB,iBASL,QAAA,CAAA,OAAA,UAAA,OAAA,WAAA,KAAA;;;;KC1BJ,OAAA,GAAU,MAAA;AAAA;AAUtB;;;;;;;AAVsB,UAUL,IAAA,kBAAsB,OAAA;EAAA,IAAA;EAAA,GAAA,GAAA,GAAA,EAE1B,QAAA,KAAa,OAAA,CAAQ,OAAA,CAAQ,QAAA;EAAA,SAAA,IAAA,GAAA,EACtB,QAAA,EAAA,QAAA;EAAA,OAAA,IAAA,KAAA,EACA,KAAA;AAAA;AAAA;;AASpB;AAUA;;;AAnBoB,KASR,UAAA,kBAA4B,OAAA;EAAA,OAAA,EAC7B,QAAA;EAAA,QAAA;AAAA;AAAA;AASX;;;;AATW,iBASK,IAAA,kBAAsB,OAAA,CAAA,CAAA,KAAA,GAAiB,IAAA,CAAK,QAAA;EAAA,GAAA,GAAA,cAAA,EAE5B,QAAA,KAAQ,OAAA,CAAA,UAAA,CAAA,QAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -51,12 +51,14 @@ function fileList(baseDir, pattern = "**/*", width = 45) {
51
51
  /**
52
52
  * Creates a task pipeline that runs tasks sequentially with spinners
53
53
  * @template TContext - The context type for the pipeline
54
+ * @param tasks - Array of tasks (supports conditional spreading with falsy values)
54
55
  */
55
56
  function pipe(tasks) {
56
57
  return { run: async (initialContext) => {
57
58
  const startTime = Date.now();
58
59
  let context = initialContext;
59
60
  for (const task of tasks) {
61
+ if (!task) continue;
60
62
  const taskStart = Date.now();
61
63
  const spinner = ora(task.name).start();
62
64
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["duration"],"sources":["../src/format.ts","../src/pipeline.ts"],"sourcesContent":["import { statSync } from 'fs'\r\nimport { join } from 'path'\r\nimport { fdir } from 'fdir'\r\nimport pc from 'picocolors'\r\nimport pm from 'picomatch'\r\nimport prettyBytes from 'pretty-bytes'\r\nimport prettyMs from 'pretty-ms'\r\n\r\n/**\r\n * Formats byte sizes into human-readable strings\r\n * @param size - The size in bytes\r\n * @returns Formatted string (e.g., \"1.23 kB\", \"4.56 MB\")\r\n */\r\nexport const bytes = prettyBytes\r\n\r\n/**\r\n * Formats milliseconds into human-readable durations\r\n * @param ms - The duration in milliseconds\r\n * @returns Formatted string (e.g., \"42ms\", \"1.2s\", \"2m 3.5s\")\r\n */\r\nexport const duration = prettyMs\r\n\r\n/**\r\n * Display a formatted list of files in a directory with their sizes\r\n * @param baseDir - The base directory to search\r\n * @param pattern - Glob pattern to match files\r\n * @param width - Row width for file paths\r\n * @returns Formatted file list with sizes and total\r\n */\r\nexport function fileList(baseDir: string, pattern = '**/*', width = 45) {\r\n // Find all files matching the pattern\r\n // Use windows: true option to handle both / and \\ as path separators\r\n const matcher = pm(pattern, { windows: true })\r\n const files = new fdir()\r\n .withRelativePaths()\r\n .filter(path => matcher(path))\r\n .crawl(baseDir)\r\n .sync()\r\n\r\n const stats = files.map(path => {\r\n const fullPath = join(baseDir, path)\r\n const displayPath = path.replace(/\\\\/g, '/')\r\n return {\r\n path: displayPath,\r\n size: statSync(fullPath).size,\r\n }\r\n })\r\n\r\n const total = stats.reduce((sum, stat) => sum + stat.size, 0)\r\n const lines = stats.map(({ path, size }) => {\r\n const paddedPath = pc.cyan(path.padEnd(width))\r\n const formattedSize = pc.dim(bytes(size))\r\n return ` ${paddedPath} ${formattedSize}`\r\n })\r\n\r\n const footerLabel = `${files.length} files, total:`.padEnd(width)\r\n const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`\r\n return [...lines, footer].join('\\n')\r\n}\r\n","import ora from 'ora'\r\n\r\n/** Base context type - extend this to add your own properties */\r\nexport type Context = Record<string, unknown>\r\n\r\n/**\r\n * A task that runs in a pipeline\r\n * @template TContext - The context type for this task\r\n * @property name - Task name shown in spinner\r\n * @property run - Task implementation - return updates to merge into context\r\n * @property onSuccess - Optional success message (default: task name)\r\n * @property onError - Optional error message (default: task name)\r\n */\r\nexport interface Task<TContext extends Context> {\r\n name: string\r\n run: (ctx: TContext) => Promise<Partial<TContext> | void>\r\n onSuccess?: (ctx: TContext, duration: number) => string\r\n onError?: (error: Error) => string\r\n}\r\n\r\n/**\r\n * Result after running a pipeline\r\n * @template TContext - The context type\r\n * @property context - Final context after all tasks\r\n * @property duration - Total duration in milliseconds\r\n */\r\nexport type PipeResult<TContext extends Context> = {\r\n context: TContext\r\n duration: number\r\n}\r\n\r\n/**\r\n * Creates a task pipeline that runs tasks sequentially with spinners\r\n * @template TContext - The context type for the pipeline\r\n */\r\nexport function pipe<TContext extends Context>(tasks: Task<TContext>[]) {\r\n return {\r\n run: async (initialContext: TContext) => {\r\n const startTime = Date.now()\r\n let context = initialContext\r\n\r\n for (const task of tasks) {\r\n const taskStart = Date.now()\r\n const spinner = ora(task.name).start()\r\n\r\n try {\r\n const updates = await task.run(context) ?? {}\r\n const duration = Date.now() - taskStart\r\n const message = task.onSuccess?.(context, duration) ?? task.name\r\n spinner.succeed(message)\r\n context = { ...context, ...updates }\r\n }\r\n catch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error))\r\n const message = task.onError?.(err) ?? task.name\r\n spinner.fail(message)\r\n throw new Error(`Task \"${task.name}\" failed: ${err.message}`, { cause: err })\r\n }\r\n }\r\n\r\n const duration = Date.now() - startTime\r\n return { context, duration } as PipeResult<TContext>\r\n },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;AAaA,MAAa,QAAQ;;;;;;AAOrB,MAAa,WAAW;;;;;;;;AASxB,SAAgB,SAAS,SAAiB,UAAU,QAAQ,QAAQ,IAAI;CAGtE,MAAM,UAAU,GAAG,SAAS,EAAE,SAAS,MAAM,CAAC;CAC9C,MAAM,QAAQ,IAAI,MAAM,CACrB,mBAAmB,CACnB,QAAO,SAAQ,QAAQ,KAAK,CAAC,CAC7B,MAAM,QAAQ,CACd,MAAM;CAET,MAAM,QAAQ,MAAM,KAAI,SAAQ;EAC9B,MAAM,WAAW,KAAK,SAAS,KAAK;AAEpC,SAAO;GACL,MAFkB,KAAK,QAAQ,OAAO,IAAI;GAG1C,MAAM,SAAS,SAAS,CAAC;GAC1B;GACD;CAEF,MAAM,QAAQ,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,MAAM,EAAE;CAC7D,MAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,WAAW;AAG1C,SAAO,KAFY,GAAG,KAAK,KAAK,OAAO,MAAM,CAAC,CAEvB,IADD,GAAG,IAAI,MAAM,KAAK,CAAC;GAEzC;CAEF,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,OAAO,MAAM;CACjE,MAAM,SAAS,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,MAAM,MAAM,CAAC;AAChE,QAAO,CAAC,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK;;;;;;;;;ACtBtC,SAAgB,KAA+B,OAAyB;AACtE,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,UAAU,IAAI,KAAK,KAAK,CAAC,OAAO;AAEtC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE;IAC7C,MAAMA,aAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,UAAU,KAAK,YAAY,SAASA,WAAS,IAAI,KAAK;AAC5D,YAAQ,QAAQ,QAAQ;AACxB,cAAU;KAAE,GAAG;KAAS,GAAG;KAAS;YAE/B,OAAO;IACZ,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACrE,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,KAAK;AAC5C,YAAQ,KAAK,QAAQ;AACrB,UAAM,IAAI,MAAM,SAAS,KAAK,KAAK,YAAY,IAAI,WAAW,EAAE,OAAO,KAAK,CAAC;;;EAIjF,MAAMA,aAAW,KAAK,KAAK,GAAG;AAC9B,SAAO;GAAE;GAAS;GAAU;IAE/B"}
1
+ {"version":3,"file":"index.mjs","names":["duration"],"sources":["../src/format.ts","../src/pipeline.ts"],"sourcesContent":["import { statSync } from 'fs'\r\nimport { join } from 'path'\r\nimport { fdir } from 'fdir'\r\nimport pc from 'picocolors'\r\nimport pm from 'picomatch'\r\nimport prettyBytes from 'pretty-bytes'\r\nimport prettyMs from 'pretty-ms'\r\n\r\n/**\r\n * Formats byte sizes into human-readable strings\r\n * @param size - The size in bytes\r\n * @returns Formatted string (e.g., \"1.23 kB\", \"4.56 MB\")\r\n */\r\nexport const bytes = prettyBytes\r\n\r\n/**\r\n * Formats milliseconds into human-readable durations\r\n * @param ms - The duration in milliseconds\r\n * @returns Formatted string (e.g., \"42ms\", \"1.2s\", \"2m 3.5s\")\r\n */\r\nexport const duration = prettyMs\r\n\r\n/**\r\n * Display a formatted list of files in a directory with their sizes\r\n * @param baseDir - The base directory to search\r\n * @param pattern - Glob pattern to match files\r\n * @param width - Row width for file paths\r\n * @returns Formatted file list with sizes and total\r\n */\r\nexport function fileList(baseDir: string, pattern = '**/*', width = 45) {\r\n // Find all files matching the pattern\r\n // Use windows: true option to handle both / and \\ as path separators\r\n const matcher = pm(pattern, { windows: true })\r\n const files = new fdir()\r\n .withRelativePaths()\r\n .filter(path => matcher(path))\r\n .crawl(baseDir)\r\n .sync()\r\n\r\n const stats = files.map(path => {\r\n const fullPath = join(baseDir, path)\r\n const displayPath = path.replace(/\\\\/g, '/')\r\n return {\r\n path: displayPath,\r\n size: statSync(fullPath).size,\r\n }\r\n })\r\n\r\n const total = stats.reduce((sum, stat) => sum + stat.size, 0)\r\n const lines = stats.map(({ path, size }) => {\r\n const paddedPath = pc.cyan(path.padEnd(width))\r\n const formattedSize = pc.dim(bytes(size))\r\n return ` ${paddedPath} ${formattedSize}`\r\n })\r\n\r\n const footerLabel = `${files.length} files, total:`.padEnd(width)\r\n const footer = ` ${pc.dim(footerLabel)} ${pc.dim(bytes(total))}`\r\n return [...lines, footer].join('\\n')\r\n}\r\n","import ora from 'ora'\r\n\r\n/** Base context type - extend this to add your own properties */\r\nexport type Context = Record<string, unknown>\r\n\r\n/**\r\n * A task that runs in a pipeline\r\n * @template TContext - The context type for this task\r\n * @property name - Task name shown in spinner\r\n * @property run - Task implementation - return updates to merge into context\r\n * @property onSuccess - Optional success message (default: task name)\r\n * @property onError - Optional error message (default: task name)\r\n */\r\nexport interface Task<TContext extends Context> {\r\n name: string\r\n run: (ctx: TContext) => Promise<Partial<TContext> | void>\r\n onSuccess?: (ctx: TContext, duration: number) => string\r\n onError?: (error: Error) => string\r\n}\r\n\r\n/**\r\n * Result after running a pipeline\r\n * @template TContext - The context type\r\n * @property context - Final context after all tasks\r\n * @property duration - Total duration in milliseconds\r\n */\r\nexport type PipeResult<TContext extends Context> = {\r\n context: TContext\r\n duration: number\r\n}\r\n\r\n/**\r\n * Creates a task pipeline that runs tasks sequentially with spinners\r\n * @template TContext - The context type for the pipeline\r\n * @param tasks - Array of tasks (supports conditional spreading with falsy values)\r\n */\r\nexport function pipe<TContext extends Context>(tasks: (Task<TContext> | false | null | undefined)[]) {\r\n return {\r\n run: async (initialContext: TContext) => {\r\n const startTime = Date.now()\r\n let context = initialContext\r\n\r\n for (const task of tasks) {\r\n // Skip falsy values (supports conditional spreading)\r\n if (!task) continue\r\n const taskStart = Date.now()\r\n const spinner = ora(task.name).start()\r\n\r\n try {\r\n const updates = await task.run(context) ?? {}\r\n const duration = Date.now() - taskStart\r\n const message = task.onSuccess?.(context, duration) ?? task.name\r\n spinner.succeed(message)\r\n context = { ...context, ...updates }\r\n }\r\n catch (error) {\r\n const err = error instanceof Error ? error : new Error(String(error))\r\n const message = task.onError?.(err) ?? task.name\r\n spinner.fail(message)\r\n throw new Error(`Task \"${task.name}\" failed: ${err.message}`, { cause: err })\r\n }\r\n }\r\n\r\n const duration = Date.now() - startTime\r\n return { context, duration } as PipeResult<TContext>\r\n },\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;AAaA,MAAa,QAAQ;;;;;;AAOrB,MAAa,WAAW;;;;;;;;AASxB,SAAgB,SAAS,SAAiB,UAAU,QAAQ,QAAQ,IAAI;CAGtE,MAAM,UAAU,GAAG,SAAS,EAAE,SAAS,MAAM,CAAC;CAC9C,MAAM,QAAQ,IAAI,MAAM,CACrB,mBAAmB,CACnB,QAAO,SAAQ,QAAQ,KAAK,CAAC,CAC7B,MAAM,QAAQ,CACd,MAAM;CAET,MAAM,QAAQ,MAAM,KAAI,SAAQ;EAC9B,MAAM,WAAW,KAAK,SAAS,KAAK;AAEpC,SAAO;GACL,MAFkB,KAAK,QAAQ,OAAO,IAAI;GAG1C,MAAM,SAAS,SAAS,CAAC;GAC1B;GACD;CAEF,MAAM,QAAQ,MAAM,QAAQ,KAAK,SAAS,MAAM,KAAK,MAAM,EAAE;CAC7D,MAAM,QAAQ,MAAM,KAAK,EAAE,MAAM,WAAW;AAG1C,SAAO,KAFY,GAAG,KAAK,KAAK,OAAO,MAAM,CAAC,CAEvB,IADD,GAAG,IAAI,MAAM,KAAK,CAAC;GAEzC;CAEF,MAAM,cAAc,GAAG,MAAM,OAAO,gBAAgB,OAAO,MAAM;CACjE,MAAM,SAAS,KAAK,GAAG,IAAI,YAAY,CAAC,IAAI,GAAG,IAAI,MAAM,MAAM,CAAC;AAChE,QAAO,CAAC,GAAG,OAAO,OAAO,CAAC,KAAK,KAAK;;;;;;;;;;ACrBtC,SAAgB,KAA+B,OAAsD;AACnG,QAAO,EACL,KAAK,OAAO,mBAA6B;EACvC,MAAM,YAAY,KAAK,KAAK;EAC5B,IAAI,UAAU;AAEd,OAAK,MAAM,QAAQ,OAAO;AAExB,OAAI,CAAC,KAAM;GACX,MAAM,YAAY,KAAK,KAAK;GAC5B,MAAM,UAAU,IAAI,KAAK,KAAK,CAAC,OAAO;AAEtC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,IAAI,QAAQ,IAAI,EAAE;IAC7C,MAAMA,aAAW,KAAK,KAAK,GAAG;IAC9B,MAAM,UAAU,KAAK,YAAY,SAASA,WAAS,IAAI,KAAK;AAC5D,YAAQ,QAAQ,QAAQ;AACxB,cAAU;KAAE,GAAG;KAAS,GAAG;KAAS;YAE/B,OAAO;IACZ,MAAM,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;IACrE,MAAM,UAAU,KAAK,UAAU,IAAI,IAAI,KAAK;AAC5C,YAAQ,KAAK,QAAQ;AACrB,UAAM,IAAI,MAAM,SAAS,KAAK,KAAK,YAAY,IAAI,WAAW,EAAE,OAAO,KAAK,CAAC;;;EAIjF,MAAMA,aAAW,KAAK,KAAK,GAAG;AAC9B,SAAO;GAAE;GAAS;GAAU;IAE/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idlesummer/tasker",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "A simple task pipeline runner with CLI spinners and formatters",
5
5
  "keywords": [
6
6
  "task",