backstage
176 строк · 5.1 Кб
1/*
2* Copyright 2020 The Backstage Authors
3*
4* Licensed under the Apache License, Version 2.0 (the "License");
5* you may not use this file except in compliance with the License.
6* You may obtain a copy of the License at
7*
8* http://www.apache.org/licenses/LICENSE-2.0
9*
10* Unless required by applicable law or agreed to in writing, software
11* distributed under the License is distributed on an "AS IS" BASIS,
12* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13* See the License for the specific language governing permissions and
14* limitations under the License.
15*/
16
17import fs from 'fs';
18import { dirname, resolve as resolvePath } from 'path';
19
20/**
21* A function that takes a set of path fragments and resolves them into a
22* single complete path, relative to some root.
23*
24* @public
25*/
26export type ResolveFunc = (...paths: string[]) => string;
27
28/**
29* Common paths and resolve functions used by the cli.
30* Currently assumes it is being executed within a monorepo.
31*
32* @public
33*/
34export type Paths = {
35// Root dir of the cli itself, containing package.json
36ownDir: string;
37
38// Monorepo root dir of the cli itself. Only accessible when running inside Backstage repo.
39ownRoot: string;
40
41// The location of the app that the cli is being executed in
42targetDir: string;
43
44// The monorepo root package of the app that the cli is being executed in.
45targetRoot: string;
46
47// Resolve a path relative to own repo
48resolveOwn: ResolveFunc;
49
50// Resolve a path relative to own monorepo root. Only accessible when running inside Backstage repo.
51resolveOwnRoot: ResolveFunc;
52
53// Resolve a path relative to the app
54resolveTarget: ResolveFunc;
55
56// Resolve a path relative to the app repo root
57resolveTargetRoot: ResolveFunc;
58};
59
60// Looks for a package.json with a workspace config to identify the root of the monorepo
61export function findRootPath(
62searchDir: string,
63filterFunc: (pkgJsonPath: string) => boolean,
64): string | undefined {
65let path = searchDir;
66
67// Some confidence check to avoid infinite loop
68for (let i = 0; i < 1000; i++) {
69const packagePath = resolvePath(path, 'package.json');
70const exists = fs.existsSync(packagePath);
71if (exists && filterFunc(packagePath)) {
72return path;
73}
74
75const newPath = dirname(path);
76if (newPath === path) {
77return undefined;
78}
79path = newPath;
80}
81
82throw new Error(
83`Iteration limit reached when searching for root package.json at ${searchDir}`,
84);
85}
86
87// Finds the root of a given package
88export function findOwnDir(searchDir: string) {
89const path = findRootPath(searchDir, () => true);
90if (!path) {
91throw new Error(
92`No package.json found while searching for package root of ${searchDir}`,
93);
94}
95return path;
96}
97
98// Finds the root of the monorepo that the package exists in. Only accessible when running inside Backstage repo.
99export function findOwnRootDir(ownDir: string) {
100const isLocal = fs.existsSync(resolvePath(ownDir, 'src'));
101if (!isLocal) {
102throw new Error(
103'Tried to access monorepo package root dir outside of Backstage repository',
104);
105}
106
107return resolvePath(ownDir, '../..');
108}
109
110/**
111* Find paths related to a package and its execution context.
112*
113* @public
114* @example
115*
116* const paths = findPaths(__dirname)
117*/
118export function findPaths(searchDir: string): Paths {
119const ownDir = findOwnDir(searchDir);
120// Drive letter can end up being lowercased here on Windows, bring back to uppercase for consistency
121const targetDir = fs
122.realpathSync(process.cwd())
123.replace(/^[a-z]:/, str => str.toLocaleUpperCase('en-US'));
124
125// Lazy load this as it will throw an error if we're not inside the Backstage repo.
126let ownRoot = '';
127const getOwnRoot = () => {
128if (!ownRoot) {
129ownRoot = findOwnRootDir(ownDir);
130}
131return ownRoot;
132};
133
134// We're not always running in a monorepo, so we lazy init this to only crash commands
135// that require a monorepo when we're not in one.
136let targetRoot = '';
137const getTargetRoot = () => {
138if (!targetRoot) {
139targetRoot =
140findRootPath(targetDir, path => {
141try {
142const content = fs.readFileSync(path, 'utf8');
143const data = JSON.parse(content);
144return Boolean(data.workspaces?.packages);
145} catch (error) {
146throw new Error(
147`Failed to parse package.json file while searching for root, ${error}`,
148);
149}
150}) ?? targetDir; // We didn't find any root package.json, assume we're not in a monorepo
151}
152return targetRoot;
153};
154
155return {
156ownDir,
157get ownRoot() {
158return getOwnRoot();
159},
160targetDir,
161get targetRoot() {
162return getTargetRoot();
163},
164resolveOwn: (...paths) => resolvePath(ownDir, ...paths),
165resolveOwnRoot: (...paths) => resolvePath(getOwnRoot(), ...paths),
166resolveTarget: (...paths) => resolvePath(targetDir, ...paths),
167resolveTargetRoot: (...paths) => resolvePath(getTargetRoot(), ...paths),
168};
169}
170
171/**
172* The name of the backstage's config file
173*
174* @public
175*/
176export const BACKSTAGE_JSON = 'backstage.json';
177