diff --git a/build-tools/packages/build-cli/docs/generate.md b/build-tools/packages/build-cli/docs/generate.md index c02f1c9855df..e4c3730ed65f 100644 --- a/build-tools/packages/build-cli/docs/generate.md +++ b/build-tools/packages/build-cli/docs/generate.md @@ -11,7 +11,6 @@ Generate commands are used to create/update code, docs, readmes, etc. * [`flub generate entrypoints`](#flub-generate-entrypoints) * [`flub generate packlist`](#flub-generate-packlist) * [`flub generate releaseNotes`](#flub-generate-releasenotes) -* [`flub generate source-entrypoints`](#flub-generate-source-entrypoints) * [`flub generate typetests`](#flub-generate-typetests) * [`flub generate upcoming`](#flub-generate-upcoming) @@ -356,28 +355,6 @@ EXAMPLES _See code: [src/commands/generate/releaseNotes.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/generate/releaseNotes.ts)_ -## `flub generate source-entrypoints` - -Generates type declaration entrypoints for Fluid Framework API levels (/alpha, /beta. etc.) as found in package.json "exports" - -``` -USAGE - $ flub generate source-entrypoints [-v | --quiet] [--mainEntrypoint ] - -FLAGS - --mainEntrypoint= [default: ./src/index.ts] Main entrypoint file containing all untrimmed exports. - -LOGGING FLAGS - -v, --verbose Enable verbose logging. - --quiet Disable all logging. - -DESCRIPTION - Generates type declaration entrypoints for Fluid Framework API levels (/alpha, /beta. etc.) as found in package.json - "exports" -``` - -_See code: [src/commands/generate/source-entrypoints.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/generate/source-entrypoints.ts)_ - ## `flub generate typetests` Generates type tests for a package or group of packages. diff --git a/build-tools/packages/build-cli/src/commands/generate/source-entrypoints.ts b/build-tools/packages/build-cli/src/commands/generate/source-entrypoints.ts deleted file mode 100644 index 46d3b5b074bd..000000000000 --- a/build-tools/packages/build-cli/src/commands/generate/source-entrypoints.ts +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import { GenerateSourceEntrypointsCommand } from "../../library/index.js"; - -export default class GenerateSourcentrypointsCommand_ extends GenerateSourceEntrypointsCommand {} diff --git a/build-tools/packages/build-cli/src/library/commands/generateEntrypoints.ts b/build-tools/packages/build-cli/src/library/commands/generateEntrypoints.ts index 8ac936f6dfee..71c2e3f4a6f3 100644 --- a/build-tools/packages/build-cli/src/library/commands/generateEntrypoints.ts +++ b/build-tools/packages/build-cli/src/library/commands/generateEntrypoints.ts @@ -123,10 +123,18 @@ export class GenerateEntrypointsCommand extends BaseCommand< ); } - // generate only node10 compat entrypoints - if (!node10TypeCompat) { - promises.push(generateEntrypoints(mainEntrypoint, mapApiTagLevelToOutput, this.logger)); - } + // 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. + const separateBetaFromAlpha = this.flags.outFileAlpha !== ApiLevel.alpha; + promises.push( + generateEntrypoints( + mainEntrypoint, + mapApiTagLevelToOutput, + this.logger, + separateBetaFromAlpha, + ), + ); if (node10TypeCompat) { promises.push( @@ -221,9 +229,17 @@ function getOutputConfiguration( [`${pathPrefix}${outFileAlpha}${outFileSuffix}`, ApiTag.alpha], [`${pathPrefix}${outFileBeta}${outFileSuffix}`, ApiTag.beta], [`${pathPrefix}${outFilePublic}${outFileSuffix}`, ApiTag.public], - [`${pathPrefix}${outFileLegacy}${outFileSuffix}`, ApiTag.legacy], ]); + // 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, + ); + } + if (node10TypeCompat) { // /internal export may be supported without API level generation; so // add query for such path for Node10 type compat generation. @@ -313,11 +329,13 @@ const generatedHeader: string = `/*! * @param mainEntrypoint - path to main entrypoint file * @param mapApiTagLevelToOutput - level oriented ApiTag to output file mapping * @param log - logger + * @param separateBetaFromAlpha - if true, beta APIs will not be included in alpha outputs */ async function generateEntrypoints( mainEntrypoint: string, mapApiTagLevelToOutput: Map, log: CommandLogger, + separateBetaFromAlpha: boolean, ): Promise { /** * List of out file save promises. Used to collect generated file save @@ -401,7 +419,7 @@ async function generateEntrypoints( // 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 (apiTagLevel !== "beta") { + if (!separateBetaFromAlpha || apiTagLevel !== "beta") { // update common set commonNamedExports = namedExports; } diff --git a/build-tools/packages/build-cli/src/library/commands/generateSourceEntrypoints.ts b/build-tools/packages/build-cli/src/library/commands/generateSourceEntrypoints.ts deleted file mode 100644 index 31d73cd4bca9..000000000000 --- a/build-tools/packages/build-cli/src/library/commands/generateSourceEntrypoints.ts +++ /dev/null @@ -1,289 +0,0 @@ -/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -import fs from "node:fs/promises"; - -import type { PackageJson } from "@fluidframework/build-tools"; -import { Flags } from "@oclif/core"; -import type { ExportSpecifierStructure, Node } from "ts-morph"; -import { ModuleKind, Project, ScriptKind } from "ts-morph"; - -import type { CommandLogger } from "../../logging.js"; -import { BaseCommand } from "./base.js"; - -import { ApiLevel } from "../apiLevel.js"; -import { ApiTag } from "../apiTag.js"; -import type { ExportData } from "../packageExports.js"; -import { queryDefaultResolutionPathsFromPackageExports } from "../packageExports.js"; -import { getApiExports, getPackageDocumentationText } from "../typescriptApi.js"; - -import type { TsConfigJson } from "type-fest"; - -const optionDefaults = { - mainEntrypoint: "./src/index.ts", - outFileAlpha: ApiLevel.alpha, - outFileBeta: ApiLevel.beta, - outFileLegacy: ApiLevel.legacy, - outFilePublic: ApiLevel.public, - outFileSuffix: ".ts", - srcDir: "src/entrypoints/", -} as const; - -/** - * Generates type declarations files for Fluid Framework APIs to support API levels (/alpha, /beta. etc.). - */ -export class GenerateSourceEntrypointsCommand extends BaseCommand< - typeof GenerateSourceEntrypointsCommand -> { - static readonly description = - `Generates type declaration entrypoints for Fluid Framework API levels (/alpha, /beta. etc.) as found in package.json "exports"`; - - static readonly flags = { - mainEntrypoint: Flags.file({ - description: "Main entrypoint file containing all untrimmed exports.", - default: optionDefaults.mainEntrypoint, - exists: true, - }), - ...BaseCommand.flags, - }; - - public async run(): Promise { - const { mainEntrypoint } = this.flags; - - const packageJson = await readPackageJson(); - - const tsConfig = await readTsConfig(); - - const { mapQueryPathToApiTagLevel, mapApiTagLevelToOutput } = getOutputConfiguration( - packageJson, - tsConfig, - this.logger, - ); - - if (mapApiTagLevelToOutput.size === 0) { - throw new Error( - `There are no package exports matching requested output entrypoints:\n\t${[ - ...mapQueryPathToApiTagLevel.keys(), - ].join("\n\t")}`, - ); - } - - const promises: Promise[] = []; - promises.push( - generateSourceEntrypoints(mainEntrypoint, mapApiTagLevelToOutput, this.logger), - ); - - // All of the output actions (deletes of stale files or writing of new/updated files) - // are all independent and can be done in parallel. - await Promise.all(promises); - } -} - -async function readPackageJson(): Promise { - const packageJson = await fs.readFile("./package.json", { encoding: "utf8" }); - return JSON.parse(packageJson) as PackageJson; -} - -async function readTsConfig(): Promise { - const tsConfigContent = await fs.readFile("./tsconfig.json", { encoding: "utf8" }); - // Trim content to avoid unexpected whitespace issues - const trimmedContent = tsConfigContent.trim(); - - // Remove trailing commas before parsing - const sanitizedContent = trimmedContent.replace(/,\s*([\]}])/g, "$1"); - - // Parse and validate JSON content - return JSON.parse(sanitizedContent) as TsConfigJson; -} - -function getOutputConfiguration( - packageJson: PackageJson, - tsconfig: TsConfigJson, - logger?: CommandLogger, -): { - mapQueryPathToApiTagLevel: Map; - mapApiTagLevelToOutput: Map; -} { - const { outFileSuffix, outFileAlpha, outFileBeta, outFileLegacy, outFilePublic, srcDir } = - optionDefaults; - - const mapQuerySrcPathToApiTagLevel: Map = new Map([ - [`${srcDir}${outFileAlpha}${outFileSuffix}`, ApiTag.alpha], - [`${srcDir}${outFileBeta}${outFileSuffix}`, ApiTag.beta], - [`${srcDir}${outFilePublic}${outFileSuffix}`, ApiTag.public], - [`${srcDir}${outFileLegacy}${outFileSuffix}`, ApiTag.legacy], - ]); - - let emitDeclarationOnly: boolean = false; - if (tsconfig.compilerOptions?.emitDeclarationOnly !== undefined) { - emitDeclarationOnly = tsconfig.compilerOptions.emitDeclarationOnly; - } - - const { mapKeyToOutput: mapSrcApiTagLevelToOutput } = - queryDefaultResolutionPathsFromPackageExports( - packageJson, - mapQuerySrcPathToApiTagLevel, - emitDeclarationOnly, - logger, - ); - - return { - mapQueryPathToApiTagLevel: mapQuerySrcPathToApiTagLevel, - mapApiTagLevelToOutput: mapSrcApiTagLevelToOutput, - }; -} - -function sourceContext(node: Node): string { - return `${node.getSourceFile().getFilePath()}:${node.getStartLineNumber()}`; -} - -const generatedHeader: string = `/*! - * Copyright (c) Microsoft Corporation and contributors. All rights reserved. - * Licensed under the MIT License. - */ - -/* - * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. - * Generated by "flub generate source-entrypoints" in @fluid-tools/build-cli. - */ - -`; - -/** - * Generate "rollup" entrypoints for the given main entrypoint file. - * - * @param mainEntrypoint - path to main entrypoint file - * @param mapApiTagLevelToOutput - level oriented ApiTag to output file mapping - * @param log - logger - */ -async function generateSourceEntrypoints( - mainEntrypoint: string, - mapApiTagLevelToOutput: Map, - log: CommandLogger, -): Promise { - /** - * List of out file save promises. Used to collect generated file save - * promises so we can await them all at once. - */ - const fileSavePromises: Promise[] = []; - - log.info(`Processing: ${mainEntrypoint}`); - - const project = new Project({ - skipAddingFilesFromTsConfig: true, - // Note: it is likely better to leverage a tsconfig file from package rather than - // assume Node16 and no other special setup. However, currently configs are pretty - // standard with simple Node16 module specification and using a tsconfig for just - // part of its setting may be confusing to document and keep tidy with dual-emit. - compilerOptions: { - module: ModuleKind.Node16, - - // Without this, JSX files are not properly handled by ts-morph. "React" is the - // value we use in our base config, so it should be a safe value. - jsx: 2 /* JSXEmit.React */, - }, - }); - const mainSourceFile = project.addSourceFileAtPath(mainEntrypoint); - const exports = getApiExports(mainSourceFile); - - const packageDocumentationHeader = getPackageDocumentationText(mainSourceFile); - const newFileHeader = `${generatedHeader}${packageDocumentationHeader}`; - - // This order is critical as alpha should include beta should include public. - // Legacy is separate and should not be included in any other level. But it - // may include public. - // (public) -> (legacy) - // `-> (beta) -> (alpha) - const apiTagLevels: readonly Exclude[] = [ - ApiTag.public, - ApiTag.legacy, - ApiTag.beta, - ApiTag.alpha, - ] as const; - let commonNamedExports: Omit[] = []; - - if (exports.unknown.size > 0) { - log.errorLog( - `${exports.unknown.size} export(s) found without a recognized API level tag:\n\t${[ - ...exports.unknown.entries(), - ] - .map( - ([name, { exportedDecl, exportDecl }]) => - `${name} from ${sourceContext(exportedDecl)}${ - exportDecl === undefined ? "" : ` via ${sourceContext(exportDecl)}` - }`, - ) - .join(`\n\t`)}`, - ); - - // 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" }); - } - commonNamedExports[0].leadingTrivia = `\n\t// Unrestricted APIs\n\t`; - commonNamedExports[commonNamedExports.length - 1].trailingTrivia = "\n"; - } - - for (const apiTagLevel of apiTagLevels) { - const namedExports = [...commonNamedExports]; - - // 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)); - 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 (apiTagLevel !== "beta") { - // update common set - commonNamedExports = namedExports; - } - } - - const output = mapApiTagLevelToOutput.get(apiTagLevel); - if (output === undefined) { - continue; - } - - const outFile = output.relPath; - log.info(`\tGenerating ${outFile}`); - const sourceFile = project.createSourceFile(outFile, undefined, { - overwrite: true, - scriptKind: ScriptKind.TS, - }); - - // Avoid adding export declaration unless there are exports. - // Adding one without any named exports results in a * export (everything). - if (namedExports.length > 0) { - sourceFile.insertText(0, newFileHeader); - sourceFile.addExportDeclaration({ - leadingTrivia: "\n", - moduleSpecifier: `../${mainSourceFile - .getBaseName() - .replace(/\.(?:d\.)?([cm]?)ts$/, ".$1js")}`, - namedExports, - isTypeOnly: mapApiTagLevelToOutput.get(apiTagLevel)?.isTypeOnly, - }); - } else { - // At this point we already know that package "export" has a request - // for this entrypoint. Warn of emptiness, but make it valid for use. - log.warning(`no exports for ${outFile} using API level tag ${apiTagLevel}`); - sourceFile.insertText(0, `${newFileHeader} export {};\n\n`); - } - - fileSavePromises.push(sourceFile.save()); - } - - await Promise.all(fileSavePromises); -} diff --git a/build-tools/packages/build-cli/src/library/commands/index.ts b/build-tools/packages/build-cli/src/library/commands/index.ts index cd1320f7c16e..4aad95205d76 100644 --- a/build-tools/packages/build-cli/src/library/commands/index.ts +++ b/build-tools/packages/build-cli/src/library/commands/index.ts @@ -9,4 +9,3 @@ export { GenerateEntrypointsCommand, getGenerateEntrypointsOutput, } from "./generateEntrypoints.js"; -export { GenerateSourceEntrypointsCommand } from "./generateSourceEntrypoints.js"; diff --git a/build-tools/packages/build-cli/src/library/index.ts b/build-tools/packages/build-cli/src/library/index.ts index 93ab035ede7b..77592bfcae30 100644 --- a/build-tools/packages/build-cli/src/library/index.ts +++ b/build-tools/packages/build-cli/src/library/index.ts @@ -30,7 +30,6 @@ export { unscopedPackageNameString, BaseCommand, GenerateEntrypointsCommand, - GenerateSourceEntrypointsCommand, } from "./commands/index.js"; export { Context, VersionDetails, isMonoRepoKind, MonoRepoKind } from "./context.js"; export { Repository } from "./git.js"; diff --git a/build-tools/packages/build-cli/src/library/packageExports.ts b/build-tools/packages/build-cli/src/library/packageExports.ts index d6ca2bb05768..3e9519019ab7 100644 --- a/build-tools/packages/build-cli/src/library/packageExports.ts +++ b/build-tools/packages/build-cli/src/library/packageExports.ts @@ -244,135 +244,3 @@ export function queryTypesResolutionPathsFromPackageExports( return { mapKeyToOutput, mapNode10CompatExportPathToData, mapTypesPathToExportPaths }; } - -export function queryDefaultResolutionPathsFromPackageExports( - packageJson: PackageJson, - mapSrcQueryPathToOutKey: ReadonlyMap, - emitDeclarationOnly: boolean, - logger?: Logger, -): { - mapKeyToOutput: Map; - mapDefaultPathToExportPaths: Map[]>; -} { - const mapKeyToOutput = new Map(); - const mapDefaultPathToExportPaths = new Map[]>(); - - const { exports } = packageJson; - if (typeof exports !== "object" || exports === null) { - throw new Error('no valid "exports" within package properties'); - } - - if (Array.isArray(exports)) { - // eslint-disable-next-line unicorn/prefer-type-error - throw new Error(`required entrypoints cannot be generated for "exports" array`); - } - - // Iterate through exports looking for properties with values matching keys in map. - for (const [exportPath, exportValue] of Object.entries(exports)) { - if (typeof exportValue !== "object") { - logger?.verbose(`ignoring non-object export path "${exportPath}": "${exportValue}"`); - continue; - } - if (exportValue === null) { - logger?.verbose(`ignoring null export path "${exportPath}"`); - continue; - } - if (Array.isArray(exportValue)) { - logger?.verbose(`ignoring array export path "${exportPath}"`); - continue; - } - - // fix this - const findResults = findDefaultPathsMatching( - mapSrcQueryPathToOutKey, - exportValue, - emitDeclarationOnly, - { - conditions: [], - }, - ); - for (const findResult of findResults) { - const { outKey, relPath, conditions, isTypeOnly } = findResult; - - const existingExportsPaths = mapDefaultPathToExportPaths.get(relPath); - if (existingExportsPaths === undefined) { - mapDefaultPathToExportPaths.set(relPath, [{ exportPath }]); - } else { - existingExportsPaths.push({ exportPath }); - } - - // Add mapping for using given key, if defined. - if (outKey !== undefined) { - if (mapKeyToOutput.has(outKey)) { - logger?.warning(`${relPath} found in exports multiple times.`); - } else { - mapKeyToOutput.set(outKey, { relPath, conditions, isTypeOnly }); - } - } - } - } - - return { mapKeyToOutput, mapDefaultPathToExportPaths }; -} - -function findDefaultPathsMatching( - mapQueryPathToOutKey: ReadonlyMap, - exports: Readonly, - emitDeclarationOnly: boolean, - previous: Readonly<{ - conditions: readonly string[]; - isTypeOnly?: boolean; - }>, -): (ExportData & { outKey: TOutKey | undefined })[] { - const results: (ExportData & { outKey: TOutKey | undefined })[] = []; - - const entries = Object.entries(exports); - for (const [entry, value] of entries) { - // First check if this entry is a leaf; where value is only expected to be a string (a relative file path). - let relPath: string; - let isTypeOnly: boolean = false; - if (typeof value === "string") { - if (emitDeclarationOnly && entry === typesExportCondition) { - relPath = value.replace(/(lib|dist)/g, "src").replace(/\.d.ts$/, ".ts"); - isTypeOnly = true; - } else if (!emitDeclarationOnly && entry === defaultExportCondition) { - relPath = value.replace(/(lib|dist)/g, "src").replace(/\.js$/, ".ts"); - } else { - continue; - } - - const queryResult = valueOfFirstKeyMatching(relPath, mapQueryPathToOutKey); - if (queryResult !== undefined) { - results.push({ - outKey: queryResult.value, - relPath, - conditions: [], - isTypeOnly, - }); - } - } else if (typeof value !== "string" && value !== null) { - // Type constrain away array set that is not supported (and not expected - // but non-fatal to known use cases). - if (Array.isArray(value)) { - continue; - } - const deepFind = findDefaultPathsMatching( - mapQueryPathToOutKey, - value, - emitDeclarationOnly, - { - conditions: [], - }, - ); - if (deepFind !== undefined) { - results.push(...deepFind); - } - } - - if (results.length > 0) { - return results; - } - } - - return results; -} diff --git a/package.json b/package.json index d259a815c423..8fc36328e42c 100644 --- a/package.json +++ b/package.json @@ -369,8 +369,6 @@ "socket.io-client has an issue with 4.8.0 which breaks the build, so avoid it: https://github.com/socketio/socket.io/issues/5202" ], "overrides": { - "@fluid-tools/build-cli": "link:build-tools/packages/build-cli", - "@fluidframework/build-tools": "link:build-tools/packages/build-tools", "@types/node@<18": "^18.19.0", "get-tsconfig": "^4.7.3", "node-fetch": "^2.6.9",