| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as path from 'node:path'; |
| |
| export class GnLabel { |
| dirPath: string; // e.g. "front_end/path/to" |
| name: string; // e.g. "bundle" |
| |
| constructor(dirPath: string, name: string) { |
| this.dirPath = dirPath; |
| this.name = name; |
| } |
| |
| static resolveDeclaredDep( |
| dep: string, |
| currentDir: string, |
| rootDir: string, |
| ): string { |
| let targetPart = ''; |
| let pathPart = dep; |
| |
| if (dep.startsWith(':')) { |
| pathPart = '.'; |
| targetPart = dep; |
| } else { |
| const colonIndex = dep.indexOf(':'); |
| if (colonIndex !== -1) { |
| pathPart = dep.substring(0, colonIndex); |
| targetPart = dep.substring(colonIndex); |
| } |
| } |
| |
| const isAbsolute = pathPart.startsWith('//'); |
| const baseDir = isAbsolute ? rootDir : currentDir; |
| const cleanPath = isAbsolute ? pathPart.slice('//'.length) : pathPart; |
| |
| const absDir = path.resolve(baseDir, cleanPath); |
| const relFromRoot = path.relative(rootDir, absDir); |
| const posixRelFromRoot = relFromRoot.split(path.sep).join(path.posix.sep); |
| |
| const target = targetPart || `:${path.basename(absDir)}`; |
| return `//${posixRelFromRoot}${target}`; |
| } |
| |
| /** |
| * Parses a full GN label like //front_end/path/to:target |
| * or //front_end/path/to |
| */ |
| static parse(label: string): GnLabel|null { |
| if (!label.startsWith('//')) { |
| return null; |
| } |
| const colonIndex = label.indexOf(':'); |
| if (colonIndex === -1) { |
| // e.g. //front_end/path/to |
| // The target name defaults to the last part of the path |
| const dirPath = label.substring(2); |
| const name = path.basename(dirPath); |
| return new GnLabel(dirPath, name); |
| } |
| const dirPath = label.substring(2, colonIndex); |
| const name = label.substring(colonIndex + 1); |
| return new GnLabel(dirPath, name); |
| } |
| |
| toString(): string { |
| return `//${this.dirPath}:${this.name}`; |
| } |
| |
| get bundleLabel(): string { |
| return `//${this.dirPath}:bundle`; |
| } |
| |
| get dirName(): string { |
| return path.basename(this.dirPath); |
| } |
| |
| /** |
| * This heuristic works as our main target templates |
| * always create a label that matches the directory name. |
| * |
| * We only check this if we need to update a target that |
| * is part of the same BUILD.gn file as the imported file. |
| * |
| * @example |
| * front_end/panels/ai_assistance/BUILD.gn |
| * devtools_ui_module("ai_assistance") |
| */ |
| get isConsumerTarget(): boolean { |
| return this.name !== this.dirName; |
| } |
| |
| toRelativeDep(currentDir: string, rootDir: string): string { |
| const absTargetDir = path.join(rootDir, this.dirPath); |
| const relTopDir = path.relative(currentDir, absTargetDir); |
| const relPath = relTopDir.split(path.sep).join(path.posix.sep); |
| |
| if (relPath === '') { |
| return `:${this.name}`; |
| } |
| |
| if (this.dirName === this.name) { |
| return relPath; |
| } |
| |
| return `${relPath}:${this.name}`; |
| } |
| |
| /** |
| * Normalizes a dependency label (absolute, relative, or shorthand) |
| * into a formatted relative GN dependency from the perspective of currentDir. |
| */ |
| static formatRelativeDep( |
| dep: string, |
| currentDir: string, |
| rootDir: string, |
| ): string { |
| const resolvedAbsolute = dep.startsWith('//') ? dep : GnLabel.resolveDeclaredDep(dep, currentDir, rootDir); |
| const parsed = GnLabel.parse(resolvedAbsolute); |
| if (!parsed) { |
| return dep; |
| } |
| return parsed.toRelativeDep(currentDir, rootDir); |
| } |
| } |