Skip to content

Commit 4ab3dfa

Browse files
Merge branch 'main' of https://github.com/microsoft/FluidFramework into ddsfuzz/rollback
2 parents 56e0a19 + 34d54e3 commit 4ab3dfa

File tree

40 files changed

+818
-457
lines changed

40 files changed

+818
-457
lines changed

build-tools/packages/build-tools/src/fluidBuild/buildGraph.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
TaskDefinition,
2222
TaskDefinitions,
2323
TaskDefinitionsOnDisk,
24+
TaskFileDependencies,
2425
getDefaultTaskDefinition,
2526
getTaskDefinitions,
2627
normalizeGlobalTaskDefinitions,
@@ -158,6 +159,7 @@ export class BuildPackage {
158159
before: [],
159160
children: [],
160161
after: [],
162+
files: undefined,
161163
};
162164
}
163165
return undefined;
@@ -172,16 +174,27 @@ export class BuildPackage {
172174
this.targetTasks.set(taskName, task);
173175
return task;
174176
}
175-
return this.createScriptTask(taskName, pendingInitDep);
177+
return this.createScriptTask(taskName, pendingInitDep, config?.files);
176178
}
177179

178-
private createScriptTask(taskName: string, pendingInitDep: Task[]) {
180+
private createScriptTask(
181+
taskName: string,
182+
pendingInitDep: Task[],
183+
files: TaskFileDependencies | undefined,
184+
) {
179185
const command = this.pkg.getScript(taskName);
180186
if (command !== undefined && !command.startsWith("fluid-build ")) {
181187
// Find the script task (without the lifecycle task)
182188
let scriptTask = this.scriptTasks.get(taskName);
183189
if (scriptTask === undefined) {
184-
scriptTask = TaskFactory.Create(this, command, this.context, pendingInitDep, taskName);
190+
scriptTask = TaskFactory.Create(
191+
this,
192+
command,
193+
this.context,
194+
pendingInitDep,
195+
taskName,
196+
files,
197+
);
185198
pendingInitDep.push(scriptTask);
186199
this.tasks.push(scriptTask);
187200
this.scriptTasks.set(taskName, scriptTask);
@@ -221,7 +234,14 @@ export class BuildPackage {
221234
throw new Error(`${this.pkg.nameColored}: '${taskName}' must be a script task`);
222235
}
223236

224-
const task = TaskFactory.Create(this, command, this.context, pendingInitDep, taskName);
237+
const task = TaskFactory.Create(
238+
this,
239+
command,
240+
this.context,
241+
pendingInitDep,
242+
taskName,
243+
config?.files,
244+
);
225245
pendingInitDep.push(task);
226246
this.tasks.push(task);
227247
this.scriptTasks.set(taskName, task);
@@ -254,7 +274,7 @@ export class BuildPackage {
254274
return existing;
255275
}
256276

257-
return this.createScriptTask(taskName, pendingInitDep);
277+
return this.createScriptTask(taskName, pendingInitDep, config?.files);
258278
}
259279

260280
public getDependsOnTasks(task: Task, taskName: string, pendingInitDep: Task[]) {

build-tools/packages/build-tools/src/fluidBuild/fluidBuildConfig.ts

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import type { InterdependencyRange } from "@fluid-tools/version-tools";
7-
import { TaskDefinitionsOnDisk } from "./fluidTaskDefinitions";
7+
import { TaskDefinitionsOnDisk, TaskFileDependencies } from "./fluidTaskDefinitions";
88

99
/**
1010
* The version of the fluidBuild configuration currently used.
@@ -99,37 +99,7 @@ export interface IFluidBuildDirs {
9999
* Note that by default, gitignored files are treated differently for input globs vs. output globs. This can be
100100
* changed using the `gitignore` property on the task. See the documentation for that property for details.
101101
*/
102-
export interface DeclarativeTask {
103-
/**
104-
* An array of globs that will be used to identify input files for the task. The globs are interpreted relative to the
105-
* package the task belongs to.
106-
*
107-
* By default, inputGlobs **will not** match files ignored by git. This can be changed using the `gitignore` property
108-
* on the task. See the documentation for that property for details.
109-
*/
110-
inputGlobs: string[];
111-
112-
/**
113-
* An array of globs that will be used to identify output files for the task. The globs are interpreted relative to
114-
* the package the task belongs to.
115-
*
116-
* By default, outputGlobs **will** match files ignored by git, because build output is often gitignored. This can be
117-
* changed using the `gitignore` property on the task. See the documentation for that property for details.
118-
*/
119-
outputGlobs: string[];
120-
121-
/**
122-
* Configures how gitignore rules are applied. "input" applies gitignore rules to the input, "output" applies them to
123-
* the output, and including both values will apply the gitignore rules to both the input and output globs.
124-
*
125-
* The default value, `["input"]` applies gitignore rules to the input, but not the output. This is the right behavior
126-
* for many tasks since most tasks use source-controlled files as input but generate gitignored build output. However,
127-
* it can be adjusted on a per-task basis depending on the needs of the task.
128-
*
129-
* @defaultValue `["input"]`
130-
*/
131-
gitignore?: GitIgnoreSetting;
132-
}
102+
export interface DeclarativeTask extends TaskFileDependencies {}
133103

134104
export type GitIgnoreSetting = ("input" | "output")[];
135105

build-tools/packages/build-tools/src/fluidBuild/fluidTaskDefinitions.ts

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,50 @@ type TaskDependency =
4646

4747
export type TaskDependencies = readonly TaskDependency[];
4848

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+
4993
export interface TaskConfig {
5094
/**
5195
* 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 {
93137
* It can be used as an alias to a group of tasks.
94138
*/
95139
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;
96148
}
97149

98150
/**
@@ -125,14 +177,22 @@ const makeClonedOrEmptyArray = <T>(value: readonly T[] | undefined): T[] =>
125177
*/
126178
function getFullTaskConfig(config: TaskConfigOnDisk): MutableTaskConfig {
127179
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+
};
129188
} else {
130189
return {
131190
dependsOn: makeClonedOrEmptyArray(config.dependsOn),
132191
script: config.script ?? true,
133192
before: makeClonedOrEmptyArray(config.before),
134193
children: [],
135194
after: makeClonedOrEmptyArray(config.after),
195+
files: structuredClone(config.files),
136196
};
137197
}
138198
}
@@ -173,13 +233,15 @@ const defaultTaskDefinition = {
173233
children: [],
174234
after: ["^*"], // TODO: include "*" so the user configured task will run first, but we need to make sure it doesn't cause circular dependency first
175235
isDefault: true, // only propagate to unnamed sub tasks if it is a group task
236+
files: undefined,
176237
} as const satisfies TaskDefinition;
177238
const defaultCleanTaskDefinition = {
178239
dependsOn: [],
179240
script: true,
180241
before: ["*"], // clean are ran before all the tasks, add a week dependency.
181242
children: [],
182243
after: [],
244+
files: undefined,
183245
} as const satisfies TaskDefinition;
184246

185247
const detectInvalid = (
@@ -213,6 +275,9 @@ export function normalizeGlobalTaskDefinitions(
213275
`Non-script global task definition '${name}' cannot have 'before' or 'after'`,
214276
);
215277
}
278+
if (full.files !== undefined) {
279+
throw new Error(`Non-script global task definition '${name}' cannot have 'files'`);
280+
}
216281
}
217282
detectInvalid(
218283
full.dependsOn,
@@ -235,13 +300,30 @@ export function normalizeGlobalTaskDefinitions(
235300
"after",
236301
true,
237302
);
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+
}
238320
taskDefinitions[name] = full;
239321
}
240322
}
241323
return taskDefinitions;
242324
}
243325

