@plugjs/plug 0.1.0 → 0.1.2

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 (73) hide show
  1. package/dist/asserts.cjs +6 -1
  2. package/dist/asserts.cjs.map +1 -1
  3. package/dist/asserts.d.ts +2 -0
  4. package/dist/asserts.mjs +5 -1
  5. package/dist/asserts.mjs.map +1 -1
  6. package/dist/build.cjs +6 -8
  7. package/dist/build.cjs.map +1 -1
  8. package/dist/build.mjs +7 -9
  9. package/dist/build.mjs.map +1 -1
  10. package/dist/files.cjs +4 -8
  11. package/dist/files.cjs.map +1 -1
  12. package/dist/files.mjs +4 -8
  13. package/dist/files.mjs.map +1 -1
  14. package/dist/fs.cjs.map +1 -1
  15. package/dist/fs.mjs.map +1 -1
  16. package/dist/helpers.cjs +10 -0
  17. package/dist/helpers.cjs.map +1 -1
  18. package/dist/helpers.d.ts +6 -0
  19. package/dist/helpers.mjs +9 -0
  20. package/dist/helpers.mjs.map +1 -1
  21. package/dist/index.cjs +26 -0
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.mjs +15 -2
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/logging/emit.d.ts +1 -1
  27. package/dist/logging/levels.d.ts +6 -6
  28. package/dist/logging/logger.cjs +8 -11
  29. package/dist/logging/logger.cjs.map +1 -1
  30. package/dist/logging/logger.d.ts +10 -10
  31. package/dist/logging/logger.mjs +8 -11
  32. package/dist/logging/logger.mjs.map +1 -1
  33. package/dist/logging/report.cjs +1 -2
  34. package/dist/logging/report.cjs.map +1 -1
  35. package/dist/logging/report.d.ts +1 -1
  36. package/dist/logging/report.mjs +1 -2
  37. package/dist/logging/report.mjs.map +1 -1
  38. package/dist/logging.cjs +2 -11
  39. package/dist/logging.cjs.map +1 -1
  40. package/dist/logging.d.ts +1 -1
  41. package/dist/logging.mjs +2 -11
  42. package/dist/logging.mjs.map +1 -1
  43. package/dist/paths.cjs +22 -24
  44. package/dist/paths.cjs.map +1 -1
  45. package/dist/paths.d.ts +8 -8
  46. package/dist/paths.mjs +22 -24
  47. package/dist/paths.mjs.map +1 -1
  48. package/dist/pipe.d.ts +7 -7
  49. package/dist/plugs/copy.cjs +11 -3
  50. package/dist/plugs/copy.cjs.map +1 -1
  51. package/dist/plugs/copy.d.ts +2 -2
  52. package/dist/plugs/copy.mjs +11 -3
  53. package/dist/plugs/copy.mjs.map +1 -1
  54. package/dist/plugs/esbuild/fix-extensions.cjs +3 -4
  55. package/dist/plugs/esbuild/fix-extensions.cjs.map +1 -1
  56. package/dist/plugs/esbuild/fix-extensions.mjs +3 -4
  57. package/dist/plugs/esbuild/fix-extensions.mjs.map +1 -1
  58. package/dist/plugs/esbuild.d.ts +1 -1
  59. package/dist/types.d.ts +7 -7
  60. package/dist/utils/match.d.ts +1 -1
  61. package/dist/utils/options.d.ts +3 -3
  62. package/extra/ts-loader.mjs +3 -3
  63. package/package.json +20 -14
  64. package/src/asserts.ts +6 -1
  65. package/src/build.ts +179 -0
  66. package/src/files.ts +4 -4
  67. package/src/fs.ts +6 -5
  68. package/src/helpers.ts +16 -0
  69. package/src/index.ts +2 -2
  70. package/src/logging/logger.ts +31 -35
  71. package/src/logging.ts +7 -13
  72. package/src/paths.ts +35 -40
  73. package/src/plugs/copy.ts +13 -5
