Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions change/change-811de810-7c2d-4e6c-96b7-c671b4d7ffcb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"changes": [
{
"type": "minor",
"comment": "Fix bug where we have unnecessary dependency edges added to the graph when a package does not have the relevant target in scripts",
"packageName": "@lage-run/cli",
"email": "1581488+christiango@users.noreply.github.com",
"dependentChangeType": "patch"
},
{
"type": "minor",
"comment": "Fix bug where we have unnecessary dependency edges added to the graph when a package does not have the relevant target in scripts",
"packageName": "@lage-run/config",
"email": "1581488+christiango@users.noreply.github.com",
"dependentChangeType": "patch"
},
{
"type": "minor",
"comment": "Fix bug where we have unnecessary dependency edges added to the graph when a package does not have the relevant target in scripts",
"packageName": "@lage-run/target-graph",
"email": "1581488+christiango@users.noreply.github.com",
"dependentChangeType": "patch"
}
]
}
32 changes: 0 additions & 32 deletions change/change-af1026b1-217e-47f5-9bf6-56d614cb46c9.json

This file was deleted.

1 change: 1 addition & 0 deletions packages/cli/src/__tests__/createTargetGraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ async function createPackageTasks(tasks: string[], packageInfos: PackageInfos, p
dependencies: options.dependencies,
dependents: options.dependents && !options.to, // --to is a short hand for --scope + --no-dependents
enableTargetConfigMerging: true,
enablePhantomTargetOptimization: false,
ignore: options.ignore.concat(config.ignore),
pipeline: config.pipeline,
repoWideChanges: config.repoWideChanges,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/info/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export async function infoAction(options: InfoActionOptions, command: Command):
packageInfos,
priorities: config.priorities,
enableTargetConfigMerging: config.enableTargetConfigMerging,
enablePhantomTargetOptimization: config.enablePhantomTargetOptimization,
});

const scope = getFilteredPackages({
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/run/createTargetGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface CreateTargetGraphOptions {
packageInfos: PackageInfos;
priorities: Priority[];
enableTargetConfigMerging: boolean;
enablePhantomTargetOptimization: boolean;
}

function getChangedFiles(since: string, cwd: string) {
Expand All @@ -45,6 +46,7 @@ export async function createTargetGraph(options: CreateTargetGraphOptions): Prom
dependencies,
dependents,
enableTargetConfigMerging,
enablePhantomTargetOptimization,
since,
scope,
repoWideChanges,
Expand All @@ -56,7 +58,7 @@ export async function createTargetGraph(options: CreateTargetGraphOptions): Prom
priorities,
} = options;

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, enableTargetConfigMerging);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, enableTargetConfigMerging, enablePhantomTargetOptimization);

const packages = getFilteredPackages({
root,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/run/runAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export async function runAction(options: RunOptions, command: Command): Promise<
packageInfos,
priorities: config.priorities,
enableTargetConfigMerging: config.enableTargetConfigMerging,
enablePhantomTargetOptimization: config.enablePhantomTargetOptimization,
});

validateTargetGraph(targetGraph, allowNoTargetRuns);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/run/watchAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function watchAction(options: RunOptions, command: Command): Promis
packageInfos,
priorities: config.priorities,
enableTargetConfigMerging: config.enableTargetConfigMerging,
enablePhantomTargetOptimization: config.enablePhantomTargetOptimization,
});

// Make sure we do not attempt writeRemoteCache in watch mode
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/server/lageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async function createInitializedPromise({ cwd, logger, serverControls, nodeArg,
packageInfos,
priorities: config.priorities,
enableTargetConfigMerging: config.enableTargetConfigMerging,
enablePhantomTargetOptimization: config.enablePhantomTargetOptimization,
});

