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

Improve how we handle Static Helpers #2716

Merged
merged 22 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/typespec-ts/eng/scripts/copy-static-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { copyFile, mkdir, readdir } from "fs/promises";
import path from "path";

const __dirname = path.dirname(new URL(import.meta.url).pathname);

async function copyFiles(srcDir, destDir, fileFilter) {
try {
const entries = await readdir(srcDir, { withFileTypes: true });
await mkdir(destDir, { recursive: true });

for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name);
const destPath = path.join(destDir, entry.name);

if (entry.isDirectory()) {
await copyFiles(srcPath, destPath, fileFilter);
} else if (entry.isFile() && fileFilter(srcPath)) {
await copyFile(srcPath, destPath);
}
}
} catch (error) {
console.error(`Error copying files from ${srcDir} to ${destDir}:`, error);
process.exit(1); // Exit with a non-zero status code on error
}
}

const srcDir = path.resolve(__dirname, "../../src/modular/static-helpers");
const destDir = path.resolve(
__dirname,
"../../dist/src/modular/static-helpers"
joheredi marked this conversation as resolved.
Show resolved Hide resolved
);

// Copy only .ts files
try {
await copyFiles(srcDir, destDir, (src) => src.endsWith(".ts"));
console.log("All files copied successfully.");
} catch (error) {
console.error("Error during the copy operation:", error);
process.exit(1); // Exit with a non-zero status code on error
}
5 changes: 3 additions & 2 deletions packages/typespec-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
"test-next": "vitest run ./test-next",
"test-next:coverage": "vitest run ./test-next --coverage",
"clean": "rimraf ./dist ./typespec-output",
"build": "tsc -p .",
"build": "tsc -p . && npm run copy-static-helpers",
joheredi marked this conversation as resolved.
Show resolved Hide resolved
"test": "npm run test-next && npm run unit-test && npm run integration-test-ci",
"lint": "eslint src --ext .ts --max-warnings=0",
"lint:fix": "eslint src --fix --ext .ts",
"format": "npm run -s prettier -- --write",
"check-format": "npm run prettier -- --check",
"prettier": "prettier --config ./.prettierrc \"src/**/*.ts\"",
"copy-static-helpers": "node ./eng/scripts/copy-static-helpers.js",
"check:tree": "node --loader ts-node/esm ./test/commands/check-clean-tree.ts",
"integration-test-ci": "npm-run-all copy:typespec integration-test-ci:rlc integration-test-ci:modular integration-test-ci:non-branded-rlc integration-test-ci:non-branded-modular",
"integration-test-ci:sequential": "npm-run-all --serial copy:typespec integration-test-ci:modular && npm run integration-test-ci:modular",
Expand Down Expand Up @@ -139,4 +140,4 @@
"url": "https://github.com/Azure/autorest.typescript/issues"
},
"homepage": "https://github.com/Azure/autorest.typescript/tree/main/packages/typespec-ts/"
}
}
59 changes: 0 additions & 59 deletions packages/typespec-ts/src/azure/dependency.ts

This file was deleted.

65 changes: 3 additions & 62 deletions packages/typespec-ts/src/framework/dependency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ReferenceableSymbol {
kind: string;
name: string;
module: string;
visibility?: "internal" | "public";
}

export type ExternalDependencies = CoreDependencies &
Expand All @@ -25,9 +26,9 @@ export interface CoreDependencies extends Record<string, ReferenceableSymbol> {
name: "OperationOptions";
module: string;
};
PathUnckeckedResponse: {
PathUncheckedResponse: {
kind: "externalDependency";
name: "PathUnckeckedResponse";
name: "PathUncheckedResponse";
module: string;
};
AbortSignalLike: {
Expand All @@ -52,64 +53,4 @@ export interface CoreDependencies extends Record<string, ReferenceableSymbol> {
};
}

const _CoreDependencies: CoreDependencies = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving these to modular/external-dependencies.ts

Client: {
kind: "externalDependency",
name: "Client",
module: "@typespec/ts-http-runtime"
},
ClientOptions: {
kind: "externalDependency",
name: "ClientOptions",
module: "@typespec/ts-http-runtime"
},
Pipeline: {
kind: "externalDependency",
name: "Pipeline",
module: "@typespec/ts-http-runtime"
},
getClient: {
kind: "externalDependency",
name: "getClient",
module: "@typespec/ts-http-runtime"
},
RestError: {
kind: "externalDependency",
name: "RestError",
module: "@typespec/ts-http-runtime"
},
OperationOptions: {
kind: "externalDependency",
name: "OperationOptions",
module: "@typespec/ts-http-runtime"
},
PathUnckeckedResponse: {
kind: "externalDependency",
name: "PathUnckeckedResponse",
module: "@typespec/ts-http-runtime"
},
AbortSignalLike: {
kind: "externalDependency",
name: "AbortSignalLike",
module: "@typespec/ts-http-runtime"
},
createRestError: {
kind: "externalDependency",
name: "createRestError",
module: "@typespec/ts-http-runtime"
},
operationOptionsToRequestParameters: {
kind: "externalDependency",
name: "operationOptionsToRequestParameters",
module: "@typespec/ts-http-runtime"
},
uint8ArrayToString: {
kind: "externalDependency",
name: "uint8ArrayToString",
module: "@typespec/ts-http-runtime"
}
} as const;

export const DEFAULT_DEPENDENCIES = _CoreDependencies;

export type CoreDependency = keyof CoreDependencies;
86 changes: 73 additions & 13 deletions packages/typespec-ts/src/framework/hooks/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { provideContext, useContext } from "../../contextManager.js";
import { ReferenceableSymbol } from "../dependency.js";
import { provideDependencies, useDependencies } from "./useDependencies.js";
import { refkey } from "../refkey.js";
import {
isStaticHelperMetadata,
SourceFileSymbol,
StaticHelperMetadata
} from "../load-static-helpers.js";

