@@ -46,6 +46,50 @@ type TaskDependency =
46
46
47
47
export type TaskDependencies = readonly TaskDependency [ ] ;
48
48
49
+ export type GitIgnoreSetting = ( "input" | "output" ) [ ] ;
50
+ export interface TaskFileDependencies {
51
+ /**
52
+ * An array of globs that will be used to identify input files for the task. The globs are interpreted relative to the
53
+ * package the task belongs to.
54
+ *
55
+ * By default, inputGlobs **will not** match files ignored by git. This can be changed using the `gitignore` property
56
+ * on the task. See the documentation for that property for details.
57
+ */
58
+ inputGlobs : readonly string [ ] ;
59
+
60
+ /**
61
+ * An array of globs that will be used to identify output files for the task. The globs are interpreted relative to
62
+ * the package the task belongs to.
63
+ *
64
+ * By default, outputGlobs **will** match files ignored by git, because build output is often gitignored. This can be
65
+ * changed using the `gitignore` property on the task. See the documentation for that property for details.
66
+ */
67
+ outputGlobs : readonly string [ ] ;
68
+
69
+ /**
70
+ * Configures how gitignore rules are applied. "input" applies gitignore rules to the input, "output" applies them to
71
+ * the output, and including both values will apply the gitignore rules to both the input and output globs.
72
+ *
73
+ * The default value, `["input"]` applies gitignore rules to the input, but not the output. This is the right behavior
74
+ * for many tasks since most tasks use source-controlled files as input but generate gitignored build output. However,
75
+ * it can be adjusted on a per-task basis depending on the needs of the task.
76
+ *
77
+ * @defaultValue `["input"]`
78
+ */
79
+ gitignore ?: GitIgnoreSetting ;
80
+
81
+ /**
82
+ * Specify whether the task will depend on the package/workspace lock file, this task will be rebuilt if the lock file
83
+ * is changed. Provides an economical but broad way to ensure rebuild when tools or package dependencies changes.
84
+ *
85
+ * Default is true, and it is equivalent to putting the lock file in the `inputGlobs`.
86
+ *
87
+ * For fine-grained control, use `inputGlobs` and `outputGlobs` to specify the dependencies in `node_modules`
88
+ * and set this to false.
89
+ */
90
+ includeLockFiles ?: boolean ;
91
+ }
92
+
49
93
export interface TaskConfig {
50
94
/**
51
95
* Task dependencies as a plain string array. Matched task will be scheduled to run before the current task.
@@ -93,6 +137,14 @@ export interface TaskConfig {
93
137
* It can be used as an alias to a group of tasks.
94
138
*/
95
139
readonly script : boolean ;
140
+
141
+ /**
142
+ * For leaf tasks (i.e. tasks that have a single executable command).
143
+ * Specify the input/output files the task depends on for incremental check.
144
+ * Can used for unknown executable isn't a known tool (i.e. non-incremental), or to override behavior of known tools.
145
+ * Error if this is specified for a non-leaf task (e.g. npm run, concurrently, multiple command with '&&').
146
+ */
147
+ readonly files : TaskFileDependencies | undefined ;
96
148
}
97
149
98
150
/**
@@ -125,14 +177,22 @@ const makeClonedOrEmptyArray = <T>(value: readonly T[] | undefined): T[] =>
125
177
*/
126
178
function getFullTaskConfig ( config : TaskConfigOnDisk ) : MutableTaskConfig {
127
179
if ( isTaskDependencies ( config ) ) {
128
- return { dependsOn : [ ...config ] , script : true , before : [ ] , children : [ ] , after : [ ] } ;
180
+ return {
181
+ dependsOn : [ ...config ] ,
182
+ script : true ,
183
+ before : [ ] ,
184
+ children : [ ] ,
185
+ after : [ ] ,
186
+ files : undefined ,
187
+ } ;
129
188
} else {
130
189
return {
131
190
dependsOn : makeClonedOrEmptyArray ( config . dependsOn ) ,
132
191
script : config . script ?? true ,
133
192
before : makeClonedOrEmptyArray ( config . before ) ,
134
193
children : [ ] ,
135
194
after : makeClonedOrEmptyArray ( config . after ) ,
195
+ files : structuredClone ( config . files ) ,
136
196
} ;
137
197
}
138
198
}
@@ -173,13 +233,15 @@ const defaultTaskDefinition = {
173
233
children : [ ] ,
174
234
after : [ "^*" ] , // TODO: include "*" so the user configured task will run first, but we need to make sure it doesn't cause circular dependency first
175
235
isDefault : true , // only propagate to unnamed sub tasks if it is a group task
236
+ files : undefined ,
176
237
} as const satisfies TaskDefinition ;
177
238
const defaultCleanTaskDefinition = {
178
239
dependsOn : [ ] ,
179
240
script : true ,
180
241
before : [ "*" ] , // clean are ran before all the tasks, add a week dependency.
181
242
children : [ ] ,
182
243
after : [ ] ,
244
+ files : undefined ,
183
245
} as const satisfies TaskDefinition ;
184
246
185
247
const detectInvalid = (
@@ -213,6 +275,9 @@ export function normalizeGlobalTaskDefinitions(
213
275
`Non-script global task definition '${ name } ' cannot have 'before' or 'after'` ,
214
276
) ;
215
277
}
278
+ if ( full . files !== undefined ) {
279
+ throw new Error ( `Non-script global task definition '${ name } ' cannot have 'files'` ) ;
280
+ }
216
281
}
217
282
detectInvalid (
218
283
full . dependsOn ,
@@ -235,13 +300,30 @@ export function normalizeGlobalTaskDefinitions(
235
300
"after" ,
236
301
true ,
237
302
) ;
303
+ if ( full . files !== undefined ) {
304
+ // "..." not allowed because the global config inherits from nothing.
305
+ detectInvalid (
306
+ full . files . inputGlobs ,
307
+ ( value ) => value === "..." ,
308
+ name ,
309
+ "files.inputGlobs" ,
310
+ true ,
311
+ ) ;
312
+ detectInvalid (
313
+ full . files . outputGlobs ,
314
+ ( value ) => value === "..." ,
315
+ name ,
316
+ "files.outputGlobs" ,
317
+ true ,
318
+ ) ;
319
+ }
238
320
taskDefinitions [ name ] = full ;
239
321
}
240
322
}
241
323
return taskDefinitions ;
242
324
}
243
325
244
- function expandDotDotDot ( config : readonly string [ ] , inherited : readonly string [ ] ) {
326
+ function expandDotDotDot ( config : readonly string [ ] , inherited ? : readonly string [ ] ) {
245
327
const expanded = config . filter ( ( value ) => value !== "..." ) ;
246
328
if ( inherited !== undefined && expanded . length !== config . length ) {
247
329
return expanded . concat ( inherited ) ;
@@ -327,6 +409,7 @@ export function getTaskDefinitions(
327
409
// `children` are not inherited from the global task definitions (which should always be empty anyway)
328
410
children : [ ] ,
329
411
after : globalTaskDefinition . after . filter ( globalAllowExpansionsStar ) ,
412
+ files : globalTaskDefinition . files ,
330
413
} ;
331
414
}
332
415
@@ -340,20 +423,37 @@ export function getTaskDefinitions(
340
423
if ( script === undefined ) {
341
424
throw new Error ( `Script not found for task definition '${ name } '` ) ;
342
425
} else if ( script . startsWith ( "fluid-build " ) ) {
343
- throw new Error ( `Script task should not invoke 'fluid-build' in '${ name } '` ) ;
426
+ throw new Error (
427
+ `Script task should not invoke 'fluid-build' in '${ name } '. Did you forget to set 'script: false' in the task definition?` ,
428
+ ) ;
344
429
}
345
430
} else {
346
431
if ( full . before . length !== 0 || full . after . length !== 0 ) {
347
432
throw new Error (
348
433
`Non-script task definition '${ name } ' cannot have 'before' or 'after'` ,
349
434
) ;
435
+ } else if ( full . files !== undefined ) {
436
+ throw new Error ( `Non-script task definition '${ name } ' cannot have 'files'` ) ;
350
437
}
351
438
}
352
439
353
440
const currentTaskConfig = taskDefinitions [ name ] ;
354
441
full . dependsOn = expandDotDotDot ( full . dependsOn , currentTaskConfig ?. dependsOn ) ;
355
442
full . before = expandDotDotDot ( full . before , currentTaskConfig ?. before ) ;
356
443
full . after = expandDotDotDot ( full . after , currentTaskConfig ?. after ) ;
444
+ const currentFiles = currentTaskConfig ?. files ;
445
+ if ( full . files === undefined ) {
446
+ full . files = currentTaskConfig ?. files ;
447
+ } else {
448
+ full . files . inputGlobs = expandDotDotDot (
449
+ full . files . inputGlobs ,
450
+ currentFiles ?. inputGlobs ,
451
+ ) ;
452
+ full . files . outputGlobs = expandDotDotDot (
453
+ full . files . outputGlobs ,
454
+ currentFiles ?. outputGlobs ,
455
+ ) ;
456
+ }
357
457
taskDefinitions [ name ] = full ;
358
458
}
359
459
}
@@ -397,6 +497,7 @@ export function getTaskDefinitions(
397
497
children : directlyCalledScripts ,
398
498
after : [ ] ,
399
499
script : true ,
500
+ files : undefined ,
400
501
} ;
401
502
} else {
402
503
// Confirm `children` is not specified in the manual task specifications
0 commit comments