244-
function expandDotDotDot(config: readonly string[], inherited: readonly string[]) {
326+
function expandDotDotDot(config: readonly string[], inherited?: readonly string[]) {
245327
const expanded = config.filter((value) => value !== "...");
246328
if (inherited !== undefined && expanded.length !== config.length) {
247329
return expanded.concat(inherited);
@@ -327,6 +409,7 @@ export function getTaskDefinitions(
327409
// `children` are not inherited from the global task definitions (which should always be empty anyway)
328410
children: [],
329411
after: globalTaskDefinition.after.filter(globalAllowExpansionsStar),
412+
files: globalTaskDefinition.files,
330413
};
331414
}
332415

@@ -340,20 +423,37 @@ export function getTaskDefinitions(
340423
if (script === undefined) {
341424
throw new Error(`Script not found for task definition '${name}'`);
342425
} 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+
);
344429
}
345430
} else {
346431
if (full.before.length !== 0 || full.after.length !== 0) {
347432
throw new Error(
348433
`Non-script task definition '${name}' cannot have 'before' or 'after'`,
349434
);
435+
} else if (full.files !== undefined) {
436+
throw new Error(`Non-script task definition '${name}' cannot have 'files'`);
350437
}
351438
}
352439

353440
const currentTaskConfig = taskDefinitions[name];
354441
full.dependsOn = expandDotDotDot(full.dependsOn, currentTaskConfig?.dependsOn);
355442
full.before = expandDotDotDot(full.before, currentTaskConfig?.before);
356443
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+
}
357457
taskDefinitions[name] = full;
358458
}
359459
}
@@ -397,6 +497,7 @@ export function getTaskDefinitions(
397497
children: directlyCalledScripts,
398498
after: [],
399499
script: true,
500+
files: undefined,
400501
};
401502
} else {
402503
// Confirm `children` is not specified in the manual task specifications

build-tools/packages/build-tools/src/fluidBuild/tasks/leaf/declarativeTask.ts

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import {
1010
type GitIgnoreSetting,
1111
gitignoreDefaultValue,
1212
} from "../../fluidBuildConfig";
13-
import type { TaskHandlerFunction } from "../taskHandlers";
14-
import { LeafTask, LeafWithGlobInputOutputDoneFileTask } from "./leafTask";
13+
import { LeafWithGlobInputOutputDoneFileTask } from "./leafTask";
1514

16-
class DeclarativeTaskHandler extends LeafWithGlobInputOutputDoneFileTask {
15+
export class DeclarativeLeafTask extends LeafWithGlobInputOutputDoneFileTask {
1716
constructor(
1817
node: BuildPackage,
1918
command: string,
@@ -35,31 +34,15 @@ class DeclarativeTaskHandler extends LeafWithGlobInputOutputDoneFileTask {
3534
return this.taskDefinition.gitignore ?? gitignoreDefaultValue;
3635
}
3736

38-
protected async getInputGlobs(): Promise<string[]> {
37+
protected override get includeLockFiles(): boolean {
38+
return this.taskDefinition.includeLockFiles ?? super.includeLockFiles;
39+
}
40+
41+
protected async getInputGlobs(): Promise<readonly string[]> {
3942
return this.taskDefinition.inputGlobs;
4043
}
4144

42-
protected async getOutputGlobs(): Promise<string[]> {
45+
protected async getOutputGlobs(): Promise<readonly string[]> {
4346
return this.taskDefinition.outputGlobs;
4447
}
4548
}
46-
47-
/**
48-
* Generates a task handler for a declarative task dynamically.
49-
*
50-
* @param taskDefinition - The declarative task definition.
51-
* @returns a function that can be used to instantiate a LeafTask to handle a task.
52-
*/
53-
export function createDeclarativeTaskHandler(
54-
taskDefinition: DeclarativeTask,
55-
): TaskHandlerFunction {
56-
const handler: TaskHandlerFunction = (
57-
node: BuildPackage,
58-
command: string,
59-
context: BuildContext,
60-
taskName?: string,
61-
): LeafTask => {
62-
return new DeclarativeTaskHandler(node, command, context, taskName, taskDefinition);
63-
};
64-
return handler;
65-
}

0 commit comments

Comments
 (0)