feat(build-tools): Add generate:node10Entrypoints command (#22937)
This PR add a new command to generate node10 type declaration entrypoints. Most of the node10 code is extracted from [generate entrypoint](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/library/commands/generateEntrypoints.ts) command.
This commit is contained in:
Родитель
0c1886ec38
Коммит
533de79180
|
@ -9,6 +9,7 @@ Generate commands are used to create/update code, docs, readmes, etc.
|
|||
* [`flub generate changelog`](#flub-generate-changelog)
|
||||
* [`flub generate changeset`](#flub-generate-changeset)
|
||||
* [`flub generate entrypoints`](#flub-generate-entrypoints)
|
||||
* [`flub generate node10Entrypoints`](#flub-generate-node10entrypoints)
|
||||
* [`flub generate packlist`](#flub-generate-packlist)
|
||||
* [`flub generate releaseNotes`](#flub-generate-releasenotes)
|
||||
* [`flub generate typetests`](#flub-generate-typetests)
|
||||
|
@ -258,6 +259,25 @@ DESCRIPTION
|
|||
|
||||
_See code: [src/commands/generate/entrypoints.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/generate/entrypoints.ts)_
|
||||
|
||||
## `flub generate node10Entrypoints`
|
||||
|
||||
Generates node10 type declaration entrypoints for Fluid Framework API levels (/alpha, /beta, /internal etc.) as found in package.json "exports"
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ flub generate node10Entrypoints [-v | --quiet]
|
||||
|
||||
LOGGING FLAGS
|
||||
-v, --verbose Enable verbose logging.
|
||||
--quiet Disable all logging.
|
||||
|
||||
DESCRIPTION
|
||||
Generates node10 type declaration entrypoints for Fluid Framework API levels (/alpha, /beta, /internal etc.) as found
|
||||
in package.json "exports"
|
||||
```
|
||||
|
||||
_See code: [src/commands/generate/node10Entrypoints.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/generate/node10Entrypoints.ts)_
|
||||
|
||||
## `flub generate packlist`
|
||||
|
||||
Outputs a list of files that will be included in a package based on its 'files' property in package.json and any .npmignore files.
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/*!
|
||||
* 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 { ApiLevel, BaseCommand, knownApiLevels } from "../../library/index.js";
|
||||
// AB#8118 tracks removing the barrel files and importing directly from the submodules, including disabling this rule.
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import { readPackageJson, readTsConfig } from "../../library/package.js";
|
||||
|
||||
import {
|
||||
type Node10CompatExportData,
|
||||
getTypesPathFromPackage,
|
||||
// AB#8118 tracks removing the barrel files and importing directly from the submodules, including disabling this rule.
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
} from "../../library/packageExports.js";
|
||||
import type { CommandLogger } from "../../logging.js";
|
||||
|
||||
export default class GenerateNode10EntrypointsCommand extends BaseCommand<
|
||||
typeof GenerateNode10EntrypointsCommand
|
||||
> {
|
||||
static readonly description =
|
||||
`Generates node10 type declaration entrypoints for Fluid Framework API levels (/alpha, /beta, /internal etc.) as found in package.json "exports"`;
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const packageJson = await readPackageJson();
|
||||
|
||||
const tsconfig = await readTsConfig();
|
||||
|
||||
let emitDeclarationOnly = false;
|
||||
if (tsconfig.compilerOptions?.emitDeclarationOnly !== undefined) {
|
||||
emitDeclarationOnly = true;
|
||||
}
|
||||
|
||||
const mapNode10CompatExportPathToData = mapExportPathsFromPackage(
|
||||
packageJson,
|
||||
emitDeclarationOnly,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
if (mapNode10CompatExportPathToData.size === 0) {
|
||||
throw new Error(
|
||||
'There are no API level "exports" requiring Node10 type compatibility generation.',
|
||||
);
|
||||
}
|
||||
|
||||
await generateNode10TypeEntrypoints(mapNode10CompatExportPathToData, this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
export function mapExportPathsFromPackage(
|
||||
packageJson: PackageJson,
|
||||
emitDeclarationOnly: boolean,
|
||||
log: CommandLogger,
|
||||
): Map<string, Node10CompatExportData> {
|
||||
const mapKeyToOutput = new Map<string, Node10CompatExportData>();
|
||||
|
||||
// Iterate through exports looking for properties with values matching keys in map.
|
||||
for (const levels of knownApiLevels) {
|
||||
// Exclude root "." path as "types" should handle that.
|
||||
if (levels === ApiLevel.public) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const typesPath = getTypesPathFromPackage(packageJson, levels, log);
|
||||
|
||||
if (typesPath === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const node10ExportPath = typesPath
|
||||
.replace(/\/index(\.d\.[cm]?ts)?$/, "/internal$1")
|
||||
.replace(/^.*\//, "");
|
||||
|
||||
mapKeyToOutput.set(node10ExportPath, {
|
||||
relPath: typesPath,
|
||||
isTypeOnly: emitDeclarationOnly,
|
||||
});
|
||||
}
|
||||
|
||||
return mapKeyToOutput;
|
||||
}
|
||||
|
||||
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 node10Entrypoints" in @fluid-tools/build-cli.
|
||||
*/
|
||||
|
||||
`;
|
||||
|
||||
async function generateNode10TypeEntrypoints(
|
||||
mapExportPathToData: Map<string, Node10CompatExportData>,
|
||||
log: CommandLogger,
|
||||
): 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>[] = [];
|
||||
|
||||
for (const [outFile, { relPath, isTypeOnly }] of mapExportPathToData.entries()) {
|
||||
log.info(`\tGenerating ${outFile}`);
|
||||
const jsImport = relPath.replace(/\.d\.([cm]?)ts/, ".$1js");
|
||||
fileSavePromises.push(
|
||||
fs.writeFile(
|
||||
outFile,
|
||||
isTypeOnly
|
||||
? `${generatedHeader}export type * from "${relPath}";\n`
|
||||
: `${generatedHeader}export * from "${jsImport}";\n`,
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (fileSavePromises.length === 0) {
|
||||
log.info(`\tNo Node10 compat files generated.`);
|
||||
}
|
||||
|
||||
await Promise.all(fileSavePromises);
|
||||
}
|
|
@ -16,7 +16,6 @@ import { Flags } from "@oclif/core";
|
|||
import { PackageName } from "@rushstack/node-core-library";
|
||||
import * as changeCase from "change-case";
|
||||
import { readJson } from "fs-extra/esm";
|
||||
import * as resolve from "resolve.exports";
|
||||
import {
|
||||
JSDoc,
|
||||
ModuleKind,
|
||||
|
@ -37,6 +36,9 @@ import {
|
|||
} from "../../library/index.js";
|
||||
// AB#8118 tracks removing the barrel files and importing directly from the submodules, including disabling this rule.
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import { getTypesPathFromPackage } from "../../library/packageExports.js";
|
||||
// AB#8118 tracks removing the barrel files and importing directly from the submodules, including disabling this rule.
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import { type TestCaseTypeData, buildTestCase } from "../../typeValidator/testGeneration.js";
|
||||
// AB#8118 tracks removing the barrel files and importing directly from the submodules, including disabling this rule.
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
|
@ -221,91 +223,6 @@ function getTypesPathWithFallback(
|
|||
return { typesPath: typesPath, entrypointUsed: chosenEntrypoint };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the types of a package using the package's export map or types/typings field.
|
||||
* If the path is found, it is returned. Otherwise it returns undefined.
|
||||
*
|
||||
* This implementation uses resolve.exports to resolve the path to types for a level.
|
||||
*
|
||||
* @param packageJson - The package.json object to check for types paths.
|
||||
* @param level - An API level to get types paths for.
|
||||
* @returns A package relative path to the types.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This implementation loosely follows TypeScript's process for finding types as described at
|
||||
* {@link https://www.typescriptlang.org/docs/handbook/modules/reference.html#packagejson-main-and-types}. If an export
|
||||
* map is found, the `types` and `typings` field are ignored. If an export map is not found, then the `types`/`typings`
|
||||
* fields will be used as a fallback _only_ for the public API level (which corresponds to the default export).
|
||||
*
|
||||
* Importantly, this code _does not_ implement falling back to the `main` field when `types` and `typings` are missing,
|
||||
* nor does it look up types from DefinitelyTyped (i.e. \@types/* packages). This fallback logic is not needed for our
|
||||
* packages because we always specify types explicitly in the types field, and types are always included in our packages
|
||||
* (as opposed to a separate \@types package).
|
||||
*/
|
||||
export function getTypesPathFromPackage(
|
||||
packageJson: PackageJson,
|
||||
level: ApiLevel,
|
||||
log: Logger,
|
||||
): string | undefined {
|
||||
if (packageJson.exports === undefined) {
|
||||
log.verbose(`${packageJson.name}: No export map found.`);
|
||||
// Use types/typings field only when the public API level is used and no exports field is found
|
||||
if (level === ApiLevel.public) {
|
||||
log.verbose(`${packageJson.name}: Using the types/typings field value.`);
|
||||
return packageJson.types ?? packageJson.typings;
|
||||
}
|
||||
// No exports and a non-public API level, so return undefined.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Package has an export map, so map the requested API level to an entrypoint and check the exports conditions.
|
||||
const entrypoint = level === ApiLevel.public ? "." : `./${level}`;
|
||||
|
||||
// resolve.exports sets some conditions by default, so the ones we supply supplement the defaults. For clarity the
|
||||
// applied conditions are noted in comments.
|
||||
let typesPath: string | undefined;
|
||||
try {
|
||||
// First try to resolve with the "import" condition, assuming the package is either ESM-only or dual-format.
|
||||
// conditions: ["default", "types", "import", "node"]
|
||||
const exports = resolve.exports(packageJson, entrypoint, { conditions: ["types"] });
|
||||
|
||||
// resolve.exports returns a `Exports.Output | void` type, though the documentation isn't clear under what
|
||||
// conditions `void` would be the return type vs. just throwing an exception. Since the types say exports could be
|
||||
// undefined or an empty array (Exports.Output is an array type), check for those conditions.
|
||||
typesPath = exports === undefined || exports.length === 0 ? undefined : exports[0];
|
||||
} catch {
|
||||
// Catch and ignore any exceptions here; we'll retry with the require condition.
|
||||
log.verbose(
|
||||
`${packageJson.name}: No types found for ${entrypoint} using "import" condition.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Found the types using the import condition, so return early.
|
||||
if (typesPath !== undefined) {
|
||||
return typesPath;
|
||||
}
|
||||
|
||||
try {
|
||||
// If nothing is found when using the "import" condition, try the "require" condition. It may be possible to do this
|
||||
// in a single call to resolve.exports, but the documentation is a little unclear. This seems a safe, if inelegant
|
||||
// solution.
|
||||
// conditions: ["default", "types", "require", "node"]
|
||||
const exports = resolve.exports(packageJson, entrypoint, {
|
||||
conditions: ["types"],
|
||||
require: true,
|
||||
});
|
||||
typesPath = exports === undefined || exports.length === 0 ? undefined : exports[0];
|
||||
} catch {
|
||||
// Catch and ignore any exceptions here; we'll retry with the require condition.
|
||||
log.verbose(
|
||||
`${packageJson.name}: No types found for ${entrypoint} using "require" condition.`,
|
||||
);
|
||||
}
|
||||
|
||||
return typesPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the file path for type validation tests.
|
||||
*
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { strict as assert } from "node:assert";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
InterdependencyRange,
|
||||
|
@ -28,12 +28,14 @@ import { PackageName } from "@rushstack/node-core-library";
|
|||
import { compareDesc, differenceInBusinessDays } from "date-fns";
|
||||
import execa from "execa";
|
||||
import { readJson, readJsonSync } from "fs-extra/esm";
|
||||
import JSON5 from "json5";
|
||||
import latestVersion from "latest-version";
|
||||
import ncu from "npm-check-updates";
|
||||
import type { Index } from "npm-check-updates/build/src/types/IndexType.js";
|
||||
import type { VersionSpec } from "npm-check-updates/build/src/types/VersionSpec.js";
|
||||
import * as semver from "semver";
|
||||
|
||||
import type { TsConfigJson } from "type-fest";
|
||||
import {
|
||||
AllPackagesSelectionCriteria,
|
||||
PackageSelectionCriteria,
|
||||
|
@ -936,3 +938,18 @@ export function getTarballName(pkg: PackageJson | string): string {
|
|||
export function getFullTarballName(pkg: PackageJson): string {
|
||||
return `${getTarballName(pkg)}-${pkg?.version ?? 0}.tgz`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses the `package.json` file in the current directory.
|
||||
* Use this function if you prefer the CLI command not to be implemented as `PackageCommand`command.
|
||||
*/
|
||||
export async function readPackageJson(): Promise<PackageJson> {
|
||||
const packageJson = await readFile("./package.json", { encoding: "utf8" });
|
||||
return JSON.parse(packageJson) as PackageJson;
|
||||
}
|
||||
|
||||
// Reads and parses the `tsconfig.json` file in the current directory.
|
||||
export async function readTsConfig(): Promise<TsConfigJson> {
|
||||
const tsConfigContent = await readFile("./tsconfig.json", { encoding: "utf8" });
|
||||
return JSON5.parse(tsConfigContent);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
import path from "node:path";
|
||||
|
||||
import type { Logger, PackageJson } from "@fluidframework/build-tools";
|
||||
import * as resolve from "resolve.exports";
|
||||
import { ApiLevel } from "./apiLevel.js";
|
||||
|
||||
/**
|
||||
* Properties for an "exports" leaf entry block in package.json.
|
||||
|
@ -244,3 +246,88 @@ export function queryTypesResolutionPathsFromPackageExports<TOutKey>(
|
|||
|
||||
return { mapKeyToOutput, mapNode10CompatExportPathToData, mapTypesPathToExportPaths };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the types of a package using the package's export map or types/typings field.
|
||||
* If the path is found, it is returned. Otherwise it returns undefined.
|
||||
*
|
||||
* This implementation uses resolve.exports to resolve the path to types for a level.
|
||||
*
|
||||
* @param packageJson - The package.json object to check for types paths.
|
||||
* @param level - An API level to get types paths for.
|
||||
* @returns A package relative path to the types.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This implementation loosely follows TypeScript's process for finding types as described at
|
||||
* {@link https://www.typescriptlang.org/docs/handbook/modules/reference.html#packagejson-main-and-types}. If an export
|
||||
* map is found, the `types` and `typings` field are ignored. If an export map is not found, then the `types`/`typings`
|
||||
* fields will be used as a fallback _only_ for the public API level (which corresponds to the default export).
|
||||
*
|
||||
* Importantly, this code _does not_ implement falling back to the `main` field when `types` and `typings` are missing,
|
||||
* nor does it look up types from DefinitelyTyped (i.e. \@types/* packages). This fallback logic is not needed for our
|
||||
* packages because we always specify types explicitly in the types field, and types are always included in our packages
|
||||
* (as opposed to a separate \@types package).
|
||||
*/
|
||||
export function getTypesPathFromPackage(
|
||||
packageJson: PackageJson,
|
||||
level: ApiLevel,
|
||||
log: Logger,
|
||||
): string | undefined {
|
||||
if (packageJson.exports === undefined) {
|
||||
log.verbose(`${packageJson.name}: No export map found.`);
|
||||
// Use types/typings field only when the public API level is used and no exports field is found
|
||||
if (level === ApiLevel.public) {
|
||||
log.verbose(`${packageJson.name}: Using the types/typings field value.`);
|
||||
return packageJson.types ?? packageJson.typings;
|
||||
}
|
||||
// No exports and a non-public API level, so return undefined.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Package has an export map, so map the requested API level to an entrypoint and check the exports conditions.
|
||||
const entrypoint = level === ApiLevel.public ? "." : `./${level}`;
|
||||
|
||||
// resolve.exports sets some conditions by default, so the ones we supply supplement the defaults. For clarity the
|
||||
// applied conditions are noted in comments.
|
||||
let typesPath: string | undefined;
|
||||
try {
|
||||
// First try to resolve with the "import" condition, assuming the package is either ESM-only or dual-format.
|
||||
// conditions: ["default", "types", "import", "node"]
|
||||
const exports = resolve.exports(packageJson, entrypoint, { conditions: ["types"] });
|
||||
|
||||
// resolve.exports returns a `Exports.Output | void` type, though the documentation isn't clear under what
|
||||
// conditions `void` would be the return type vs. just throwing an exception. Since the types say exports could be
|
||||
// undefined or an empty array (Exports.Output is an array type), check for those conditions.
|
||||
typesPath = exports === undefined || exports.length === 0 ? undefined : exports[0];
|
||||
} catch {
|
||||
// Catch and ignore any exceptions here; we'll retry with the require condition.
|
||||
log.verbose(
|
||||
`${packageJson.name}: No types found for ${entrypoint} using "import" condition.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Found the types using the import condition, so return early.
|
||||
if (typesPath !== undefined) {
|
||||
return typesPath;
|
||||
}
|
||||
|
||||
try {
|
||||
// If nothing is found when using the "import" condition, try the "require" condition. It may be possible to do this
|
||||
// in a single call to resolve.exports, but the documentation is a little unclear. This seems a safe, if inelegant
|
||||
// solution.
|
||||
// conditions: ["default", "types", "require", "node"]
|
||||
const exports = resolve.exports(packageJson, entrypoint, {
|
||||
conditions: ["types"],
|
||||
require: true,
|
||||
});
|
||||
typesPath = exports === undefined || exports.length === 0 ? undefined : exports[0];
|
||||
} catch {
|
||||
// Catch and ignore any exceptions here; we'll retry with the require condition.
|
||||
log.verbose(
|
||||
`${packageJson.name}: No types found for ${entrypoint} using "require" condition.`,
|
||||
);
|
||||
}
|
||||
|
||||
return typesPath;
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче