Change default feature export to happen in publish script
Signed-off-by: Harrison Hogg <hhogg@spotify.com>
This commit is contained in:
@@ -68,18 +68,6 @@ export const isValidPackageFeatureType = (
|
||||
*/
|
||||
export type BackstagePackageFeatureType = (typeof packageFeatureTypes)[number];
|
||||
|
||||
/**
|
||||
* Metadata relating to an exported Backstage system component
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackstagePackageFeature = {
|
||||
// The export path, if omitted then it is the default package export
|
||||
path?: string;
|
||||
// The type of the export
|
||||
type: BackstagePackageFeatureType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Known fields in Backstage package.json files.
|
||||
*
|
||||
@@ -128,11 +116,6 @@ export interface BackstagePackageJson {
|
||||
* All packages that are part of the plugin. Must always and only be set for plugin packages and plugin library packages.
|
||||
*/
|
||||
pluginPackages?: string[];
|
||||
|
||||
/**
|
||||
* The Backstage system components exported from this package
|
||||
*/
|
||||
features?: BackstagePackageFeature[];
|
||||
};
|
||||
|
||||
exports?: JsonValue;
|
||||
|
||||
@@ -22,7 +22,6 @@ export {
|
||||
type PackageGraphNode,
|
||||
type BackstagePackage,
|
||||
type BackstagePackageJson,
|
||||
type BackstagePackageFeature,
|
||||
type BackstagePackageFeatureType,
|
||||
} from './PackageGraph';
|
||||
export {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import {
|
||||
BackstagePackage,
|
||||
BackstagePackageJson,
|
||||
@@ -24,13 +23,9 @@ import {
|
||||
} from '@backstage/cli-node';
|
||||
import { OptionValues } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { resolve as resolvePath, posix, relative as relativePath } from 'path';
|
||||
import { Project } from 'ts-morph';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { publishPreflightCheck } from '../../lib/publishing';
|
||||
import { getFeaturesMetadata } from '../../lib/features';
|
||||
import { readEntryPoints } from '../../lib/entryPoints';
|
||||
|
||||
/**
|
||||
* A mutable object representing a package.json file with potential fixes.
|
||||
@@ -431,46 +426,7 @@ export function fixPluginPackages(
|
||||
}
|
||||
}
|
||||
|
||||
// For each of the defined export locations, we want to annotate
|
||||
// the backstage field with the exported system components. The
|
||||
// "exports" field in package.json is a mapping of import paths to file paths.
|
||||
//
|
||||
// While the exports field is really flexible, this function will enforce a
|
||||
// particular structure, which is a Record<string, string> where the key is the
|
||||
// import path and the value is the file path.
|
||||
export function fixPluginFeatures(
|
||||
pkg: FixablePackage,
|
||||
_packages: FixablePackage[],
|
||||
project: Project,
|
||||
) {
|
||||
const { dir, packageJson } = pkg;
|
||||
|
||||
if (
|
||||
!packageJson.backstage ||
|
||||
!packageJson.backstage.role ||
|
||||
!packageJson.exports
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPoints = readEntryPoints(packageJson);
|
||||
const { role } = packageJson.backstage;
|
||||
const featuresMetadata = getFeaturesMetadata(project, role, dir, entryPoints);
|
||||
|
||||
if (
|
||||
featuresMetadata.length &&
|
||||
!isEqual(packageJson.backstage.features, featuresMetadata)
|
||||
) {
|
||||
packageJson.backstage.features = featuresMetadata;
|
||||
pkg.changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
type PackageFixer = (
|
||||
pkg: FixablePackage,
|
||||
packages: FixablePackage[],
|
||||
project: Project,
|
||||
) => void;
|
||||
type PackageFixer = (pkg: FixablePackage, packages: FixablePackage[]) => void;
|
||||
|
||||
export async function command(opts: OptionValues): Promise<void> {
|
||||
const packages = await readFixablePackages();
|
||||
@@ -484,20 +440,14 @@ export async function command(opts: OptionValues): Promise<void> {
|
||||
fixRepositoryField,
|
||||
fixPluginId,
|
||||
fixPluginPackages,
|
||||
fixPluginFeatures,
|
||||
// Run the publish preflight check too, to make sure we don't uncover errors during publishing
|
||||
publishPreflightCheck,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceRoot = findPaths(process.cwd()).targetRoot;
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolvePath(workspaceRoot, 'tsconfig.json'),
|
||||
});
|
||||
|
||||
for (const fixer of fixers) {
|
||||
for (const pkg of packages) {
|
||||
fixer(pkg, packages, project);
|
||||
fixer(pkg, packages);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePackageFeatureType, PackageRole } from '@backstage/cli-node';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { Project } from 'ts-morph';
|
||||
import { EntryPoint } from '../entryPoints';
|
||||
import { getDistTypeRoot } from '../typeDistProject';
|
||||
|
||||
const mockEntryPoint = {
|
||||
mount: '.',
|
||||
path: './src/index.d.ts',
|
||||
name: 'index',
|
||||
ext: '.d.ts',
|
||||
};
|
||||
|
||||
type CreateFeatureEnvironmentOptions = {
|
||||
$$type?: BackstagePackageFeatureType;
|
||||
format?:
|
||||
| 'DefaultExportAssignment'
|
||||
| 'DefaultExportFromFile'
|
||||
| 'DefaultExportFromFileAsDefault'
|
||||
| 'DefaultExportFromFileWithSibling';
|
||||
role?: PackageRole;
|
||||
};
|
||||
|
||||
type FeatureEnvironment = {
|
||||
project: Project;
|
||||
role: PackageRole;
|
||||
dir: string;
|
||||
entryPoint: EntryPoint;
|
||||
};
|
||||
|
||||
type File = {
|
||||
path: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const createTestType = ($$type: BackstagePackageFeatureType): File[] => [
|
||||
{
|
||||
path: './src/createTestType.d.ts',
|
||||
content: `
|
||||
export interface TestType {
|
||||
readonly $$type: '${$$type}';
|
||||
};
|
||||
|
||||
export function createTestType(): TestType {
|
||||
return {
|
||||
$$type: '${$$type}',
|
||||
};
|
||||
};
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createMockDefaultExportAssignment = (): File[] => [
|
||||
{
|
||||
path: mockEntryPoint.path,
|
||||
content: `
|
||||
declare const _default: import("./createTestType").TestType;
|
||||
export default _default;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createMockDefaultExportFromFile = (): File[] => [
|
||||
{
|
||||
path: mockEntryPoint.path,
|
||||
content: `export { default } from './linked';`,
|
||||
},
|
||||
{
|
||||
path: './src/linked.d.ts',
|
||||
content: `
|
||||
declare const _default: import("./createTestType").TestType;
|
||||
export default _default;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createMockDefaultExportFromFileAsDefault = (): File[] => [
|
||||
{
|
||||
path: mockEntryPoint.path,
|
||||
content: `export { test as default } from './linked';`,
|
||||
},
|
||||
{
|
||||
path: './src/linked.d.ts',
|
||||
content: `
|
||||
export declare const test: import("./createTestType").TestType;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createMockDefaultExportFromFileWithSibling = (): File[] => [
|
||||
{
|
||||
path: mockEntryPoint.path,
|
||||
content: `export { default, test } from './linked';`,
|
||||
},
|
||||
{
|
||||
path: './src/linked.d.ts',
|
||||
content: `
|
||||
import { createTestType } from './createTestType';
|
||||
|
||||
export declare const test: import("./createTestType").TestType;
|
||||
declare const _default: import("./createTestType").TestType;
|
||||
export default _default;
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const formatToFiles = {
|
||||
DefaultExportAssignment: createMockDefaultExportAssignment,
|
||||
DefaultExportFromFile: createMockDefaultExportFromFile,
|
||||
DefaultExportFromFileAsDefault: createMockDefaultExportFromFileAsDefault,
|
||||
DefaultExportFromFileWithSibling: createMockDefaultExportFromFileWithSibling,
|
||||
};
|
||||
|
||||
export default function createFeatureEnvironment(
|
||||
options?: CreateFeatureEnvironmentOptions,
|
||||
): FeatureEnvironment {
|
||||
const {
|
||||
$$type = '@backstage/BackendFeature',
|
||||
format = 'DefaultExportAssignment',
|
||||
role = 'backend-plugin',
|
||||
} = options ?? {};
|
||||
|
||||
const project = new Project();
|
||||
const files = [...createTestType($$type), ...formatToFiles[format]()];
|
||||
|
||||
for (const file of files) {
|
||||
project.createSourceFile(
|
||||
resolvePath(getDistTypeRoot(''), file.path),
|
||||
file.content,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
role,
|
||||
dir: project.getFileSystem().getCurrentDirectory(),
|
||||
entryPoint: mockEntryPoint,
|
||||
};
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePackageFeatureType, PackageRole } from '@backstage/cli-node';
|
||||
import createFeatureEnvironment from '../tests/createFeatureEnvironment';
|
||||
import { getEntryPointExports, getFeaturesMetadata } from './features';
|
||||
|
||||
describe('features', () => {
|
||||
describe('for package role', () => {
|
||||
// This Record makes sure we're checking all package roles
|
||||
const packageRoles: Record<PackageRole, boolean> = {
|
||||
// Allowed
|
||||
'backend-plugin': true,
|
||||
'backend-plugin-module': true,
|
||||
'frontend-plugin': true,
|
||||
'frontend-plugin-module': true,
|
||||
'web-library': true,
|
||||
'node-library': true,
|
||||
// Disallowed
|
||||
frontend: false,
|
||||
backend: false,
|
||||
cli: false,
|
||||
'common-library': false,
|
||||
};
|
||||
|
||||
const allowedPackageRoles = Object.keys(packageRoles).filter(
|
||||
role => packageRoles[role as PackageRole],
|
||||
);
|
||||
|
||||
const disallowedPackageRoles = Object.keys(packageRoles).filter(
|
||||
role => !packageRoles[role as PackageRole],
|
||||
);
|
||||
|
||||
it.each(allowedPackageRoles)(`returns features for %s`, r => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
role: r as PackageRole,
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual([
|
||||
{
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(disallowedPackageRoles)(`does not return features for %s`, r => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
role: r as PackageRole,
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for feature $$type', () => {
|
||||
// This Record makes sure we're checking all feature types
|
||||
const featureTypes: Record<BackstagePackageFeatureType, boolean> = {
|
||||
// Allowed
|
||||
'@backstage/BackendFeature': true,
|
||||
'@backstage/BackstagePlugin': true,
|
||||
'@backstage/FrontendPlugin': true,
|
||||
'@backstage/FrontendModule': true,
|
||||
// Disallowed
|
||||
'@backstage/BackendFeatureFactory': false,
|
||||
'@backstage/BackstageCredentials': false,
|
||||
'@backstage/Extension': false,
|
||||
'@backstage/ExtensionDataRef': false,
|
||||
'@backstage/ExtensionDataValue': false,
|
||||
'@backstage/ExtensionDefinition': false,
|
||||
'@backstage/ExtensionInput': false,
|
||||
'@backstage/ExtensionOverrides': false,
|
||||
'@backstage/ExtensionPoint': false,
|
||||
'@backstage/ExternalRouteRef': false,
|
||||
'@backstage/RouteRef': false,
|
||||
'@backstage/ServiceRef': false,
|
||||
'@backstage/SubRouteRef': false,
|
||||
'@backstage/TranslationMessages': false,
|
||||
'@backstage/TranslationRef': false,
|
||||
'@backstage/TranslationResource': false,
|
||||
};
|
||||
|
||||
const allowedFeatureTypes = Object.keys(featureTypes).filter(
|
||||
$$type => featureTypes[$$type as BackstagePackageFeatureType],
|
||||
);
|
||||
|
||||
const disallowedFeatureTypes = Object.keys(featureTypes).filter(
|
||||
$$type => !featureTypes[$$type as BackstagePackageFeatureType],
|
||||
);
|
||||
|
||||
it.each(allowedFeatureTypes)(`returns features for "%s" $$type`, $$type => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
$$type: $$type as BackstagePackageFeatureType,
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual([
|
||||
{
|
||||
type: $$type,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(disallowedFeatureTypes)(
|
||||
`does not return features for "%s" $$type`,
|
||||
$$type => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
$$type: $$type as BackstagePackageFeatureType,
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual(
|
||||
[],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('export declaration formats', () => {
|
||||
it('supports format of "export default ..."', () => {
|
||||
const {
|
||||
project,
|
||||
dir,
|
||||
entryPoints: [entryPoint],
|
||||
} = createFeatureEnvironment({
|
||||
format: 'DefaultExport',
|
||||
});
|
||||
|
||||
expect(getEntryPointExports(entryPoint, project, dir)).toEqual([
|
||||
{
|
||||
location: '.',
|
||||
name: 'default',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports format of "export { default } from ..."', () => {
|
||||
const {
|
||||
project,
|
||||
dir,
|
||||
entryPoints: [entryPoint],
|
||||
} = createFeatureEnvironment({
|
||||
format: 'DefaultExportLinked',
|
||||
});
|
||||
|
||||
expect(getEntryPointExports(entryPoint, project, dir)).toEqual([
|
||||
{
|
||||
location: '.',
|
||||
name: 'default',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports format of "export { default, ... } from ..."', () => {
|
||||
const {
|
||||
project,
|
||||
dir,
|
||||
entryPoints: [entryPoint],
|
||||
} = createFeatureEnvironment({
|
||||
format: 'DefaultExportLinkedWithSibling',
|
||||
});
|
||||
|
||||
expect(getEntryPointExports(entryPoint, project, dir)).toEqual([
|
||||
{
|
||||
location: '.',
|
||||
name: 'default',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
{
|
||||
location: '.',
|
||||
name: 'test',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports format of "export const foo = ..."', () => {
|
||||
const {
|
||||
project,
|
||||
dir,
|
||||
entryPoints: [entryPoint],
|
||||
} = createFeatureEnvironment({
|
||||
format: 'NamedExport',
|
||||
});
|
||||
|
||||
expect(getEntryPointExports(entryPoint, project, dir)).toEqual([
|
||||
{
|
||||
location: '.',
|
||||
name: 'test',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports format of "export * from ..."', () => {
|
||||
const {
|
||||
project,
|
||||
dir,
|
||||
entryPoints: [entryPoint],
|
||||
} = createFeatureEnvironment({
|
||||
format: 'WildCardExport',
|
||||
});
|
||||
|
||||
expect(getEntryPointExports(entryPoint, project, dir)).toEqual([
|
||||
{
|
||||
location: '.',
|
||||
name: 'test',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entry points', () => {
|
||||
it('returns features for multiple entry points', () => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
exports: {
|
||||
'.': 'src/index.ts',
|
||||
'./alpha': 'src/alpha.ts',
|
||||
'./beta': 'src/beta.ts',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual([
|
||||
{
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
{
|
||||
path: './alpha',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
{
|
||||
path: './beta',
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores entry points that are not .ts or .tsx files', () => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
exports: {
|
||||
'.': 'src/index.js',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('only returns default exports', () => {
|
||||
const { project, role, dir, entryPoints } = createFeatureEnvironment({
|
||||
format: 'DefaultExportLinkedWithSibling',
|
||||
});
|
||||
|
||||
expect(getFeaturesMetadata(project, role, dir, entryPoints)).toEqual([
|
||||
{
|
||||
type: '@backstage/BackendFeature',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
isValidPackageFeatureType,
|
||||
PackageRole,
|
||||
type BackstagePackageFeature,
|
||||
type BackstagePackageFeatureType,
|
||||
} from '@backstage/cli-node';
|
||||
import { Project, SyntaxKind, ts, Type } from 'ts-morph';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import { EntryPoint } from './entryPoints';
|
||||
|
||||
// A list of the package roles we want to extract features for
|
||||
const targetPackageRoles: PackageRole[] = [
|
||||
'backend-plugin',
|
||||
'backend-plugin-module',
|
||||
'frontend-plugin',
|
||||
'frontend-plugin-module',
|
||||
'web-library',
|
||||
'node-library',
|
||||
];
|
||||
|
||||
// A list of the feature types we want to extract from the project
|
||||
// and include in the metadata
|
||||
const targetFeatureTypes: BackstagePackageFeatureType[] = [
|
||||
'@backstage/BackendFeature',
|
||||
'@backstage/BackstagePlugin',
|
||||
'@backstage/FrontendPlugin',
|
||||
'@backstage/FrontendModule',
|
||||
];
|
||||
|
||||
// Returns all valid Backstage package features from a project
|
||||
// for all entry points.
|
||||
export function getFeaturesMetadata(
|
||||
project: Project,
|
||||
role: PackageRole,
|
||||
dir: string,
|
||||
entryPoints: EntryPoint[],
|
||||
): BackstagePackageFeature[] {
|
||||
if (!targetPackageRoles.includes(role)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return entryPoints
|
||||
.flatMap(entryPoint => getEntryPointExports(entryPoint, project, dir))
|
||||
.filter(isDefaultExport)
|
||||
.filter(isTargetFeatureType)
|
||||
.map(toBackstagePackageFeature);
|
||||
}
|
||||
|
||||
type EntryPointExport = {
|
||||
name: string;
|
||||
location: string;
|
||||
type: BackstagePackageFeatureType;
|
||||
};
|
||||
|
||||
// Returns all exports (default and named) from an entry point
|
||||
// that are valid Backstage package features
|
||||
export function getEntryPointExports(
|
||||
{ mount: location, path, ext }: EntryPoint,
|
||||
project: Project,
|
||||
dir: string,
|
||||
): EntryPointExport[] {
|
||||
const fullFilePath = resolvePath(dir, path);
|
||||
const sourceFile = project.getSourceFile(fullFilePath);
|
||||
|
||||
if (!sourceFile || (ext !== '.ts' && ext !== '.tsx')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const exports = [];
|
||||
|
||||
for (const exportSymbol of sourceFile.getExportSymbols()) {
|
||||
const declaration = exportSymbol.getDeclarations()[0];
|
||||
const exportName = declaration.getSymbol()?.getName();
|
||||
let exportType: Type<ts.Type> | undefined;
|
||||
|
||||
if (declaration) {
|
||||
if (declaration.isKind(SyntaxKind.ExportAssignment)) {
|
||||
exportType = declaration.getExpression().getType();
|
||||
} else if (declaration.isKind(SyntaxKind.ExportSpecifier)) {
|
||||
if (!declaration.isTypeOnly()) {
|
||||
exportType = declaration.getType();
|
||||
}
|
||||
} else if (declaration.isKind(SyntaxKind.VariableDeclaration)) {
|
||||
exportType = declaration.getType();
|
||||
}
|
||||
}
|
||||
|
||||
if (exportName && exportType) {
|
||||
const $$type = getBackstagePackageFeature$$TypeFromType(exportType);
|
||||
|
||||
if ($$type) {
|
||||
exports.push({
|
||||
name: exportName,
|
||||
location,
|
||||
type: $$type,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
// Given a TS type, returns the Backstage package feature $$type value
|
||||
function getBackstagePackageFeature$$TypeFromType(
|
||||
type: Type,
|
||||
): BackstagePackageFeatureType | null {
|
||||
// Returns the concrete type of a generic type
|
||||
const exportType = type.getTargetType() ?? type;
|
||||
|
||||
for (const property of exportType.getProperties()) {
|
||||
if (property.getName() === '$$type') {
|
||||
const $$type = property
|
||||
.getValueDeclaration()
|
||||
?.getText()
|
||||
.match(/(?<type>@backstage\/\w+)/)?.groups?.type;
|
||||
|
||||
if ($$type && isValidPackageFeatureType($$type)) {
|
||||
return $$type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Returns whether an export is the default export
|
||||
function isDefaultExport({ name }: EntryPointExport): boolean {
|
||||
return name === 'default';
|
||||
}
|
||||
|
||||
// Returns whether an export is a valid Backstage package feature type
|
||||
function isTargetFeatureType({ type }: EntryPointExport): boolean {
|
||||
return targetFeatureTypes.includes(type);
|
||||
}
|
||||
|
||||
// Converts an entry point export to a Backstage package feature
|
||||
function toBackstagePackageFeature({
|
||||
type,
|
||||
location,
|
||||
}: EntryPointExport): BackstagePackageFeature {
|
||||
const feature: BackstagePackageFeature = { type };
|
||||
|
||||
if (location !== '.') {
|
||||
feature.path = location;
|
||||
}
|
||||
|
||||
return feature;
|
||||
}
|
||||
@@ -19,6 +19,10 @@ import npmPackList from 'npm-packlist';
|
||||
import { resolve as resolvePath, posix as posixPath } from 'path';
|
||||
import { BackstagePackageJson } from '@backstage/cli-node';
|
||||
import { readEntryPoints } from '../entryPoints';
|
||||
import {
|
||||
createTypeDistProject,
|
||||
getEntryPointDefaultFeatureType,
|
||||
} from '../typeDistProject';
|
||||
|
||||
const PKG_PATH = 'package.json';
|
||||
const PKG_BACKUP_PATH = 'package.json-prepack';
|
||||
@@ -147,11 +151,14 @@ async function prepareExportsEntryPoints(
|
||||
>();
|
||||
|
||||
const entryPoints = readEntryPoints(pkg);
|
||||
const project = await createTypeDistProject();
|
||||
|
||||
for (const entryPoint of entryPoints) {
|
||||
if (!SCRIPT_EXTS.includes(entryPoint.ext)) {
|
||||
outputExports[entryPoint.mount] = entryPoint.path;
|
||||
continue;
|
||||
}
|
||||
|
||||
const exp = {} as Record<string, string>;
|
||||
for (const [key, ext] of Object.entries(EXPORT_MAP)) {
|
||||
const name = `${entryPoint.name}${ext}`;
|
||||
@@ -161,6 +168,19 @@ async function prepareExportsEntryPoints(
|
||||
}
|
||||
exp.default = exp.require ?? exp.import;
|
||||
|
||||
const defaultFeatureType =
|
||||
pkg.backstage?.role &&
|
||||
getEntryPointDefaultFeatureType(
|
||||
pkg.backstage?.role,
|
||||
packageDir,
|
||||
project,
|
||||
entryPoint,
|
||||
);
|
||||
|
||||
if (defaultFeatureType) {
|
||||
exp.backstage = defaultFeatureType;
|
||||
}
|
||||
|
||||
// This creates a directory with a lone package.json for backwards compatibility
|
||||
if (entryPoint.mount === '.') {
|
||||
if (exp.default) {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePackageFeatureType, PackageRole } from '@backstage/cli-node';
|
||||
import createFeatureEnvironment from './__testUtils__/createFeatureEnvironment';
|
||||
import { getEntryPointDefaultFeatureType } from './typeDistProject';
|
||||
|
||||
describe('typeDistProject', () => {
|
||||
describe('for package role', () => {
|
||||
// This Record makes sure we're checking all package roles
|
||||
const packageRoles: Record<PackageRole, boolean> = {
|
||||
// Allowed
|
||||
'backend-plugin': true,
|
||||
'backend-plugin-module': true,
|
||||
'frontend-plugin': true,
|
||||
'frontend-plugin-module': true,
|
||||
'web-library': true,
|
||||
'node-library': true,
|
||||
// Disallowed
|
||||
frontend: false,
|
||||
backend: false,
|
||||
cli: false,
|
||||
'common-library': false,
|
||||
};
|
||||
|
||||
const allowedPackageRoles = Object.keys(packageRoles).filter(
|
||||
role => packageRoles[role as PackageRole],
|
||||
);
|
||||
|
||||
const disallowedPackageRoles = Object.keys(packageRoles).filter(
|
||||
role => !packageRoles[role as PackageRole],
|
||||
);
|
||||
|
||||
it.each(allowedPackageRoles)(`returns features for %s`, r => {
|
||||
const { project, role, dir, entryPoint } = createFeatureEnvironment({
|
||||
role: r as PackageRole,
|
||||
});
|
||||
|
||||
expect(
|
||||
getEntryPointDefaultFeatureType(role, dir, project, entryPoint),
|
||||
).toEqual('@backstage/BackendFeature');
|
||||
});
|
||||
|
||||
it.each(disallowedPackageRoles)(`does not return features for %s`, r => {
|
||||
const { project, role, dir, entryPoint } = createFeatureEnvironment({
|
||||
role: r as PackageRole,
|
||||
});
|
||||
|
||||
expect(
|
||||
getEntryPointDefaultFeatureType(role, dir, project, entryPoint),
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for feature $$type', () => {
|
||||
// This Record makes sure we're checking all feature types
|
||||
const featureTypes: Record<BackstagePackageFeatureType, boolean> = {
|
||||
// Allowed
|
||||
'@backstage/BackendFeature': true,
|
||||
'@backstage/BackstagePlugin': true,
|
||||
'@backstage/FrontendPlugin': true,
|
||||
'@backstage/FrontendModule': true,
|
||||
// Disallowed
|
||||
'@backstage/BackendFeatureFactory': false,
|
||||
'@backstage/BackstageCredentials': false,
|
||||
'@backstage/Extension': false,
|
||||
'@backstage/ExtensionDataRef': false,
|
||||
'@backstage/ExtensionDataValue': false,
|
||||
'@backstage/ExtensionDefinition': false,
|
||||
'@backstage/ExtensionInput': false,
|
||||
'@backstage/ExtensionOverrides': false,
|
||||
'@backstage/ExtensionPoint': false,
|
||||
'@backstage/ExternalRouteRef': false,
|
||||
'@backstage/RouteRef': false,
|
||||
'@backstage/ServiceRef': false,
|
||||
'@backstage/SubRouteRef': false,
|
||||
'@backstage/TranslationMessages': false,
|
||||
'@backstage/TranslationRef': false,
|
||||
'@backstage/TranslationResource': false,
|
||||
};
|
||||
|
||||
const allowedFeatureTypes = Object.keys(featureTypes).filter(
|
||||
$$type => featureTypes[$$type as BackstagePackageFeatureType],
|
||||
);
|
||||
|
||||
const disallowedFeatureTypes = Object.keys(featureTypes).filter(
|
||||
$$type => !featureTypes[$$type as BackstagePackageFeatureType],
|
||||
);
|
||||
|
||||
it.each(allowedFeatureTypes)(`returns features for "%s" $$type`, $$type => {
|
||||
const { project, role, dir, entryPoint } = createFeatureEnvironment({
|
||||
$$type: $$type as BackstagePackageFeatureType,
|
||||
});
|
||||
|
||||
expect(
|
||||
getEntryPointDefaultFeatureType(role, dir, project, entryPoint),
|
||||
).toEqual($$type);
|
||||
});
|
||||
|
||||
it.each(disallowedFeatureTypes)(
|
||||
`does not return features for "%s" $$type`,
|
||||
$$type => {
|
||||
const { project, role, dir, entryPoint } = createFeatureEnvironment({
|
||||
$$type: $$type as BackstagePackageFeatureType,
|
||||
});
|
||||
|
||||
expect(
|
||||
getEntryPointDefaultFeatureType(role, dir, project, entryPoint),
|
||||
).toEqual(null);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'DefaultExportAssignment',
|
||||
'DefaultExportFromFile',
|
||||
'DefaultExportFromFileAsDefault',
|
||||
'DefaultExportFromFileWithSibling',
|
||||
] as const)('returns features for format "%s"', format => {
|
||||
const { project, role, dir, entryPoint } = createFeatureEnvironment({
|
||||
format,
|
||||
});
|
||||
|
||||
expect(
|
||||
getEntryPointDefaultFeatureType(role, dir, project, entryPoint),
|
||||
).toEqual('@backstage/BackendFeature');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import {
|
||||
BackstagePackageFeatureType,
|
||||
isValidPackageFeatureType,
|
||||
PackageGraph,
|
||||
PackageRole,
|
||||
} from '@backstage/cli-node';
|
||||
import { builtinModules } from 'module';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { Project, SourceFile, SyntaxKind, ts, Type } from 'ts-morph';
|
||||
import { EntryPoint, readEntryPoints } from './entryPoints';
|
||||
|
||||
export const getDistTypeRoot = (dir: string) => {
|
||||
const workspaceRoot = findPaths(process.cwd()).targetRoot;
|
||||
const relativeDirectory = dir.replace(workspaceRoot, '');
|
||||
const distTypeRoot = resolvePath(
|
||||
workspaceRoot,
|
||||
`./dist-types${relativeDirectory}`,
|
||||
);
|
||||
|
||||
return distTypeRoot;
|
||||
};
|
||||
|
||||
const getPackagesDistTypeMap = async () => {
|
||||
const packages = await PackageGraph.listTargetPackages();
|
||||
const distTypeMap: Record<string, string> = {};
|
||||
|
||||
for (const { dir, packageJson } of packages) {
|
||||
const distTypeRoot = getDistTypeRoot(dir);
|
||||
|
||||
for (const { name, path, ext } of readEntryPoints(packageJson)) {
|
||||
const dtsPath = resolvePath(distTypeRoot, path.replace(ext, '.d.ts'));
|
||||
|
||||
if (name === 'index') {
|
||||
distTypeMap[packageJson.name] = dtsPath;
|
||||
} else {
|
||||
distTypeMap[`${packageJson.name}/${name}`] = dtsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distTypeMap;
|
||||
};
|
||||
|
||||
export const createTypeDistProject = async () => {
|
||||
const distTypeMap = await getPackagesDistTypeMap();
|
||||
const workspaceRoot = findPaths(process.cwd()).targetRoot;
|
||||
|
||||
return new Project({
|
||||
tsConfigFilePath: resolvePath(workspaceRoot, 'tsconfig.json'),
|
||||
skipAddingFilesFromTsConfig: true,
|
||||
resolutionHost: (moduleResolutionHost, getCompilerOptions) => {
|
||||
return {
|
||||
resolveModuleNames: (moduleNames, containingFile) => {
|
||||
const compilerOptions = getCompilerOptions();
|
||||
const resolvedModules: ts.ResolvedModule[] = [];
|
||||
|
||||
for (let moduleName of moduleNames) {
|
||||
// Handle resolve internal plugins and package entry points to dist-types folder
|
||||
if (distTypeMap[moduleName]) {
|
||||
resolvedModules.push({
|
||||
resolvedFileName: distTypeMap[moduleName],
|
||||
isExternalLibraryImport: false,
|
||||
resolvedUsingTsExtension: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle resolving builtin node modules to @types/node
|
||||
if (moduleName.startsWith('node:')) {
|
||||
moduleName = moduleName.slice(5);
|
||||
}
|
||||
if (builtinModules.includes(moduleName)) {
|
||||
const result = ts.resolveModuleName(
|
||||
`@types/node/${moduleName}`,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
moduleResolutionHost,
|
||||
);
|
||||
|
||||
if (result.resolvedModule) {
|
||||
resolvedModules.push(result.resolvedModule);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle resolve relative paths and external node_modules.
|
||||
const result = ts.resolveModuleName(
|
||||
moduleName,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
moduleResolutionHost,
|
||||
);
|
||||
|
||||
if (result.resolvedModule) {
|
||||
resolvedModules.push(result.resolvedModule);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Failed to resolve module: ${moduleName}`);
|
||||
}
|
||||
|
||||
return resolvedModules;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// A list of the package roles we want to extract features for
|
||||
const targetPackageRoles: PackageRole[] = [
|
||||
'backend-plugin',
|
||||
'backend-plugin-module',
|
||||
'frontend-plugin',
|
||||
'frontend-plugin-module',
|
||||
'web-library',
|
||||
'node-library',
|
||||
];
|
||||
|
||||
// A list of the feature types we want to extract from the project
|
||||
// and include in the metadata
|
||||
const targetFeatureTypes: BackstagePackageFeatureType[] = [
|
||||
'@backstage/BackendFeature',
|
||||
'@backstage/BackstagePlugin',
|
||||
'@backstage/FrontendPlugin',
|
||||
'@backstage/FrontendModule',
|
||||
];
|
||||
|
||||
export const getEntryPointDefaultFeatureType = (
|
||||
role: PackageRole,
|
||||
packageDir: string,
|
||||
project: Project,
|
||||
entryPoint: EntryPoint,
|
||||
): BackstagePackageFeatureType | null => {
|
||||
if (isTargetPackageRole(role)) {
|
||||
const dtsPath = resolvePath(
|
||||
getDistTypeRoot(packageDir),
|
||||
entryPoint.path.replace(entryPoint.ext, '.d.ts'),
|
||||
);
|
||||
|
||||
const defaultFeatureType = getSourceFileDefaultFeatureType(
|
||||
project.addSourceFileAtPath(dtsPath),
|
||||
);
|
||||
|
||||
if (isTargetFeatureType(defaultFeatureType)) {
|
||||
return defaultFeatureType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Condition for a package role matches a target package role
|
||||
function isTargetPackageRole(role: PackageRole): boolean {
|
||||
return !!role && targetPackageRoles.includes(role);
|
||||
}
|
||||
|
||||
// Returns whether an export is a valid Backstage package feature type
|
||||
function isTargetFeatureType(
|
||||
type: BackstagePackageFeatureType | null,
|
||||
): boolean {
|
||||
return !!type && targetFeatureTypes.includes(type);
|
||||
}
|
||||
|
||||
// Returns all exports (default and named) from an entry point
|
||||
// that are valid Backstage package features
|
||||
function getSourceFileDefaultFeatureType(
|
||||
sourceFile: SourceFile,
|
||||
): BackstagePackageFeatureType | null {
|
||||
for (const exportSymbol of sourceFile.getExportSymbols()) {
|
||||
const declaration = exportSymbol.getDeclarations()[0];
|
||||
const exportName = declaration.getSymbol()?.getName();
|
||||
|
||||
if (exportName !== 'default') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let exportType: Type<ts.Type> | undefined;
|
||||
|
||||
if (declaration) {
|
||||
if (declaration.isKind(SyntaxKind.ExportAssignment)) {
|
||||
exportType = declaration.getExpression().getType();
|
||||
} else if (declaration.isKind(SyntaxKind.ExportSpecifier)) {
|
||||
if (!declaration.isTypeOnly()) {
|
||||
exportType = declaration.getType();
|
||||
}
|
||||
} else if (declaration.isKind(SyntaxKind.VariableDeclaration)) {
|
||||
exportType = declaration.getType();
|
||||
}
|
||||
}
|
||||
|
||||
if (exportName && exportType) {
|
||||
const $$type = getBackstagePackageFeature$$TypeFromType(exportType);
|
||||
|
||||
if ($$type) {
|
||||
return $$type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Given a TS type, returns the Backstage package feature $$type value
|
||||
function getBackstagePackageFeature$$TypeFromType(
|
||||
type: Type,
|
||||
): BackstagePackageFeatureType | null {
|
||||
// Returns the concrete type of a generic type
|
||||
const exportType = type.getTargetType() ?? type;
|
||||
|
||||
for (const property of exportType.getProperties()) {
|
||||
if (property.getName() === '$$type') {
|
||||
const $$type = property
|
||||
.getValueDeclaration()
|
||||
?.getText()
|
||||
.match(/(?<type>@backstage\/\w+)/)?.groups?.type;
|
||||
|
||||
if ($$type && isValidPackageFeatureType($$type)) {
|
||||
return $$type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePackageFeatureType, PackageRole } from '@backstage/cli-node';
|
||||
import path from 'path';
|
||||
import { Project } from 'ts-morph';
|
||||
import { EntryPoint } from '../lib/entryPoints';
|
||||
|
||||
type CreateFeatureEnvironmentOptions = {
|
||||
$$type?: BackstagePackageFeatureType;
|
||||
exports?: Record<string, string>;
|
||||
format?:
|
||||
| 'DefaultExport'
|
||||
| 'DefaultExportLinked'
|
||||
| 'DefaultExportLinkedWithSibling'
|
||||
| 'NamedExport'
|
||||
| 'WildCardExport';
|
||||
role?: PackageRole;
|
||||
};
|
||||
|
||||
type FeatureEnvironment = {
|
||||
project: Project;
|
||||
role: PackageRole;
|
||||
dir: string;
|
||||
entryPoints: EntryPoint[];
|
||||
};
|
||||
|
||||
type File = {
|
||||
path: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
const createForExports = (
|
||||
exports: Record<string, string>,
|
||||
content: string,
|
||||
): File[] => {
|
||||
return Object.entries(exports).map(([_, filePath]) => ({
|
||||
path: `./${filePath}`,
|
||||
content,
|
||||
}));
|
||||
};
|
||||
|
||||
const createTestType = ($$type: BackstagePackageFeatureType): File[] => [
|
||||
{
|
||||
path: './src/createTestType.ts',
|
||||
content: `
|
||||
export interface TestType {
|
||||
readonly $$type: '${$$type}';
|
||||
};
|
||||
|
||||
export function createTestType(): TestType {
|
||||
return {
|
||||
$$type: '${$$type}',
|
||||
};
|
||||
};
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createTestDefaultExport = (exports: Record<string, string>): File[] =>
|
||||
createForExports(
|
||||
exports,
|
||||
`
|
||||
import { createTestType } from './createTestType';
|
||||
|
||||
export { TestType } from './createTestType';
|
||||
export default createTestType();
|
||||
`,
|
||||
);
|
||||
|
||||
const createTestDefaultExportLinked = (
|
||||
exports: Record<string, string>,
|
||||
): File[] => [
|
||||
...createForExports(
|
||||
exports,
|
||||
`
|
||||
export { TestType } from './createTestType';
|
||||
export { default } from './linked';
|
||||
`,
|
||||
),
|
||||
{
|
||||
path: './src/linked.ts',
|
||||
content: `
|
||||
import { createTestType } from './createTestType';
|
||||
|
||||
export default createTestType();
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createTestDefaultExportLinkedWithSibling = (
|
||||
exports: Record<string, string>,
|
||||
): File[] => [
|
||||
...createForExports(
|
||||
exports,
|
||||
`
|
||||
export { TestType } from './createTestType';
|
||||
export { default, test } from './linked';
|
||||
`,
|
||||
),
|
||||
{
|
||||
path: './src/linked.ts',
|
||||
content: `
|
||||
import { createTestType } from './createTestType';
|
||||
|
||||
export const test = createTestType();
|
||||
export default createTestType();
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const createTestNamedExport = (exports: Record<string, string>): File[] => [
|
||||
...createForExports(
|
||||
exports,
|
||||
`
|
||||
import { createTestType } from './createTestType';
|
||||
|
||||
export { TestType } from './createTestType';
|
||||
export const test = createTestType();
|
||||
`,
|
||||
),
|
||||
];
|
||||
|
||||
const createTestWildCardExport = (exports: Record<string, string>): File[] => [
|
||||
...createForExports(
|
||||
exports,
|
||||
`
|
||||
export * from './linked';
|
||||
`,
|
||||
),
|
||||
{
|
||||
path: './src/linked.ts',
|
||||
content: `
|
||||
import { createTestType } from './createTestType';
|
||||
|
||||
export { TestType } from './createTestType';
|
||||
export const test = createTestType();
|
||||
export default createTestType();
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const formatToFiles = {
|
||||
DefaultExport: createTestDefaultExport,
|
||||
DefaultExportLinked: createTestDefaultExportLinked,
|
||||
DefaultExportLinkedWithSibling: createTestDefaultExportLinkedWithSibling,
|
||||
NamedExport: createTestNamedExport,
|
||||
WildCardExport: createTestWildCardExport,
|
||||
};
|
||||
|
||||
export default function createFeatureEnvironment(
|
||||
options?: CreateFeatureEnvironmentOptions,
|
||||
): FeatureEnvironment {
|
||||
const {
|
||||
$$type = '@backstage/BackendFeature',
|
||||
exports = { '.': 'src/index.ts' },
|
||||
format = 'DefaultExport',
|
||||
role = 'backend-plugin',
|
||||
} = options ?? {};
|
||||
|
||||
const project = new Project();
|
||||
const entryPoints: EntryPoint[] = Object.entries(exports ?? {}).map(
|
||||
([mount, filePath]) => ({
|
||||
mount,
|
||||
path: filePath,
|
||||
name: mount,
|
||||
ext: path.extname(filePath),
|
||||
}),
|
||||
);
|
||||
|
||||
const files = [...createTestType($$type), ...formatToFiles[format](exports)];
|
||||
|
||||
for (const file of files) {
|
||||
project.createSourceFile(file.path, file.content);
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
role,
|
||||
dir: project.getFileSystem().getCurrentDirectory(),
|
||||
entryPoints,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user