Skip to content

Commit aad5ebb

Browse files
feat(build-tools): modify fluid-imports /legacy support (#20672)
- refactor ApiTag from ApiLevel - replace MemberData with name to level map where special cases are already accounted for - add Legacy to ApiLevel - detect /legacy export for `@alpha` tag mapping --------- Co-authored-by: Tyler Butler <[email protected]>
1 parent 558c48d commit aad5ebb

File tree

7 files changed

+174
-117
lines changed

7 files changed

+174
-117
lines changed

build-tools/packages/build-cli/docs/modify.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Modify commands are used to modify projects to add or remove dependencies, updat
77

88
## `flub modify fluid-imports`
99

10-
Rewrite imports for Fluid Framework APIs to use the correct subpath import (/alpha, /beta. etc.)
10+
Rewrite imports for Fluid Framework APIs to use the correct subpath import (/beta, /legacy, etc.)
1111

1212
```
1313
USAGE
@@ -17,7 +17,7 @@ USAGE
1717
FLAGS
1818
--data=<value> Optional path to a data file containing raw API level data. Overrides API levels extracted
1919
from package data.
20-
--onlyInternal Use /internal for all non-public APIs instead of /alpha or /beta.
20+
--onlyInternal Use /internal for all non-public APIs instead of /beta or /legacy.
2121
--packageRegex=<value> Regular expression filtering import packages to adjust
2222
--tsconfigs=<value>... [default: ./tsconfig.json] Tsconfig file paths that will be used to load project files. When
2323
multiple are given all must depend on the same version of packages; otherwise results are
@@ -28,7 +28,7 @@ LOGGING FLAGS
2828
--quiet Disable all logging.
2929
3030
DESCRIPTION
31-
Rewrite imports for Fluid Framework APIs to use the correct subpath import (/alpha, /beta. etc.)
31+
Rewrite imports for Fluid Framework APIs to use the correct subpath import (/beta, /legacy, etc.)
3232
```
3333

3434
_See code: [src/commands/modify/fluid-imports.ts](https://github.com/microsoft/FluidFramework/blob/main/build-tools/packages/build-cli/src/commands/modify/fluid-imports.ts)_

build-tools/packages/build-cli/src/commands/generate/entrypoints.ts

+41-39
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { ExportSpecifierStructure, Node } from "ts-morph";
1212
import { ModuleKind, Project, ScriptKind } from "ts-morph";
1313

1414
import { BaseCommand } from "../../base";
15-
import { ApiLevel, getApiExports } from "../../library";
15+
import { ApiLevel, ApiTag, getApiExports } from "../../library";
1616
import type { CommandLogger } from "../../logging";
1717

1818
/**
@@ -96,23 +96,23 @@ export default class GenerateEntrypointsCommand extends BaseCommand<
9696

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

99-
const mapQueryPathToApiLevel: Map<string | RegExp, ApiLevel | undefined> = new Map([
100-
[`${pathPrefix}${outFileAlpha}${outFileSuffix}`, ApiLevel.alpha],
101-
[`${pathPrefix}${outFileBeta}${outFileSuffix}`, ApiLevel.beta],
102-
[`${pathPrefix}${outFilePublic}${outFileSuffix}`, ApiLevel.public],
99+
const mapQueryPathToApiTagLevel: Map<string | RegExp, ApiTag | undefined> = new Map([
100+
[`${pathPrefix}${outFileAlpha}${outFileSuffix}`, ApiTag.alpha],
101+
[`${pathPrefix}${outFileBeta}${outFileSuffix}`, ApiTag.beta],
102+
[`${pathPrefix}${outFilePublic}${outFileSuffix}`, ApiTag.public],
103103
]);
104104

105105
if (node10TypeCompat) {
106106
// /internal export may be supported without API level generation; so
107107
// add query for such path for Node10 type compat generation.
108108
const dirPath = pathPrefix.replace(/\/[^/]*$/, "");
109109
const internalPathRegex = new RegExp(`${dirPath}\\/index\\.d\\.?[cm]?ts$`);
110-
mapQueryPathToApiLevel.set(internalPathRegex, undefined);
110+
mapQueryPathToApiTagLevel.set(internalPathRegex, undefined);
111111
}
112112

113-
const { mapApiLevelToOutputPath, mapExportPathToData } = buildOutputMaps(
113+
const { mapApiTagLevelToOutputPath, mapExportPathToData } = buildOutputMaps(
114114
packageJson,
115-
mapQueryPathToApiLevel,
115+
mapQueryPathToApiTagLevel,
116116
node10TypeCompat,
117117
this.logger,
118118
);
@@ -121,11 +121,11 @@ export default class GenerateEntrypointsCommand extends BaseCommand<
121121

122122
// Requested specific outputs that are not in the output map are explicitly
123123
// removed for clean incremental build support.
124-
for (const [outputPath, apiLevel] of mapQueryPathToApiLevel.entries()) {
124+
for (const [outputPath, apiLevel] of mapQueryPathToApiTagLevel.entries()) {
125125
if (
126126
apiLevel !== undefined &&
127127
typeof outputPath === "string" &&
128-
!mapApiLevelToOutputPath.has(apiLevel)
128+
!mapApiTagLevelToOutputPath.has(apiLevel)
129129
) {
130130
promises.push(fs.rm(outputPath, { force: true }));
131131
}
@@ -137,15 +137,17 @@ export default class GenerateEntrypointsCommand extends BaseCommand<
137137
);
138138
}
139139

140-
if (mapApiLevelToOutputPath.size === 0) {
140+
if (mapApiTagLevelToOutputPath.size === 0) {
141141
throw new Error(
142142
`There are no package exports matching requested output entrypoints:\n\t${[
143-
...mapQueryPathToApiLevel.keys(),
143+
...mapQueryPathToApiTagLevel.keys(),
144144
].join("\n\t")}`,
145145
);
146146
}
147147

148-
promises.push(generateEntrypoints(mainEntrypoint, mapApiLevelToOutputPath, this.logger));
148+
promises.push(
149+
generateEntrypoints(mainEntrypoint, mapApiTagLevelToOutputPath, this.logger),
150+
);
149151

150152
if (node10TypeCompat) {
151153
promises.push(generateNode10TypeEntrypoints(mapExportPathToData, this.logger));
@@ -202,13 +204,13 @@ function getLocalUnscopedPackageName(packageJson: PackageJson): string {
202204
type ExportsRecordValue = Exclude<Extract<PackageJson["exports"], object>, unknown[]>;
203205

204206
function findTypesPathMatching(
205-
mapQueryPathToApiLevel: Map<string | RegExp, ApiLevel | undefined>,
207+
mapQueryPathToApiTagLevel: Map<string | RegExp, ApiTag | undefined>,
206208
exports: ExportsRecordValue,
207-
): { apiLevel: ApiLevel | undefined; relPath: string; isTypeOnly: boolean } | undefined {
209+
): { apiTagLevel: ApiTag | undefined; relPath: string; isTypeOnly: boolean } | undefined {
208210
for (const [entry, value] of Object.entries(exports)) {
209211
if (typeof value === "string") {
210212
if (entry === "types") {
211-
for (const [key, apiLevel] of mapQueryPathToApiLevel.entries()) {
213+
for (const [key, apiTagLevel] of mapQueryPathToApiTagLevel.entries()) {
212214
// eslint-disable-next-line max-depth
213215
if (
214216
typeof key === "string"
@@ -220,15 +222,15 @@ function findTypesPathMatching(
220222
"import" in exports ||
221223
"require" in exports
222224
);
223-
return { apiLevel, relPath: value, isTypeOnly };
225+
return { apiTagLevel, relPath: value, isTypeOnly };
224226
}
225227
}
226228
}
227229
} else if (value !== null) {
228230
if (Array.isArray(value)) {
229231
continue;
230232
}
231-
const deepFind = findTypesPathMatching(mapQueryPathToApiLevel, value);
233+
const deepFind = findTypesPathMatching(mapQueryPathToApiTagLevel, value);
232234
if (deepFind !== undefined) {
233235
return deepFind;
234236
}
@@ -240,14 +242,14 @@ function findTypesPathMatching(
240242

241243
function buildOutputMaps(
242244
packageJson: PackageJson,
243-
mapQueryPathToApiLevel: Map<string | RegExp, ApiLevel | undefined>,
245+
mapQueryPathToApiTagLevel: Map<string | RegExp, ApiTag | undefined>,
244246
node10TypeCompat: boolean,
245247
log: CommandLogger,
246248
): {
247-
mapApiLevelToOutputPath: Map<ApiLevel, string>;
249+
mapApiTagLevelToOutputPath: Map<ApiTag, string>;
248250
mapExportPathToData: Map<string, ExportData>;
249251
} {
250-
const mapApiLevelToOutputPath = new Map<ApiLevel, string>();
252+
const mapApiTagLevelToOutputPath = new Map<ApiTag, string>();
251253
const mapExportPathToData = new Map<string, ExportData>();
252254

253255
const { exports } = packageJson;
@@ -275,16 +277,16 @@ function buildOutputMaps(
275277
continue;
276278
}
277279

278-
const findResult = findTypesPathMatching(mapQueryPathToApiLevel, exportValue);
280+
const findResult = findTypesPathMatching(mapQueryPathToApiTagLevel, exportValue);
279281
if (findResult !== undefined) {
280-
const { apiLevel, relPath, isTypeOnly } = findResult;
282+
const { apiTagLevel, relPath, isTypeOnly } = findResult;
281283

282284
// Add mapping for API level file generation
283-
if (apiLevel !== undefined) {
284-
if (mapApiLevelToOutputPath.has(apiLevel)) {
285+
if (apiTagLevel !== undefined) {
286+
if (mapApiTagLevelToOutputPath.has(apiTagLevel)) {
285287
log.warning(`${relPath} found in exports multiple times.`);
286288
} else {
287-
mapApiLevelToOutputPath.set(apiLevel, relPath);
289+
mapApiTagLevelToOutputPath.set(apiTagLevel, relPath);
288290
}
289291
}
290292

@@ -303,7 +305,7 @@ function buildOutputMaps(
303305
}
304306
}
305307

306-
return { mapApiLevelToOutputPath, mapExportPathToData: mapExportPathToData };
308+
return { mapApiTagLevelToOutputPath, mapExportPathToData };
307309
}
308310

309311
function sourceContext(node: Node): string {
@@ -324,7 +326,7 @@ const generatedHeader: string = `/*!
324326

325327
async function generateEntrypoints(
326328
mainEntrypoint: string,
327-
mapApiLevelToOutput: Map<ApiLevel, string>,
329+
mapApiTagLevelToOutput: Map<ApiTag, string>,
328330
log: CommandLogger,
329331
): Promise<void> {
330332
/**
@@ -347,16 +349,16 @@ async function generateEntrypoints(
347349
const exports = getApiExports(mainSourceFile);
348350

349351
// This order is critical as public should include beta should include alpha.
350-
const apiLevels: readonly Exclude<ApiLevel, typeof ApiLevel.internal>[] = [
351-
ApiLevel.public,
352-
ApiLevel.beta,
353-
ApiLevel.alpha,
352+
const apiTagLevels: readonly Exclude<ApiTag, typeof ApiTag.internal>[] = [
353+
ApiTag.public,
354+
ApiTag.beta,
355+
ApiTag.alpha,
354356
] as const;
355357
const namedExports: Omit<ExportSpecifierStructure, "kind">[] = [];
356358

357359
if (exports.unknown.size > 0) {
358360
log.errorLog(
359-
`${exports.unknown.size} export(s) found without a recognized API level:\n\t${[
361+
`${exports.unknown.size} export(s) found without a recognized API level tag:\n\t${[
360362
...exports.unknown.entries(),
361363
]
362364
.map(
@@ -376,19 +378,19 @@ async function generateEntrypoints(
376378
namedExports[namedExports.length - 1].trailingTrivia = "\n";
377379
}
378380

379-
for (const apiLevel of apiLevels) {
380-
// Append this levels additional (or only) exports sorted by ascending case-sensitive name
381+
for (const apiTagLevel of apiTagLevels) {
382+
// Append this level's additional (or only) exports sorted by ascending case-sensitive name
381383
const orgLength = namedExports.length;
382-
const levelExports = [...exports[apiLevel]].sort((a, b) => (a.name > b.name ? 1 : -1));
384+
const levelExports = [...exports[apiTagLevel]].sort((a, b) => (a.name > b.name ? 1 : -1));
383385
for (const levelExport of levelExports) {
384386
namedExports.push({ ...levelExport, leadingTrivia: "\n\t" });
385387
}
386388
if (namedExports.length > orgLength) {
387-
namedExports[orgLength].leadingTrivia = `\n\t// ${apiLevel} APIs\n\t`;
389+
namedExports[orgLength].leadingTrivia = `\n\t// @${apiTagLevel} APIs\n\t`;
388390
namedExports[namedExports.length - 1].trailingTrivia = "\n";
389391
}
390392

391-
const outFile = mapApiLevelToOutput.get(apiLevel);
393+
const outFile = mapApiTagLevelToOutput.get(apiTagLevel);
392394
if (outFile === undefined) {
393395
continue;
394396
}
@@ -413,7 +415,7 @@ async function generateEntrypoints(
413415
} else {
414416
// At this point we already know that package "export" has a request
415417
// for this entrypoint. Warn of emptiness, but make it valid for use.
416-
log.warning(`no exports for ${outFile} using API level ${apiLevel}`);
418+
log.warning(`no exports for ${outFile} using API level tag ${apiTagLevel}`);
417419
sourceFile.insertText(0, `${generatedHeader}export {}\n\n`);
418420
}
419421

0 commit comments

Comments
 (0)