const targetHasher = new TargetHasher({
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/getConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export async function getConfig(cwd: string): Promise<ConfigOptions> {
concurrency: config?.concurrency ?? availableParallelism,
allowNoTargetRuns: config?.allowNoTargetRuns ?? false,
enableTargetConfigMerging: config?.enableTargetConfigMerging ?? false,
enablePhantomTargetOptimization: config?.enablePhantomTargetOptimization ?? false,
reporters: config?.reporters ?? {},
};
}
7 changes: 7 additions & 0 deletions packages/config/src/types/ConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ export interface ConfigOptions {
*/
enableTargetConfigMerging: boolean;

/**
* Enables phantom target optimization. When enabled, targets created for packages that don't actually define
* the corresponding npm script are excluded from transitive (`^^`) dependency expansion.
* This prevents unwanted cross-package dependency chains when phantom targets are later removed.
*/
enablePhantomTargetOptimization: boolean;

/**
* Custom reporters that can be referenced by name in the --reporter CLI flag.
* The key is the reporter name, and the value is the path to the reporter module.
Expand Down
47 changes: 47 additions & 0 deletions packages/e2e-tests/src/transitiveTaskDeps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,51 @@ describe("transitive task deps test", () => {

await repo.cleanup();
});

it("reproduce bug where transitive dependencies were being added that were not necessary", async () => {
// Simulates a bug from an internal repo that implemented isolated declarations for some packages
const repo = new Monorepo("transitiveDeps-isolated-declarations-info");

await repo.init();

// This repo has some packages that have isolated declarations configured and some that do not.
// For the packages that do not have isolatedDeclarations enabled, we have a dummy emitDeclarations task defined for them whose sole purpose is to make sure we block on those package's typecheck step for d.ts emission
// For packages that do have isolatedDeclarations enabled, we emit the d.ts during transpile so we omit the emitDeclarations task.
await repo.setLageConfig(`module.exports = {
pipeline: {
transpile: [],
emitDeclarations: ["typecheck"],
typecheck: ["^^emitDeclarations", "transpile", "^^transpile"]
},
}`);

await repo.addPackage("dep", [], {
transpile: "echo dep:transpile",
typecheck: "echo dep:typecheck",
});

await repo.addPackage("app", ["dep"], {
transpile: "echo app:transpile",
typecheck: "echo app:typecheck",
emitDeclarations: "echo app:emitDeclarations",
});

await repo.install();

const results = await repo.run("writeInfo", ["typecheck", "--scope", "app"]);

const output = results.stdout + results.stderr;
const jsonOutput = parseNdJson(output);
const packageTasks = jsonOutput[0].data.packageTasks;

const appTypecheckTask = packageTasks.find(({ id }: { id: string }) => id === "app#typecheck");
expect(appTypecheckTask).toBeTruthy();

expect(appTypecheckTask.dependencies).toContain("app#transpile");

// This was the bug, we'd end up with app depending on the typecheck step of the dependency which does not have an emitDeclarations step
expect(appTypecheckTask.dependencies).not.toContain("dep#typecheck");

await repo.cleanup();
});
});
12 changes: 10 additions & 2 deletions packages/target-graph/src/WorkspaceTargetGraphBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export class WorkspaceTargetGraphBuilder {
constructor(
root: string,
private packageInfos: PackageInfos,
private enableTargetConfigMerging: boolean
private enableTargetConfigMerging: boolean,
private enablePhantomTargetOptimization: boolean
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ecraig12345 - Let me know if any of these kinds of changes would be breaking changes and if I should use default initializers. I just copied the pattern from enableTargetConfigMerging

) {
this.dependencyMap = createDependencyMap(packageInfos, { withDevDependencies: true, withPeerDependencies: false });
this.graphBuilder = new TargetGraphBuilder();
Expand Down Expand Up @@ -95,8 +96,10 @@ export class WorkspaceTargetGraphBuilder {
this.processStagedConfig(target, config, changedFiles);
} else {
const packages = Object.keys(this.packageInfos);

for (const packageName of packages) {
const task = id;

const targetConfig = this.determineFinalTargetConfig(getTargetId(packageName, task), config);
const target = this.targetFactory.createPackageTarget(packageName!, task, targetConfig);
this.graphBuilder.addTarget(target);
Expand Down Expand Up @@ -207,7 +210,12 @@ export class WorkspaceTargetGraphBuilder {
priorities?: { package?: string; task: string; priority: number }[]
): Promise<TargetGraph> {
// Expands the dependency specs from the target definitions
const fullDependencies = expandDepSpecs(this.graphBuilder.targets, this.dependencyMap);
const fullDependencies = expandDepSpecs(
this.graphBuilder.targets,
this.dependencyMap,
this.packageInfos,
this.enablePhantomTargetOptimization
);

for (const [from, to] of fullDependencies) {
this.graphBuilder.addDependency(from, to);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("workspace target graph builder", () => {
b: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("build", {
dependsOn: ["^build"],
});
Expand Down Expand Up @@ -80,7 +80,7 @@ describe("workspace target graph builder", () => {
b: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("test");
await builder.addTargetConfig("lint");

Expand Down Expand Up @@ -119,7 +119,7 @@ describe("workspace target graph builder", () => {
c: ["b"],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);

await builder.addTargetConfig("build", {
dependsOn: ["^build"],
Expand Down Expand Up @@ -161,7 +161,7 @@ describe("workspace target graph builder", () => {
c: ["b"],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);

await builder.addTargetConfig("build", {
dependsOn: ["^build"],
Expand Down Expand Up @@ -195,7 +195,7 @@ describe("workspace target graph builder", () => {
c: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);

await builder.addTargetConfig("bundle", {
dependsOn: ["^^transpile"],
Expand Down Expand Up @@ -240,7 +240,7 @@ describe("workspace target graph builder", () => {
common: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);

await builder.addTargetConfig("build", {
dependsOn: ["common#copy", "^build"],
Expand Down Expand Up @@ -296,7 +296,7 @@ describe("workspace target graph builder", () => {
b: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("build", {
dependsOn: ["^build", "#global:task"],
});
Expand Down Expand Up @@ -345,7 +345,7 @@ describe("workspace target graph builder", () => {
b: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("build", {
dependsOn: ["^build", "#global:task"],
});
Expand Down Expand Up @@ -374,7 +374,7 @@ describe("workspace target graph builder", () => {
b: [],
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false);
const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("build", {
dependsOn: ["^build"],
});
Expand Down Expand Up @@ -402,4 +402,77 @@ describe("workspace target graph builder", () => {
]
`);
});

it("should not create phantom transitive deps for packages missing a script", async () => {
const root = "/repos/a";

// "app" has emitDeclarations in scripts, "dep" does not
const packageInfos = createPackageInfoWithScripts({
app: { deps: ["dep"], scripts: ["transpile", "typecheck", "emitDeclarations"] },
dep: { deps: [], scripts: ["transpile", "typecheck"] },
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("transpile");
await builder.addTargetConfig("emitDeclarations", {
dependsOn: ["typecheck"],
});
await builder.addTargetConfig("typecheck", {
dependsOn: ["^^emitDeclarations", "transpile", "^^transpile"],
});

const targetGraph = await builder.build(["typecheck"], ["app"]);
const graph = getGraphFromTargets(targetGraph);

// app#typecheck should depend on app#transpile (same-package dep)
expect(graph).toContainEqual(["app#transpile", "app#typecheck"]);

// app#typecheck should depend on dep#transpile (via ^^transpile, dep has the script)
expect(graph).toContainEqual(["dep#transpile", "app#typecheck"]);

// app#typecheck should NOT depend on dep#typecheck — dep doesn't have emitDeclarations,
// so the phantom dep#emitDeclarations should not create a transitive link
expect(graph).not.toContainEqual(["dep#typecheck", "app#typecheck"]);
});

it("should preserve ^^ deps for non-npmScript typed targets even without scripts entry", async () => {
const root = "/repos/a";

// "dep" doesn't have "customTask" in scripts, but it's configured as a worker type
const packageInfos = createPackageInfoWithScripts({
app: { deps: ["dep"], scripts: ["build"] },
dep: { deps: [], scripts: ["build"] },
});

const builder = new WorkspaceTargetGraphBuilder(root, packageInfos, false, false);
await builder.addTargetConfig("customTask", {
type: "worker",
});
await builder.addTargetConfig("build", {
dependsOn: ["^^customTask"],
});

const targetGraph = await builder.build(["build"], ["app"]);
const graph = getGraphFromTargets(targetGraph);

// app#build should depend on dep#customTask even though dep doesn't have it in scripts,
// because the target has an explicit non-npmScript type ("worker")
expect(graph).toContainEqual(["dep#customTask", "app#build"]);
});
});

function createPackageInfoWithScripts(packages: { [id: string]: { deps: string[]; scripts: string[] } }) {
const packageInfos: PackageInfos = {};
Object.keys(packages).forEach((id) => {
const { deps, scripts } = packages[id];
packageInfos[id] = {
packageJsonPath: `/path/to/${id}/package.json`,
name: id,
version: "1.0.0",
dependencies: deps.reduce((acc, dep) => ({ ...acc, [dep]: "*" }), {}),
devDependencies: {},
scripts: scripts.reduce((acc, script) => ({ ...acc, [script]: `echo ${script}` }), {}),
};
});
return packageInfos;
}
Loading
Loading