export interface DeclarationInfo {
name: string;
Expand All @@ -17,6 +22,7 @@ export interface DeclarationInfo {
}

export interface BinderOptions {
staticHelpers?: Map<string, StaticHelperMetadata>;
dependencies?: Record<string, ReferenceableSymbol>;
}

Expand All @@ -39,15 +45,18 @@ export interface Binder {

class BinderImp implements Binder {
private declarations = new Map<unknown, DeclarationInfo>();
private references = new Map<unknown, Set<SourceFile>>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this map to track things that are used, this is helpful to cleanup unused static-helpers

private imports = new Map<SourceFile, ImportDeclarationStructure[]>();
private symbolsBySourceFile = new Map<SourceFile, Set<string>>();
private project: Project;
private dependencies: Record<string, ReferenceableSymbol>;
private staticHelpers: Map<string, StaticHelperMetadata>;

constructor(project: Project, options: BinderOptions = {}) {
this.project = project;

provideDependencies(options.dependencies);
this.staticHelpers = options.staticHelpers ?? new Map();
this.dependencies = useDependencies();
}

Expand Down Expand Up @@ -146,7 +155,7 @@ class BinderImp implements Binder {
* @returns The serialized placeholder string.
*/
private serializePlaceholder(refkey: unknown): string {
return `"<PLACEHOLDER:${String(refkey)}>"`;
return `_PLACEHOLDER_${String(refkey)}_`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This format is safer with ts-morph. Sometimes the double quotes and < may cause unexpected ts-morph behavior. This makes the placeholder a valid identifier

}

/**
Expand All @@ -172,7 +181,7 @@ class BinderImp implements Binder {
moduleSpecifier =
fileWhereImportIsAdded.getRelativePathAsModuleSpecifierTo(
fileWhereImportPointsTo
);
) + ".js";
}

const importStructures = this.imports.get(fileWhereImportIsAdded) || [];
Expand Down Expand Up @@ -222,6 +231,8 @@ class BinderImp implements Binder {
sourceFile.addImportDeclaration(importStructure);
}
}

this.cleanUnreferencedHelpers();
}

private resolveDependencyReferences(file: SourceFile) {
Expand All @@ -244,28 +255,77 @@ class BinderImp implements Binder {
if (!hasAnyPlaceholders(file)) {
return;
}
for (const [declarationKey, declaration] of this.declarations) {

for (const [declarationKey, declaration] of [
...this.declarations,
...this.staticHelpers
]) {
const placeholderKey = this.serializePlaceholder(declarationKey);
if (
isStaticHelperMetadata(declaration) &&
!countPlaceholderOccurrences(file, placeholderKey)
) {
continue;
}

let name = declaration.name;
if (file !== declaration.sourceFile) {
const importDec = this.addImport(
file,
declaration.sourceFile,
declaration.name
);
name = importDec.alias ?? declaration.name;
let declarationSourceFile: SourceFile;

if ("sourceFile" in declaration) {
declarationSourceFile = declaration.sourceFile;
} else {
declarationSourceFile = declaration[SourceFileSymbol]!;
}

if (file !== declarationSourceFile) {
this.trackReference(declarationKey, file);
const importDec = this.addImport(file, declarationSourceFile, name);
name = importDec.alias ?? name;
}
replacePlaceholder(file, placeholderKey, name);
}
}

private trackReference(refkey: unknown, sourceFile: SourceFile): void {
if (!this.references.has(refkey)) {
this.references.set(refkey, new Set());
}

this.references.get(refkey)!.add(sourceFile);
}

private cleanUnreferencedHelpers() {
const usedHelperFiles = new Set<SourceFile>();
for (const helper of this.staticHelpers.values()) {
const sourceFile = helper[SourceFileSymbol];
if (!sourceFile) {
// This should be unreachable
throw new Error(
`Static helper ${helper.name} does not have a source file. Make sure that loadStaticHelpers has been correctly initialized in index.ts`
);
}
const referencedHelper = this.references.get(refkey(helper));

if (referencedHelper?.size) {
usedHelperFiles.add(sourceFile);
}
}

this.project
.getSourceFiles("**/static-helpers/**/*.ts")
.filter((helperFile) => !usedHelperFiles.has(helperFile))
.forEach((helperFile) => helperFile.delete());
}
}

// Provide the binder context to be used globally
export function provideBinder(
project: Project,
options: BinderOptions = {}
): void {
return provideContext("binder", new BinderImp(project, options));
): Binder {
const binder = new BinderImp(project, options);
provideContext("binder", binder);
return binder;
}

/**
Expand Down Expand Up @@ -310,5 +370,5 @@ function escapeRegExp(string: string): string {
}

function hasAnyPlaceholders(sourceFile: SourceFile): boolean {
return sourceFile.getFullText().includes(`<PLACEHOLDER:`);
return sourceFile.getFullText().includes(`_PLACEHOLDER_`);
}
5 changes: 3 additions & 2 deletions packages/typespec-ts/src/framework/hooks/useDependencies.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { provideContext, useContext } from "../../contextManager.js";
import { DEFAULT_DEPENDENCIES, ExternalDependencies } from "../dependency.js";
import { DefaultCoreDependencies } from "../../modular/external-dependencies.js";
import { ExternalDependencies } from "../dependency.js";

export function provideDependencies(
customDependencies: Partial<ExternalDependencies> = {}
) {
const dependencies = {
...DEFAULT_DEPENDENCIES,
...DefaultCoreDependencies,
...customDependencies
} as ExternalDependencies;

Expand Down
Loading