Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An Example of Legacy Alpha #23210

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ alpha.d.ts
beta.d.ts
internal.d.ts
legacy.d.ts
legacy-alpha.d.ts

# TypeScript incremental build cache
*.tsbuildinfo
Expand Down
41 changes: 21 additions & 20 deletions build-tools/packages/build-cli/docs/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,22 +231,23 @@ Generates type declaration entrypoints for Fluid Framework API levels (/alpha, /
```
USAGE
$ flub generate entrypoints [-v | --quiet] [--mainEntrypoint <value>] [--outDir <value>] [--outFilePrefix <value>]
[--outFileAlpha <value>] [--outFileBeta <value>] [--outFileLegacy <value>] [--outFilePublic <value>]
[--outFileSuffix <value>] [--node10TypeCompat]
[--outFileAlpha <value>] [--outFileBeta <value>] [--outFileLegacy <value>] [--outFileLegacyAlpha <value>]
[--outFilePublic <value>] [--outFileSuffix <value>] [--node10TypeCompat]

FLAGS
--mainEntrypoint=<value> [default: ./src/index.ts] Main entrypoint file containing all untrimmed exports.
--node10TypeCompat Optional generation of Node10 resolution compatible type entrypoints matching others.
--outDir=<value> [default: ./lib] Directory to emit entrypoint declaration files.
--outFileAlpha=<value> [default: alpha] Base file name for alpha entrypoint declaration files.
--outFileBeta=<value> [default: beta] Base file name for beta entrypoint declaration files.
--outFileLegacy=<value> [default: legacy] Base file name for legacy entrypoint declaration files.
--outFilePrefix=<value> File name prefix for emitting entrypoint declaration files. Pattern of
'{@unscopedPackageName}' within value will be replaced with the unscoped name of this
package.
--outFilePublic=<value> [default: public] Base file name for public entrypoint declaration files.
--outFileSuffix=<value> [default: .d.ts] File name suffix including extension for emitting entrypoint declaration
files.
--mainEntrypoint=<value> [default: ./src/index.ts] Main entrypoint file containing all untrimmed exports.
--node10TypeCompat Optional generation of Node10 resolution compatible type entrypoints matching others.
--outDir=<value> [default: ./lib] Directory to emit entrypoint declaration files.
--outFileAlpha=<value> [default: alpha] Base file name for alpha entrypoint declaration files.
--outFileBeta=<value> [default: beta] Base file name for beta entrypoint declaration files.
--outFileLegacy=<value> [default: legacy] Base file name for legacy entrypoint declaration files.
--outFileLegacyAlpha=<value> [default: legacy-alpha] Base file name for legacyAlpha entrypoint declaration files.
--outFilePrefix=<value> File name prefix for emitting entrypoint declaration files. Pattern of
'{@unscopedPackageName}' within value will be replaced with the unscoped name of this
package.
--outFilePublic=<value> [default: public] Base file name for public entrypoint declaration files.
--outFileSuffix=<value> [default: .d.ts] File name suffix including extension for emitting entrypoint
declaration files.

LOGGING FLAGS
-v, --verbose Enable verbose logging.
Expand Down Expand Up @@ -381,17 +382,17 @@ Generates type tests for a package or group of packages.

```
USAGE
$ flub generate typetests [-v | --quiet] [--entrypoint public|alpha|beta|internal|legacy] [--outDir <value>]
[--outFile <value>] [--publicFallback] [--concurrency <value>] [--branch <value> [--changed | | | | [--all |
--dir <value> | --packages | -g client|server|azure|build-tools|gitrest|historian|all... | --releaseGroupRoot
client|server|azure|build-tools|gitrest|historian|all...] | ]] [--private] [--scope <value>... | --skipScope
<value>...]
$ flub generate typetests [-v | --quiet] [--entrypoint public|alpha|beta|internal|legacy|legacy-alpha] [--outDir
<value>] [--outFile <value>] [--publicFallback] [--concurrency <value>] [--branch <value> [--changed | | | |
[--all | --dir <value> | --packages | -g client|server|azure|build-tools|gitrest|historian|all... |
--releaseGroupRoot client|server|azure|build-tools|gitrest|historian|all...] | ]] [--private] [--scope <value>... |
--skipScope <value>...]

FLAGS
--concurrency=<value> [default: 25] The number of tasks to execute concurrently.
--entrypoint=<option> What entrypoint to generate tests for. Use "public" for the default entrypoint. If this flag is
provided it will override the typeValidation.entrypoint setting in the package's package.json.
<options: public|alpha|beta|internal|legacy>
<options: public|alpha|beta|internal|legacy|legacy-alpha>
--outDir=<value> [default: ./src/test/types] Where to emit the type tests file.
--outFile=<value> [default: validate{@unscopedPackageName}Previous.generated.ts] File name for the generated type
tests. The pattern '{@unscopedPackageName}' within the value will be replaced with the unscoped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,21 @@ class ApiLevelReader {
addUniqueNamedExportsToMap(exports.alpha, memberData, ApiLevel.internal);
} else {
addUniqueNamedExportsToMap(exports.legacy, memberData, ApiLevel.legacy);
if (exports["legacy-alpha"].length > 0) {
// Check for a legacyAlpha export to map @legacy & @alpha as legacyAlpha.
const legacyAlphaExport =
this.tempSource
.addImportDeclaration({
moduleSpecifier: `${packageName}/${ApiLevel.legacyAlpha}`,
})
.getModuleSpecifierSourceFile() !== undefined;

addUniqueNamedExportsToMap(
exports["legacy-alpha"],
memberData,
legacyAlphaExport ? ApiLevel.legacyAlpha : ApiLevel.legacy,
);
}
addUniqueNamedExportsToMap(exports.beta, memberData, ApiLevel.beta);
if (exports.alpha.length > 0) {
// @alpha APIs have been mapped to both /alpha and /legacy paths.
Expand Down
2 changes: 2 additions & 0 deletions build-tools/packages/build-cli/src/library/apiLevel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const ApiLevel = {
alpha: "alpha",
internal: "internal",
legacy: "legacy",
legacyAlpha: "legacy-alpha",
} as const;
export type ApiLevel = (typeof ApiLevel)[keyof typeof ApiLevel];

Expand All @@ -28,6 +29,7 @@ export const knownApiLevels = [
ApiLevel.beta,
ApiLevel.internal,
ApiLevel.legacy,
ApiLevel.legacyAlpha,
] as const;

const knownApiLevelSet: ReadonlySet<string> = new Set(knownApiLevels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { CommandLogger } from "../../logging.js";
import { BaseCommand } from "./base.js";

import { ApiLevel } from "../apiLevel.js";
import { ApiTag } from "../apiTag.js";
import type { ExportData, Node10CompatExportData } from "../packageExports.js";
import { queryTypesResolutionPathsFromPackageExports } from "../packageExports.js";
import { getApiExports, getPackageDocumentationText } from "../typescriptApi.js";
Expand All @@ -29,6 +28,7 @@ const optionDefaults = {
outFileAlpha: ApiLevel.alpha,
outFileBeta: ApiLevel.beta,
outFileLegacy: ApiLevel.legacy,
outFileLegacyAlpha: ApiLevel.legacyAlpha,
outFilePublic: ApiLevel.public,
outFileSuffix: ".d.ts",
} as const;
Expand Down Expand Up @@ -69,6 +69,10 @@ export class GenerateEntrypointsCommand extends BaseCommand<
description: "Base file name for legacy entrypoint declaration files.",
default: optionDefaults.outFileLegacy,
}),
outFileLegacyAlpha: Flags.string({
description: "Base file name for legacyAlpha entrypoint declaration files.",
default: optionDefaults.outFileLegacyAlpha,
}),
outFilePublic: Flags.string({
description: "Base file name for public entrypoint declaration files.",
default: optionDefaults.outFilePublic,
Expand Down Expand Up @@ -123,6 +127,13 @@ export class GenerateEntrypointsCommand extends BaseCommand<
);
}

// check the packages exports to see
// if it supports legacyAlpha, and if so seperate them
const separateLegacyFromAlpha =
typeof packageJson.exports === "object" &&
packageJson.exports !== null &&
`./${ApiLevel.legacyAlpha}` in packageJson.exports;

// In the past @alpha APIs could be mapped to /legacy via --outFileAlpha.
// When @alpha is mapped to /legacy, @beta should not be included in
// @alpha aka /legacy entrypoint.
Expand All @@ -133,6 +144,7 @@ export class GenerateEntrypointsCommand extends BaseCommand<
mapApiTagLevelToOutput,
this.logger,
separateBetaFromAlpha,
separateLegacyFromAlpha,
),
);