package/src/build.ts ADDED
@@ -0,0 +1,179 @@
1
+ import { assert } from './asserts'
2
+ import { runAsync } from './async'
3
+ import { $ms, $p, $t, getLogger, log, logOptions } from './logging'
4
+ import { Context, ContextPromises, PipeImpl } from './pipe'
5
+ import { findCaller } from './utils/caller'
6
+ import { parseOptions } from './utils/options'
7
+
8
+ import type { Pipe } from './index'
9
+ import type { AbsolutePath } from './paths'
10
+ import type {
11
+ Build,
12
+ BuildDef,
13
+ Props,
14
+ Result,
15
+ State,
16
+ Task,
17
+ TaskDef,
18
+ Tasks,
19
+ ThisBuild,
20
+ } from './types'
21
+
22
+ /* ========================================================================== *
23
+ * TASK *
24
+ * ========================================================================== */
25
+
26
+ class TaskImpl implements Task {
27
+ constructor(
28
+ public readonly buildFile: AbsolutePath,
29
+ public readonly tasks: Tasks,
30
+ public readonly props: Props,
31
+ private readonly _def: TaskDef,
32
+ ) {}
33
+
34
+ invoke(state: State, taskName: string): Promise<Result> {
35
+ assert(! state.stack.includes(this), `Recursion detected calling ${$t(taskName)}`)
36
+
37
+ /* Check cache */
38
+ const cached = state.cache.get(this)
39
+ if (cached) return cached
40
+
41
+ /* Create new substate merging sibling tasks/props and adding this to the stack */
42
+ const props: Record<string, string> = Object.assign({}, this.props, state.props)
43
+ const tasks: Record<string, Task> = Object.assign({}, this.tasks, state.tasks)
44
+ const stack = [ ...state.stack, this ]
45
+ const cache = state.cache
46
+
47
+ /* Create run context and build */
48
+ const context = new Context(this.buildFile, taskName)
49
+
50
+ const build = new Proxy({}, {
51
+ get(_: any, name: string): void | string | (() => Pipe) {
52
+ // Tasks first, props might come also from environment
53
+ if (name in tasks) {
54
+ return (): Pipe => {
55
+ const state = { stack, cache, tasks, props }
56
+ const promise = tasks[name]!.invoke(state, name)
57
+ return new PipeImpl(context, promise)
58
+ }
59
+ } else if (name in props) {
60
+ return props[name]
61
+ }
62
+ },
63
+ })
64
+
65
+ /* Some logging */
66
+ context.log.info('Running...')
67
+ const now = Date.now()
68
+
69
+ /* Run asynchronously in an asynchronous context */
70
+ const promise = runAsync(context, taskName, async () => {
71
+ return await this._def.call(build) || undefined
72
+ }).then((result) => {
73
+ context.log.notice(`Success ${$ms(Date.now() - now)}`)
74
+ return result
75
+ }).catch((error) => {
76
+ throw context.log.fail(`Failure ${$ms(Date.now() - now)}`, error)
77
+ }).finally(() => ContextPromises.wait(context))
78
+
79
+ /* Cache the resulting promise and return it */
80
+ cache.set(this, promise)
81
+ return promise
82
+ }
83
+ }
84
+
85
+ /* ========================================================================== *
86
+ * BUILD COMPILER *
87
+ * ========================================================================== */
88
+
89
+ /** Symbol indicating that an object is a {@link Build} */
90
+ const buildMarker = Symbol.for('plugjs:isBuild')
91
+
92
+ /** Compile a {@link BuildDef | build definition} into a {@link Build} */
93
+ export function build<
94
+ D extends BuildDef, B extends ThisBuild<D>
95
+ >(def: D & ThisType<B>): Build<D> {
96
+ const buildFile = findCaller(build)
97
+ const tasks: Record<string, Task> = {}
98
+ const props: Record<string, string> = {}
99
+
100
+ /* Iterate through all definition extracting properties and tasks */
101
+ for (const [ key, val ] of Object.entries(def)) {
102
+ let len = 0
103
+ if (typeof val === 'string') {
104
+ props[key] = val
105
+ } else if (typeof val === 'function') {
106
+ tasks[key] = new TaskImpl(buildFile, tasks, props, val)
107
+ len = key.length
108
+ } else if (val instanceof TaskImpl) {
109
+ tasks[key] = val
110
+ len = key.length
111
+ }
112
+
113
+ /* Update the logger's own "taskLength" for nice printing */
114
+ /* coverage ignore if */
115
+ if (len > logOptions.taskLength) logOptions.taskLength = len
116
+ }
117
+
118
+ /* Create the "call" function for this build */
119
+ const invoke: InvokeBuild = async function invoke(
120
+ taskNames: string[],
121
+ overrideProps: Record<string, string | undefined> = {},
122
+ ): Promise<void> {
123
+ /* Our "root" logger and initial (empty) state */
124
+ const logger = getLogger()
125
+ const state = {
126
+ cache: new Map<Task, Promise<Result>>(),
127
+ stack: [] as Task[],
128
+ props: Object.assign({}, props, overrideProps),
129
+ tasks: tasks,
130
+ }
131
+
132
+ /* Let's go down to business */
133
+ logger.notice('Starting...')
134
+ const now = Date.now()
135
+
136
+ try {
137
+ /* Run tasks _serially_ */
138
+ for (const name of taskNames) {
139
+ const task = tasks[name]
140
+ assert(task, `Task ${$t(name)} not found in build ${$p(buildFile)}`)
141
+ await task.invoke(state, name)
142
+ }
143
+ logger.notice(`Build successful ${$ms(Date.now() - now)}`)
144
+ } catch (error) {
145
+ throw logger.fail(`Build failed ${$ms(Date.now() - now)}`, error)
146
+ }
147
+ }
148
+
149
+ /* Create our build, the collection of all props and tasks */
150
+ const compiled = Object.assign({}, props, tasks) as Build<D>
151
+
152
+ /* Sneak our "call" function in the build, for the CLI and "call" below */
153
+ Object.defineProperty(compiled, buildMarker, { value: invoke })
154
+
155
+ /* All done! */
156
+ return compiled
157
+ }
158
+
159
+ /** Internal type describing the build invocation function */
160
+ type InvokeBuild = (tasks: string[], props?: Record<string, string | undefined>) => Promise<void>
161
+
162
+ /** Serially invoke tasks in a {@link Build} optionally overriding properties */
163
+ export async function invoke(
164
+ build: Build,
165
+ ...args:
166
+ | [ ...taskNames: [ string, ...string[] ] ]
167
+ | [ ...taskNames: [ string, ...string[] ], options: Record<string, string | undefined> ]
168
+ ): Promise<void> {
169
+ const { params: tasks, options: props } = parseOptions(args, {})
170
+
171
+ /* Get the calling function from the sneaked-in property in build */
172
+ const invoke: InvokeBuild = (build as any)[buildMarker]
173
+
174
+ /* Triple check that we actually _have_ a function (no asserts here, log!) */
175
+ if (typeof invoke !== 'function') log.fail('Unknown build type')
176
+
177
+ /* Call everyhin that needs to be called */
178
+ return await invoke(tasks, props)
179
+ }
package/src/files.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { assert } from './asserts'
1
2
  import { mkdir, writeFile } from './fs'