Expand Down Expand Up @@ -210,33 +222,41 @@ function getOutputConfiguration(
packageJson: PackageJson,
logger?: CommandLogger,
): {
mapQueryPathToApiTagLevel: Map<string | RegExp, ApiTag | undefined>;
mapApiTagLevelToOutput: Map<ApiTag, ExportData>;
mapQueryPathToApiTagLevel: Map<string | RegExp, ApiLevel | undefined>;
mapApiTagLevelToOutput: Map<ApiLevel, ExportData>;
mapNode10CompatExportPathToData: Map<string, Node10CompatExportData>;
} {
const {
outFileSuffix,
outFileAlpha,
outFileBeta,
outFileLegacy,
outFileLegacyAlpha,
outFilePublic,
node10TypeCompat,
} = flags;

const pathPrefix = getOutPathPrefix(flags, packageJson).replace(/\\/g, "/");

const mapQueryPathToApiTagLevel: Map<string | RegExp, ApiTag | undefined> = new Map([
[`${pathPrefix}${outFileAlpha}${outFileSuffix}`, ApiTag.alpha],
[`${pathPrefix}${outFileBeta}${outFileSuffix}`, ApiTag.beta],
[`${pathPrefix}${outFilePublic}${outFileSuffix}`, ApiTag.public],
const mapQueryPathToApiTagLevel: Map<string | RegExp, ApiLevel | undefined> = new Map([
[`${pathPrefix}${outFileAlpha}${outFileSuffix}`, ApiLevel.alpha],
[`${pathPrefix}${outFileBeta}${outFileSuffix}`, ApiLevel.beta],
[`${pathPrefix}${outFilePublic}${outFileSuffix}`, ApiLevel.public],
]);

// In the past @alpha APIs could be mapped to /legacy via --outFileAlpha.
// If @alpha is not mapped to same as @legacy, then @legacy can be mapped.
if (outFileAlpha !== outFileLegacy) {
mapQueryPathToApiTagLevel.set(
`${pathPrefix}${outFileLegacy}${outFileSuffix}`,
ApiTag.legacy,
ApiLevel.legacy,
);
}

if (outFileLegacyAlpha !== outFileLegacy) {
mapQueryPathToApiTagLevel.set(
`${pathPrefix}${outFileLegacyAlpha}${outFileSuffix}`,
ApiLevel.legacyAlpha,
);
}

Expand Down Expand Up @@ -333,16 +353,11 @@ const generatedHeader: string = `/*!
*/
async function generateEntrypoints(
mainEntrypoint: string,
mapApiTagLevelToOutput: Map<ApiTag, ExportData>,
mapApiTagLevelToOutput: Map<ApiLevel, ExportData>,
log: CommandLogger,
separateBetaFromAlpha: boolean,
separateLegacyFromAlpha: boolean,
): Promise<void> {
/**
* List of out file save promises. Used to collect generated file save
* promises so we can await them all at once.
*/
const fileSavePromises: Promise<void>[] = [];

log.info(`Processing: ${mainEntrypoint}`);

const project = new Project({
Expand Down Expand Up @@ -370,13 +385,7 @@ async function generateEntrypoints(
// may include public.
// (public) -> (legacy)
// `-> (beta) -> (alpha)
const apiTagLevels: readonly Exclude<ApiTag, typeof ApiTag.internal>[] = [
ApiTag.public,
ApiTag.legacy,
ApiTag.beta,
ApiTag.alpha,
] as const;
let commonNamedExports: Omit<ExportSpecifierStructure, "kind">[] = [];
const unknownExports: Omit<ExportSpecifierStructure, "kind">[] = [];

if (exports.unknown.size > 0) {
log.errorLog(
Expand All @@ -394,40 +403,43 @@ async function generateEntrypoints(

// Export all unrecognized APIs preserving behavior of api-extractor roll-ups.
for (const name of [...exports.unknown.keys()].sort()) {
commonNamedExports.push({ name, leadingTrivia: "\n\t" });
unknownExports.push({ name, leadingTrivia: "\n\t" });
}
commonNamedExports[0].leadingTrivia = `\n\t// Unrestricted APIs\n\t`;
commonNamedExports[commonNamedExports.length - 1].trailingTrivia = "\n";
unknownExports[0].leadingTrivia = `\n\t// Unrestricted APIs\n\t`;
unknownExports[unknownExports.length - 1].trailingTrivia = "\n";
}

for (const apiTagLevel of apiTagLevels) {
const namedExports = [...commonNamedExports];
const namedExportMap: Record<ApiLevel, Omit<ExportSpecifierStructure, "kind">[]> = {
alpha: [],
beta: [],
internal: [],
legacy: [],
"legacy-alpha": [],
public: [...unknownExports],
};

for (const [apiTagLevel, namedExports] of Object.entries(namedExportMap)) {
// Append this level's additional (or only) exports sorted by ascending case-sensitive name
const orgLength = namedExports.length;
const levelExports = [...exports[apiTagLevel]].sort((a, b) => (a.name > b.name ? 1 : -1));
const levelExports = [...exports[apiTagLevel as ApiLevel]].sort((a, b) =>
a.name > b.name ? 1 : -1,
);
for (const levelExport of levelExports) {
namedExports.push({ ...levelExport, leadingTrivia: "\n\t" });
}
if (namedExports.length > orgLength) {
namedExports[orgLength].leadingTrivia = `\n\t// @${apiTagLevel} APIs\n\t`;
namedExports[namedExports.length - 1].trailingTrivia = "\n";
}
}

// legacy APIs do not accumulate to others
if (apiTagLevel !== "legacy") {
// Additionally, if beta should not accumulate to alpha (alpha may be
// treated specially such as mapped to /legacy) then skip beta too.
// eslint-disable-next-line unicorn/no-lonely-if
if (!separateBetaFromAlpha || apiTagLevel !== "beta") {
// update common set
commonNamedExports = namedExports;
}
}

const writeExport = async (
apiTagLevel: ApiLevel,
namedExports: Omit<ExportSpecifierStructure, "kind">[],
): Promise<void> => {
const output = mapApiTagLevelToOutput.get(apiTagLevel);
if (output === undefined) {
continue;
return;
}

const outFile = output.relPath;
Expand Down Expand Up @@ -455,10 +467,32 @@ async function generateEntrypoints(
sourceFile.insertText(0, `${newFileHeader}export {}\n\n`);
}

fileSavePromises.push(sourceFile.save());
}
await sourceFile.save();
};

await Promise.all(fileSavePromises);
await Promise.all([
writeExport(ApiLevel.public, namedExportMap.public),
writeExport(ApiLevel.beta, [...namedExportMap.public, ...namedExportMap.beta]),
writeExport(ApiLevel.alpha, [
...namedExportMap.public,
...namedExportMap.beta,
...namedExportMap.alpha,
]),
writeExport(ApiLevel.legacy, [
...namedExportMap.public,
...namedExportMap.legacy,
...(separateLegacyFromAlpha ? [] : namedExportMap["legacy-alpha"]),
]),
separateLegacyFromAlpha
? writeExport(ApiLevel.legacyAlpha, [
...namedExportMap.public,
...namedExportMap.beta,
...namedExportMap.alpha,
...namedExportMap.legacy,
...namedExportMap["legacy-alpha"],
])
: undefined,
]);
}

async function generateNode10TypeEntrypoints(
Expand Down
1 change: 0 additions & 1 deletion build-tools/packages/build-cli/src/library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

export { ApiLevel, knownApiLevels, isKnownApiLevel } from "./apiLevel.js";
export { ApiTag } from "./apiTag.js";
export {
generateBumpVersionBranchName,
generateBumpVersionCommitMessage,
Expand Down
16 changes: 6 additions & 10 deletions build-tools/packages/build-cli/src/library/typescriptApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,15 @@
import type { ExportDeclaration, ExportedDeclarations, JSDoc, SourceFile } from "ts-morph";
import { Node, SyntaxKind } from "ts-morph";

import type { ApiLevel } from "./apiLevel.js";
import type { ApiTag } from "./apiTag.js";
import { ApiLevel } from "./apiLevel.js";
import { ApiTag } from "./apiTag.js";
import { isKnownApiTag } from "./apiTag.js";

interface ExportRecord {
name: string;
isTypeOnly: boolean;
}
interface ExportRecords {
public: ExportRecord[];
legacy: ExportRecord[];
beta: ExportRecord[];
alpha: ExportRecord[];
internal: ExportRecord[];
interface ExportRecords extends Record<ApiLevel, ExportRecord[]> {
/**
* Entries here represent exports with unrecognized tags.
* These may be errors or just concerns depending on context.
Expand Down Expand Up @@ -102,8 +97,8 @@ function getNodeApiLevel(node: Node): ApiLevel | undefined {
if (apiTags === undefined) {
return undefined;
}
if (apiTags.includes("legacy")) {
return "legacy";
if (apiTags.includes(ApiTag.legacy)) {
return apiTags.includes(ApiTag.alpha) ? ApiLevel.legacyAlpha : ApiLevel.legacy;
}
if (apiTags.length === 1) {
return apiTags[0];
Expand All @@ -124,6 +119,7 @@ export function getApiExports(sourceFile: SourceFile): ExportRecords {
const records: ExportRecords = {
public: [],
legacy: [],
"legacy-alpha": [],
beta: [],
alpha: [],
internal: [],
Expand Down
Loading
Loading