2
3
  import { assertRelativeChildPath, getAbsoluteParent, resolveAbsolutePath } from './paths'
3
4
 
@@ -88,9 +89,8 @@ export class Files {
88
89
  directory: instance.directory,
89
90
 
90
91
  add(...files: string[]): FilesBuilder {
91
- if (built) throw new Error('FileBuilder "build()" already called')
92
+ assert(! built, 'FileBuilder "build()" already called')
92
93
 
93
- if (typeof files === 'string') files = [ files ]
94
94
  for (const file of files) {
95
95
  const relative = assertRelativeChildPath(instance.directory, file)
96
96
  set.add(relative)
@@ -100,7 +100,7 @@ export class Files {
100
100
  },
101
101
 
102
102
  merge(...args: Files[]): FilesBuilder {
103
- if (built) throw new Error('FileBuilder "build()" already called')
103
+ assert(! built, 'FileBuilder "build()" already called')
104
104
 
105
105
  for (const files of args) {
106
106
  for (const file of files.absolutePaths()) {
@@ -124,7 +124,7 @@ export class Files {
124
124
  },
125
125
 
126
126
  build(): Files {
127
- if (built) throw new Error('FileBuilder "build()" already called')
127
+ assert(! built, 'FileBuilder "build()" already called')
128
128
 
129
129
  built = true
130
130
  instance._files.push(...set)
package/src/fs.ts CHANGED
@@ -30,11 +30,12 @@ const fs = Object.entries(fsp as any).reduce((fs, [ key, val ]) => {
30
30
  /* If the value is a function, wrap it! */
31
31
  const f = function(...args: any[]): any {
32
32
  /* Call the function, and _catch_ any error */
33
- return val.apply(fsp, args).catch((error: any) => {
34
- /* For any error caught, we fill in the stack trace */
35
- Error.captureStackTrace(error)
36
- throw error
37
- })
33
+ return val.apply(fsp, args)
34
+ .catch(/* coverage ignore next*/ (error: any) => {
35
+ /* For any error caught, we fill in the stack trace */
36
+ Error.captureStackTrace(error)
37
+ throw error
38
+ })
38
39
  }
39
40
 
40
41
  /* Make sure that the functions are called correctly */
package/src/helpers.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { tmpdir } from 'node:os'
2
+ import { join } from 'node:path'
3
+ import { mkdtempSync } from 'node:fs'
4
+
1
5
  import { assert, assertPromises } from './asserts'
2
6
  import { requireContext } from './async'
3
7
  import { Files } from './files'
@@ -65,6 +69,7 @@ export async function rmrf(directory: string): Promise<void> {
65
69
  assert(dir !== context.resolve('@'),
66
70
  `Cowardly refusing to wipe build file directory ${$p(dir)}`)
67
71
 
72
+ /* coverage ignore if */
68
73
  if (! resolveDirectory(dir)) {
69
74
  log.info('Directory', $p(dir), 'not found')
70
75
  return
@@ -159,6 +164,17 @@ export function isDirectory(...paths: [ string, ...string[] ]): AbsolutePath | u
159
164
  return resolveDirectory(path)
160
165
  }
161
166
 
167
+ /**
168
+ * Create a temporary directory and return its {@link AbsolutePath}.
169
+ *
170
+ * The directory will be rooted in `/tmp` or wherever `os.tmpdir()` decides.
171
+ */
172
+ export function mkdtemp(): AbsolutePath {
173
+ const prefix = join(tmpdir(), 'plugjs-')
174
+ const path = mkdtempSync(prefix)
175
+ return resolve(path)
176
+ }
177
+
162
178
  /**
163
179
  * Execute a command and await for its result from within a task.
164
180
  *
package/src/index.ts CHANGED
@@ -30,8 +30,8 @@ export * as pipe from './pipe'
30
30
  export * as utils from './utils'
31
31
 
32
32
  // Individual utilities
33
- export { log } from './logging'
34
- export { assert } from './asserts'
33
+ export { log, $ms, $p, $t, $blu, $cyn, $grn, $gry, $mgt, $red, $und, $wht, $ylw } from './logging'
34
+ export { assert, fail } from './asserts'
35
35
 
36
36
  // Our minimal exports
37
37
  export * from './build'
@@ -27,17 +27,17 @@ logOptions.on('changed', ({ defaultTaskName, colors, level }) => {
27
27
  /** The basic interface giving access to log facilities. */
28
28
  export interface Log {
29
29
  /** Log a `TRACE` message */
30
- trace(...args: [ any, ...any ]): this
30
+ trace(...args: [ any, ...any ]): void
31
31
  /** Log a `DEBUG` message */
32
- debug(...args: [ any, ...any ]): this
32
+ debug(...args: [ any, ...any ]): void
33
33
  /** Log an `INFO` message */
34
- info(...args: [ any, ...any ]): this
34
+ info(...args: [ any, ...any ]): void
35
35
  /** Log a `NOTICE` message */
36
- notice(...args: [ any, ...any ]): this
36
+ notice(...args: [ any, ...any ]): void
37
37
  /** Log a `WARNING` message */
38
- warn(...args: [ any, ...any ]): this
38
+ warn(...args: [ any, ...any ]): void
39
39
  /** Log an `ERROR` message */
40
- error(...args: [ any, ...any ]): this
40
+ error(...args: [ any, ...any ]): void
41
41
  /** Log an `ERROR` message and fail the build */
42
42
  fail(...args: [ any, ...any ]): never
43
43
  }
@@ -48,13 +48,13 @@ export interface Logger extends Log {
48
48
  level: LogLevel,
49
49
 
50
50
  /** Enter a sub-level of logging, increasing indent */
51
- enter(): this
51
+ enter(): void
52
52
  /** Enter a sub-level of logging, increasing indent */
53
- enter(evel: LogLevel, message: string): this
53
+ enter(evel: LogLevel, message: string): void
54
54
  /** Leave a sub-level of logging, decreasing indent */
55
- leave(): this
55
+ leave(): void
56
56
  /** Leave a sub-level of logging, decreasing indent */
57
- leave(level: LogLevel, message: string): this
57
+ leave(level: LogLevel, message: string): void
58
58
  /** Create a {@link Report} associated with this instance */
59
59
  report(title: string): Report
60
60
  }
@@ -88,8 +88,8 @@ class LoggerImpl implements Logger {
88
88
  private readonly _emitter: LogEmitter,
89
89
  ) {}
90
90
 
91
- private _emit(level: LogLevel, args: [ any, ...any ]): this {
92
- if (this._level > level) return this
91
+ private _emit(level: LogLevel, args: [ any, ...any ]): void {
92
+ if (this._level > level) return
93
93
 
94
94
  // The `BuildFailure` is a bit special case
95
95
  const params = args.filter((arg) => {
@@ -116,7 +116,7 @@ class LoggerImpl implements Logger {
116
116
  })
117
117
 
118
118
  // If there's nothing left to log, then we're done
119
- if (params.length === 0) return this
119
+ if (params.length === 0) return
120
120
 
121
121
  // Prepare our options for logging
122
122
  const options = { level, taskName: this._task, indent: this._indent }
@@ -131,7 +131,6 @@ class LoggerImpl implements Logger {
131
131
 
132
132
  // Emit our log lines and return
133
133
  this._emitter(options, params)
134
- return this
135
134
  }
136
135
 
137
136
  get level(): LogLevel {
@@ -142,28 +141,28 @@ class LoggerImpl implements Logger {
142
141
  this._level = level
143
142
  }
144
143
 
145
- trace(...args: [ any, ...any ]): this {
146
- return this._emit(TRACE, args)
144
+ trace(...args: [ any, ...any ]): void {
145
+ this._emit(TRACE, args)
147
146
  }
148
147
 
149
- debug(...args: [ any, ...any ]): this {
150
- return this._emit(DEBUG, args)
148
+ debug(...args: [ any, ...any ]): void {
149
+ this._emit(DEBUG, args)
151
150
  }
152
151
 
153
- info(...args: [ any, ...any ]): this {
154
- return this._emit(INFO, args)
152
+ info(...args: [ any, ...any ]): void {
153
+ this._emit(INFO, args)
155
154
  }
156
155
 
157
- notice(...args: [ any, ...any ]): this {
158
- return this._emit(NOTICE, args)
156
+ notice(...args: [ any, ...any ]): void {
157
+ this._emit(NOTICE, args)
159
158
  }
160
159
 
161
- warn(...args: [ any, ...any ]): this {
162
- return this._emit(WARN, args)
160
+ warn(...args: [ any, ...any ]): void {
161
+ this._emit(WARN, args)
163
162
  }
164
163
 
165
- error(...args: [ any, ...any ]): this {
166
- return this._emit(ERROR, args)
164
+ error(...args: [ any, ...any ]): void {
165
+ this._emit(ERROR, args)
167
166
  }
168
167
 
169
168
  fail(...args: [ any, ...any ]): never {
@@ -171,21 +170,20 @@ class LoggerImpl implements Logger {
171
170
  throw BuildFailure.fail()
172
171
  }
173
172
 
174
- enter(): this
175
- enter(level: LogLevel, message: string): this
176
- enter(...args: [] | [ level: LogLevel, message: string ]): this {
173
+ enter(): void
174
+ enter(level: LogLevel, message: string): void
175
+ enter(...args: [] | [ level: LogLevel, message: string ]): void {
177
176
  if (args.length) {
178
177
  const [ level, message ] = args
179
178
  this._stack.push({ level, message, indent: this._indent })
180
179
  }
181
180
 
182
181
  this._indent ++
183
- return this
184
182
  }
185
183
 
186
- leave(): this
187
- leave(level: LogLevel, message: string): this
188
- leave(...args: [] | [ level: LogLevel, message: string ]): this {
184
+ leave(): void
185
+ leave(level: LogLevel, message: string): void
186
+ leave(...args: [] | [ level: LogLevel, message: string ]): void {
189
187
  this._stack.pop()
190
188
  this._indent --
191
189
 
@@ -195,8 +193,6 @@ class LoggerImpl implements Logger {
195
193
  const [ level, message ] = args
196
194
  this._emit(level, [ message ])
197
195
  }
198
-
199
- return this
200
196
  }
201
197
 
202
198
  report(title: string): Report {
package/src/logging.ts CHANGED
@@ -29,38 +29,32 @@ export const log: LogFunction = ((): LogFunction => {
29
29
 
30
30
  /* Create a Logger wrapping the current logger */
31
31
  const wrapper: Log = {
32
- trace(...args: [ any, ...any ]): Log {
32
+ trace(...args: [ any, ...any ]): void {
33
33
  logger().trace(...args)
34
- return wrapper
35
34
  },
36
35
 
37
- debug(...args: [ any, ...any ]): Log {
36
+ debug(...args: [ any, ...any ]): void {
38
37
  logger().debug(...args)
39
- return wrapper
40
38
  },
41
39
 
42
- info(...args: [ any, ...any ]): Log {
40
+ info(...args: [ any, ...any ]): void {
43
41
  logger().info(...args)
44
- return wrapper
45
42
  },
46
43
 
47
- notice(...args: [ any, ...any ]): Log {
44
+ notice(...args: [ any, ...any ]): void {
48
45
  logger().notice(...args)
49
- return wrapper
50
46
  },
51
47
 
52
- warn(...args: [ any, ...any ]): Log {
48
+ warn(...args: [ any, ...any ]): void {
53
49
  logger().warn(...args)
54
- return wrapper
55
50
  },
56
51
 
57
- error(...args: [ any, ...any ]): Log {
52
+ error(...args: [ any, ...any ]): void {
58
53
  logger().error(...args)
59
- return wrapper
60
54
  },
61
55
 
62
56
  fail(...args: [ any, ...any ]): never {
63
- throw logger().fail(...args) // fail() returns never but ?!?!?!?!?
57
+ return logger().fail(...args)
64
58
  },
65
59
  }
66
60
 
package/src/paths.ts CHANGED
@@ -14,9 +14,8 @@ export type AbsolutePath = string & { __brand_absolute_path: never }
14
14
 
15
15
  /** Resolve a path into an {@link AbsolutePath} */
16
16
  export function resolveAbsolutePath(directory: AbsolutePath, ...paths: string[]): AbsolutePath {
17
- const resolved = resolve(directory, ...paths) as AbsolutePath
18
- assert(isAbsolute(resolved), `Path "${join(...paths)}" resolved in "${directory}" is not absolute`)
19
- return resolved
17
+ assertAbsolutePath(directory)
18
+ return resolve(directory, ...paths) as AbsolutePath
20
19
  }
21
20
 
22
21
  /**
@@ -32,8 +31,6 @@ export function resolveAbsolutePath(directory: AbsolutePath, ...paths: string[])
32
31
  * ```
33
32
  */
34
33
  export function resolveRelativeChildPath(directory: AbsolutePath, ...paths: string[]): string | undefined {
35
- assertAbsolutePath(directory)
36
-
37
34
  const abs = resolveAbsolutePath(directory, ...paths)
38
35
  const rel = relative(directory, abs)
39
36
  return (isAbsolute(rel) || (rel === '..') || rel.startsWith(`..${sep}`)) ? undefined : rel
@@ -43,7 +40,6 @@ export function resolveRelativeChildPath(directory: AbsolutePath, ...paths: stri
43
40
  * Asserts that a path is a relative path to the directory specified, failing
44
41
  * the build if it's not (see also {@link resolveRelativeChildPath}).
45
42
  */
46
-
47
43
  export function assertRelativeChildPath(directory: AbsolutePath, ...paths: string[]): string {
48
44
  const relative = resolveRelativeChildPath(directory, ...paths)
49
45
  assert(relative, `Path "${join(...paths)}" not relative to "${directory}"`)
@@ -76,6 +72,38 @@ export function getCurrentWorkingDirectory(): AbsolutePath {
76
72
  return cwd
77
73
  }
78
74
 
75
+ /**
76
+ * Return the _common_ path amongst all specified paths.
77
+ *
78
+ * While the first `path` _must_ be an {@link AbsolutePath}, all other `paths`
79
+ * can be _relative_ and will be resolved against the first `path`.
80
+ */
81
+ export function commonPath(path: AbsolutePath, ...paths: string[]): AbsolutePath {
82
+ assertAbsolutePath(path)
83
+
84
+ // Here the first path will be split into its components
85
+ // on win => [ 'C:', 'Windows', 'System32' ]
86
+ // on unx => [ '', 'usr'
87
+ const components = normalize(path).split(sep)
88
+
89
+ let length = components.length
90
+ for (const current of paths) {
91
+ const absolute = resolveAbsolutePath(path, current)
92
+ const parts = absolute.split(sep)
93
+ for (let i = 0; i < length; i++) {
94
+ if (components[i] !== parts[i]) {
95
+ length = i
96
+ break
97
+ }
98
+ }
99
+
100
+ assert(length, 'No common ancestors amongst paths')
101
+ }
102
+
103
+ const common = components.slice(0, length).join(sep)
104
+ assertAbsolutePath(common)
105
+ return common
106
+ }
79
107
 
80
108
  /* ========================================================================== *
81
109
  * MODULE RESOLUTION FUNCTIONS *
@@ -130,7 +158,7 @@ export function requireResolve(__fileurl: string, module: string): AbsolutePath
130
158
  // ... then delegate to the standard "require.resolve(...)"
131
159
  const url = pathToFileURL(file)
132
160
  const ext = extname(file)
133
- const checks = ext ? [ `${module}`, `${module}${ext}`, `${module}/index${ext}` ] : [ module ]
161
+ const checks = [ `${module}`, `${module}${ext}`, `${module}/index${ext}` ]
134
162
 
135
163
  for (const check of checks) {
136
164
  const resolved = fileURLToPath(new URL(check, url)) as AbsolutePath
@@ -147,39 +175,6 @@ export function requireResolve(__fileurl: string, module: string): AbsolutePath
147
175
  return required
148
176
  }
149
177
 
150
- /**
151
- * Return the _common_ path amongst all specified paths.
152
- *
153
- * While the first `path` _must_ be an {@link AbsolutePath}, all other `paths`
154
- * can be _relative_ and will be resolved against the first `path`.
155
- */
156
- export function commonPath(path: AbsolutePath, ...paths: string[]): AbsolutePath {
157
- assertAbsolutePath(path)
158
-
159
- // Here the first path will be split into its components
160
- // on win => [ 'C:', 'Windows', 'System32' ]
161
- // on unx => [ '', 'usr'
162
- const components = normalize(path).split(sep)
163
-
164
- let length = components.length
165
- for (const current of paths) {
166
- const absolute = resolveAbsolutePath(path, current)
167
- const parts = absolute.split(sep)
168
- for (let i = 0; i < length; i++) {
169
- if (components[i] !== parts[i]) {
170
- length = i
171
- break
172
- }
173
- }
174
-
175
- assert(length, 'No common ancestors amongst paths')
176
- }
177
-
178
- const common = components.slice(0, length).join(sep)
179
- assertAbsolutePath(common)
180
- return common
181
- }
182
-
183
178
  /* ========================================================================== *
184
179
  * FILE CHECKING FUNCTIONS *
185
180
  * ========================================================================== */
package/src/plugs/copy.ts CHANGED
@@ -9,8 +9,8 @@ import type { Context, PipeParameters, Plug } from '../pipe'
9
9
 
10
10
  /** Options for copying files */
11
11
  export interface CopyOptions {
12
- /** Whether to allow overwriting or not (default `false`). */
13
- overwrite?: boolean,
12
+ /** Whether to allow overwriting or not (default `fail`). */
13
+ overwrite?: 'overwrite' | 'fail' | 'skip',
14
14
  /** If specified, use this `mode` (octal string) when creating files. */
15
15
  mode?: string | number,
16
16
  /** If specified, use this `mode` (octal string) when creating directories. */
@@ -51,8 +51,8 @@ install('copy', class Copy implements Plug<Files> {
51
51
 
52
52
  async pipe(files: Files, context: Context): Promise<Files> {
53
53
  /* Destructure our options with some defaults and compute write flags */
54
- const { mode, dirMode, overwrite, rename = (s): string => s } = this._options
55
- const flags = overwrite ? fsConstants.COPYFILE_EXCL : 0
54
+ const { mode, dirMode, overwrite = 'fail', rename = (s): string => s } = this._options
55
+ const flags = overwrite === 'overwrite' ? 0 : fsConstants.COPYFILE_EXCL
56
56
  const dmode = parseMode(dirMode)
57
57
  const fmode = parseMode(mode)
58
58
 
@@ -88,7 +88,15 @@ install('copy', class Copy implements Plug<Files> {
88
88
 
89
89
  /* Actually _copy_ the file */
90
90
  context.log.trace(`Copying "${$p(absolute)}" to "${$p(target)}"`)
91
- await copyFile(absolute, target, flags)
91
+ try {
92
+ await copyFile(absolute, target, flags)
93
+ } catch (error: any) {
94
+ if ((error.code === 'EEXIST') && (overwrite === 'skip')) {
95
+ context.log.warn(`Not overwriting existing file ${$p(target)}`)
96
+ } else {
97
+ throw error
98
+ }
99
+ }
92
100
 
93
101
  /* Set the mode, if we need to */
94
102
  if (fmode !== undefined) {