diff --git a/tools/tsp-client/CHANGELOG.md b/tools/tsp-client/CHANGELOG.md
index 5a60f9c133e..d00372437fa 100644
--- a/tools/tsp-client/CHANGELOG.md
+++ b/tools/tsp-client/CHANGELOG.md
@@ -1,5 +1,10 @@
# Release
+## 2025-01-07 - 0.15.0
+
+- Support specifying an entrypoint file in tsp-location.yaml.
+- Ensure client.tsp selection over main.tsp in the entrypoint file search.
+
## 2024-12-20 - 0.14.3
- Bumped `@autorest/openapi-to-typespec` version to `0.10.5`.
diff --git a/tools/tsp-client/README.md b/tools/tsp-client/README.md
index a589c32e517..3caa423c1e8 100644
--- a/tools/tsp-client/README.md
+++ b/tools/tsp-client/README.md
@@ -124,6 +124,7 @@ The file has the following properties:
| additionalDirectories | Sometimes a typespec file will use a relative import that might not be under the main directory. In this case a single `directory` will not be enough to pull down all necessary files. To support this you can specify additional directories as a list to sync so that all needed files are synced. | false: default = null |
| commit | The commit sha for the version of the typespec files you want to generate off of. This allows us to have idempotence on generation until we opt into pointing at a later version. | true |
| repo | The repo this spec lives in. This should be either `Azure/azure-rest-api-specs` or `Azure/azure-rest-api-specs-pr`. Note that pr will work locally but not in CI until we add another change to handle token based auth. | true |
+| entrypointFile | A specific entrypoint file used to compile the TypeSpec project. NOTE: This option should only be used with a non-standard entrypoint file name. DO NOT use this option with standard entrypoints: `client.tsp` or `main.tsp`. | false |
Example:
diff --git a/tools/tsp-client/package-lock.json b/tools/tsp-client/package-lock.json
index 96023d365a0..89fb3db6fc4 100644
--- a/tools/tsp-client/package-lock.json
+++ b/tools/tsp-client/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@azure-tools/typespec-client-generator-cli",
- "version": "0.14.3",
+ "version": "0.15.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@azure-tools/typespec-client-generator-cli",
- "version": "0.14.3",
+ "version": "0.15.0",
"license": "MIT",
"dependencies": {
"@autorest/core": "^3.10.2",
diff --git a/tools/tsp-client/package.json b/tools/tsp-client/package.json
index 2b7dfc95f90..83f09451297 100644
--- a/tools/tsp-client/package.json
+++ b/tools/tsp-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@azure-tools/typespec-client-generator-cli",
- "version": "0.14.3",
+ "version": "0.15.0",
"description": "A tool to generate Azure SDKs from TypeSpec",
"main": "dist/index.js",
"homepage": "https://github.com/Azure/azure-sdk-tools/tree/main/tools/tsp-client#readme",
diff --git a/tools/tsp-client/src/commands.ts b/tools/tsp-client/src/commands.ts
index 0dc3a35a826..4009db64caa 100644
--- a/tools/tsp-client/src/commands.ts
+++ b/tools/tsp-client/src/commands.ts
@@ -9,7 +9,12 @@ import {
} from "./fs.js";
import { cp, mkdir, readFile, stat, unlink, writeFile } from "fs/promises";
import { npmCommand, nodeCommand } from "./npm.js";
-import { compileTsp, discoverMainFile, resolveTspConfigUrl, TspLocation } from "./typespec.js";
+import {
+ compileTsp,
+ discoverEntrypointFile,
+ resolveTspConfigUrl,
+ TspLocation,
+} from "./typespec.js";
import {
writeTspLocationYaml,
getAdditionalDirectoryName,
@@ -257,7 +262,7 @@ export async function generateCommand(argv: any) {
if (!emitter) {
throw new Error("emitter is undefined");
}
- const mainFilePath = await discoverMainFile(srcDir);
+ const mainFilePath = await discoverEntrypointFile(srcDir, tspLocation.entrypointFile);
const resolvedMainFilePath = joinPaths(srcDir, mainFilePath);
Logger.info("Installing dependencies from npm...");
const args: string[] = [];
diff --git a/tools/tsp-client/src/typespec.ts b/tools/tsp-client/src/typespec.ts
index c20ce39a1a6..7df514d9ac5 100644
--- a/tools/tsp-client/src/typespec.ts
+++ b/tools/tsp-client/src/typespec.ts
@@ -13,6 +13,7 @@ export interface TspLocation {
commit: string;
repo: string;
additionalDirectories?: string[];
+ entrypointFile?: string;
}
export function resolveTspConfigUrl(configUrl: string): {
@@ -42,18 +43,32 @@ export function resolveTspConfigUrl(configUrl: string): {
}
}
-export async function discoverMainFile(srcDir: string): Promise {
+export async function discoverEntrypointFile(
+ srcDir: string,
+ specifiedEntrypointFile?: string,
+): Promise {
Logger.debug(`Discovering entry file in ${srcDir}`);
- let entryTsp = "";
+ let entryTsp: string | undefined = undefined;
const files = await readdir(srcDir, { recursive: true });
- for (const file of files) {
- if (file.includes("client.tsp") || file.includes("main.tsp")) {
- entryTsp = file;
- Logger.debug(`Found entry file: ${entryTsp}`);
- return entryTsp;
+
+ function findEntrypoint(name: string): string | undefined {
+ return files.find((file) => file.endsWith(name)) ?? undefined;
+ }
+ if (specifiedEntrypointFile) {
+ entryTsp = findEntrypoint(specifiedEntrypointFile);
+ if (!entryTsp) {
+ throw new Error(
+ `Couldn't find the entrypoint file specified in tsp-location.yaml: "${specifiedEntrypointFile}". Please verify that the entrypoint file name is correct.`,
+ );
+ }
+ } else {
+ entryTsp = findEntrypoint("client.tsp") ?? findEntrypoint("main.tsp");
+ if (!entryTsp) {
+ throw new Error(`No main.tsp or client.tsp found`);
}
}
- throw new Error(`No main.tsp or client.tsp found`);
+ Logger.debug(`Found entry file: ${entryTsp}`);
+ return entryTsp;
}
export async function compileTsp({
diff --git a/tools/tsp-client/test/commands.spec.ts b/tools/tsp-client/test/commands.spec.ts
index d921bceee47..f29afb14369 100644
--- a/tools/tsp-client/test/commands.spec.ts
+++ b/tools/tsp-client/test/commands.spec.ts
@@ -96,6 +96,21 @@ describe.sequential("Verify commands", () => {
assert.isTrue(dir.isFile());
});
+ it("Generate with alternate entrypoint", async () => {
+ try {
+ const args = {
+ "output-dir": joinPaths(cwd(), "./test/examples/sdk/alternate-entrypoint"),
+ "local-spec-repo":
+ "./test/examples/specification/contosowidgetmanager/Contoso.WidgetManager",
+ };
+ await updateCommand(args);
+ } catch (error) {
+ assert.fail(`Failed to generate. Error: ${error}`);
+ }
+ const tspLocation = await readTspLocation("./test/examples/sdk/alternate-entrypoint");
+ assert.equal(tspLocation.entrypointFile, "foo.tsp");
+ });
+
it("Update example sdk", async () => {
try {
const args = {
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/.eslintrc.json b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/.eslintrc.json
new file mode 100644
index 00000000000..8793fba07e9
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/.eslintrc.json
@@ -0,0 +1,14 @@
+{
+ "plugins": ["@azure/azure-sdk"],
+ "extends": ["plugin:@azure/azure-sdk/azure-sdk-base"],
+ "rules": {
+ "@azure/azure-sdk/ts-modules-only-named": "warn",
+ "@azure/azure-sdk/ts-apiextractor-json-types": "warn",
+ "@azure/azure-sdk/ts-package-json-types": "warn",
+ "@azure/azure-sdk/ts-package-json-engine-is-present": "warn",
+ "tsdoc/syntax": "warn",
+ "@azure/azure-sdk/ts-package-json-module": "off",
+ "@azure/azure-sdk/ts-package-json-files-required": "off",
+ "@azure/azure-sdk/ts-package-json-main-is-cjs": "off"
+ }
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/README.md b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/README.md
new file mode 100644
index 00000000000..b92cb462fb0
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/README.md
@@ -0,0 +1,57 @@
+# Azure WidgetManager REST client library for JavaScript
+
+
+
+**Please rely heavily on our [REST client docs](https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/rest-clients.md) to use this library**
+
+Key links:
+
+- [Package (NPM)](https://www.npmjs.com/package/@azure-rest/contoso-widgetmanager-rest)
+- [API reference documentation](https://docs.microsoft.com/javascript/api/@azure-rest/contoso-widgetmanager-rest?view=azure-node-preview)
+
+## Getting started
+
+### Currently supported environments
+
+- LTS versions of Node.js
+
+### Prerequisites
+
+- You must have an [Azure subscription](https://azure.microsoft.com/free/) to use this package.
+
+### Install the `@azure-rest/contoso-widgetmanager-rest` package
+
+Install the Azure WidgetManager REST client REST client library for JavaScript with `npm`:
+
+```bash
+npm install @azure-rest/contoso-widgetmanager-rest
+```
+
+### Create and authenticate a `WidgetManagerClient`
+
+To use an [Azure Active Directory (AAD) token credential](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/identity/identity/samples/AzureIdentityExamples.md#authenticating-with-a-pre-fetched-access-token),
+provide an instance of the desired credential type obtained from the
+[@azure/identity](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/identity/identity#credentials) library.
+
+To authenticate with AAD, you must first `npm` install [`@azure/identity`](https://www.npmjs.com/package/@azure/identity)
+
+After setup, you can choose which type of [credential](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/identity/identity#credentials) from `@azure/identity` to use.
+As an example, [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/identity/identity#defaultazurecredential)
+can be used to authenticate the client.
+
+Set the values of the client ID, tenant ID, and client secret of the AAD application as environment variables:
+AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET
+
+## Troubleshooting
+
+### Logging
+
+Enabling logging may help uncover useful information about failures. In order to see a log of HTTP requests and responses, set the `AZURE_LOG_LEVEL` environment variable to `info`. Alternatively, logging can be enabled at runtime by calling `setLogLevel` in the `@azure/logger`:
+
+```javascript
+const { setLogLevel } = require("@azure/logger");
+
+setLogLevel("info");
+```
+
+For more detailed instructions on how to enable logs, you can look at the [@azure/logger package docs](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/logger).
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/api-extractor.json b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/api-extractor.json
new file mode 100644
index 00000000000..aa42d0a349d
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/api-extractor.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "mainEntryPointFilePath": "./dist/esm/index.d.ts",
+ "docModel": { "enabled": true },
+ "apiReport": { "enabled": true, "reportFolder": "./review" },
+ "dtsRollup": {
+ "enabled": true,
+ "untrimmedFilePath": "",
+ "publicTrimmedFilePath": "./types/contoso-widgetmanager-rest.d.ts"
+ },
+ "messages": {
+ "tsdocMessageReporting": { "default": { "logLevel": "none" } },
+ "extractorMessageReporting": {
+ "ae-missing-release-tag": { "logLevel": "none" },
+ "ae-unresolved-link": { "logLevel": "none" }
+ }
+ }
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/karma.conf.js b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/karma.conf.js
new file mode 100644
index 00000000000..a9d5f1b5fc5
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/karma.conf.js
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+// https://github.com/karma-runner/karma-chrome-launcher
+process.env.CHROME_BIN = require("puppeteer").executablePath();
+require("dotenv").config();
+const { relativeRecordingsPath } = require("@azure-tools/test-recorder");
+process.env.RECORDINGS_RELATIVE_PATH = relativeRecordingsPath();
+
+module.exports = function (config) {
+ config.set({
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: "./",
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ["source-map-support", "mocha"],
+
+ plugins: [
+ "karma-mocha",
+ "karma-mocha-reporter",
+ "karma-chrome-launcher",
+ "karma-firefox-launcher",
+ "karma-env-preprocessor",
+ "karma-coverage",
+ "karma-sourcemap-loader",
+ "karma-junit-reporter",
+ "karma-source-map-support",
+ ],
+
+ // list of files / patterns to load in the browser
+ files: [
+ "dist-test/index.browser.js",
+ {
+ pattern: "dist-test/index.browser.js.map",
+ type: "html",
+ included: false,
+ served: true,
+ },
+ ],
+
+ // list of files / patterns to exclude
+ exclude: [],
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ "**/*.js": ["sourcemap", "env"],
+ // IMPORTANT: COMMENT following line if you want to debug in your browsers!!
+ // Preprocess source file to calculate code coverage, however this will make source file unreadable
+ // "dist-test/index.js": ["coverage"]
+ },
+
+ envPreprocessor: [
+ "TEST_MODE",
+ "ENDPOINT",
+ "AZURE_CLIENT_SECRET",
+ "AZURE_CLIENT_ID",
+ "AZURE_TENANT_ID",
+ "SUBSCRIPTION_ID",
+ "RECORDINGS_RELATIVE_PATH",
+ ],
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ["mocha", "coverage", "junit"],
+
+ coverageReporter: {
+ // specify a common output directory
+ dir: "coverage-browser/",
+ reporters: [
+ { type: "json", subdir: ".", file: "coverage.json" },
+ { type: "lcovonly", subdir: ".", file: "lcov.info" },
+ { type: "html", subdir: "html" },
+ { type: "cobertura", subdir: ".", file: "cobertura-coverage.xml" },
+ ],
+ },
+
+ junitReporter: {
+ outputDir: "", // results will be saved as $outputDir/$browserName.xml
+ outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile
+ suite: "", // suite will become the package name attribute in xml testsuite element
+ useBrowserName: false, // add browser name to report and classes names
+ nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element
+ classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element
+ properties: {}, // key value pair of properties to add to the section of the report
+ },
+
+ // web server port
+ port: 9876,
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+ // --no-sandbox allows our tests to run in Linux without having to change the system.
+ // --disable-web-security allows us to authenticate from the browser without having to write tests using interactive auth, which would be far more complex.
+ browsers: ["ChromeHeadlessNoSandbox"],
+ customLaunchers: {
+ ChromeHeadlessNoSandbox: {
+ base: "ChromeHeadless",
+ flags: ["--no-sandbox", "--disable-web-security"],
+ },
+ },
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: 1,
+
+ browserNoActivityTimeout: 60000000,
+ browserDisconnectTimeout: 10000,
+ browserDisconnectTolerance: 3,
+
+ client: {
+ mocha: {
+ // change Karma's debug.html to the mocha web reporter
+ reporter: "html",
+ timeout: "600000",
+ },
+ },
+ });
+};
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/package.json b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/package.json
new file mode 100644
index 00000000000..7a2235c0da3
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "@azure-rest/contoso-widgetmanager-rest",
+ "version": "1.0.0-beta.1",
+ "description": "A generated SDK for WidgetManagerClient.",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "sideEffects": false,
+ "autoPublish": false,
+ "tshy": {
+ "exports": {
+ "./package.json": "./package.json",
+ ".": "./src/index.ts"
+ },
+ "dialects": ["esm", "commonjs"],
+ "esmDialects": ["browser", "react-native"],
+ "selfLink": false
+ },
+ "type": "module",
+ "keywords": ["node", "azure", "cloud", "typescript", "browser", "isomorphic"],
+ "author": "Microsoft Corporation",
+ "license": "MIT",
+ "files": ["dist", "README.md", "LICENSE", "review/*"],
+ "sdk-type": "client",
+ "repository": "github:Azure/azure-sdk-for-js",
+ "bugs": {
+ "url": "https://github.com/Azure/azure-sdk-for-js/issues"
+ },
+ "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json",
+ "//metadata": {
+ "constantPaths": [
+ {
+ "path": "src/widgetManagerClient.ts",
+ "prefix": "package-version"
+ }
+ ]
+ },
+ "dependencies": {
+ "@azure-rest/core-client": "^2.1.0",
+ "@azure/core-auth": "^1.6.0",
+ "@azure/core-rest-pipeline": "^1.5.0",
+ "@azure/logger": "^1.0.0",
+ "tslib": "^2.6.2",
+ "@azure/core-lro": "3.0.0",
+ "@azure/abort-controller": "^2.1.2",
+ "@azure/core-paging": "^1.5.0"
+ },
+ "devDependencies": {
+ "dotenv": "^16.0.0",
+ "@microsoft/api-extractor": "^7.40.3",
+ "@types/node": "^18.0.0",
+ "eslint": "^8.55.0",
+ "prettier": "^3.2.5",
+ "rimraf": "^5.0.5",
+ "mkdirp": "^3.0.1",
+ "typescript": "~5.4.5",
+ "tshy": "^1.11.1",
+ "@azure/core-util": "^1.0.0",
+ "@azure/identity": "^4.2.1",
+ "@vitest/browser": "^1.3.1",
+ "@vitest/coverage-istanbul": "^1.3.1",
+ "playwright": "^1.41.2",
+ "vitest": "^1.3.1",
+ "@azure-tools/test-credential": "^2.0.0",
+ "@azure-tools/test-recorder": "^4.0.0",
+ "@azure/dev-tool": "^1.0.0",
+ "@azure/eslint-plugin-azure-sdk": "^3.0.0"
+ },
+ "scripts": {
+ "clean": "rimraf --glob dist dist-browser dist-esm test-dist temp types *.tgz *.log",
+ "extract-api": "rimraf review && mkdirp ./review && dev-tool run extract-api",
+ "pack": "npm pack 2>&1",
+ "lint": "eslint package.json api-extractor.json src test --ext .ts --ext .cts --ext .mts",
+ "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --ext .cts --ext .mts --fix --fix-type [problem,suggestion]",
+ "unit-test": "npm run unit-test:node && npm run unit-test:browser",
+ "unit-test:browser": "npm run build:test && dev-tool run test:vitest --browser",
+ "unit-test:node": "dev-tool run test:vitest",
+ "integration-test": "npm run integration-test:node && npm run integration-test:browser",
+ "integration-test:browser": "echo skipped",
+ "integration-test:node": "echo skipped",
+ "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit",
+ "build:samples": "dev-tool samples publish --force",
+ "check-format": "dev-tool run vendored prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.{ts,cts,mts}\" \"test/**/*.{ts,cts,mts}\" \"*.{js,cjs,mjs,json}\"",
+ "execute:samples": "dev-tool samples run samples-dev",
+ "format": "dev-tool run vendored prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.{ts,cts,mts}\" \"test/**/*.{ts,cts,mts}\" \"*.{js,cjs,mjs,json}\"",
+ "generate:client": "echo skipped",
+ "test:browser": "npm run clean && npm run build:test && npm run unit-test:browser && npm run integration-test:browser",
+ "minify": "uglifyjs -c -m --comments --source-map \"content='./dist/index.js.map'\" -o ./dist/index.min.js ./dist/index.js",
+ "build:test": "npm run clean && tshy && dev-tool run build-test",
+ "build": "npm run clean && tshy && mkdirp ./review && dev-tool run extract-api",
+ "test:node": "npm run clean && tshy && npm run unit-test:node && npm run integration-test:node",
+ "test": "npm run clean && tshy && npm run unit-test:node && dev-tool run bundle && npm run unit-test:browser && npm run integration-test"
+ }
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/clientDefinitions.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/clientDefinitions.ts
new file mode 100644
index 00000000000..af0e3f73f4f
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/clientDefinitions.ts
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import {
+ GetWidgetParameters,
+ CreateOrUpdateWidgetParameters,
+ DeleteWidgetParameters,
+ GetWidgetOperationStatusParameters,
+ ListWidgetsParameters,
+} from "./parameters.js";
+import {
+ GetWidget200Response,
+ GetWidgetDefaultResponse,
+ CreateOrUpdateWidget200Response,
+ CreateOrUpdateWidget201Response,
+ CreateOrUpdateWidgetDefaultResponse,
+ DeleteWidget202Response,
+ DeleteWidgetDefaultResponse,
+ GetWidgetOperationStatus200Response,
+ GetWidgetOperationStatusDefaultResponse,
+ ListWidgets200Response,
+ ListWidgetsDefaultResponse,
+} from "./responses.js";
+import { Client, StreamableMethod } from "@azure-rest/core-client";
+
+export interface GetWidget {
+ /** Fetch a Widget by name. */
+ get(
+ options?: GetWidgetParameters,
+ ): StreamableMethod;
+ /** Creates or updates a Widget asynchronously. */
+ patch(
+ options: CreateOrUpdateWidgetParameters,
+ ): StreamableMethod<
+ | CreateOrUpdateWidget200Response
+ | CreateOrUpdateWidget201Response
+ | CreateOrUpdateWidgetDefaultResponse
+ >;
+ /** Delete a Widget asynchronously. */
+ delete(
+ options?: DeleteWidgetParameters,
+ ): StreamableMethod;
+}
+
+export interface GetWidgetOperationStatus {
+ /** Gets status of a Widget operation. */
+ get(
+ options?: GetWidgetOperationStatusParameters,
+ ): StreamableMethod<
+ | GetWidgetOperationStatus200Response
+ | GetWidgetOperationStatusDefaultResponse
+ >;
+}
+
+export interface ListWidgets {
+ /** List Widget resources */
+ get(
+ options?: ListWidgetsParameters,
+ ): StreamableMethod;
+}
+
+export interface Routes {
+ /** Resource for '/widgets/\{widgetName\}' has methods for the following verbs: get, patch, delete */
+ (path: "/widgets/{widgetName}", widgetName: string): GetWidget;
+ /** Resource for '/widgets/\{widgetName\}/operations/\{operationId\}' has methods for the following verbs: get */
+ (
+ path: "/widgets/{widgetName}/operations/{operationId}",
+ widgetName: string,
+ operationId: string,
+ ): GetWidgetOperationStatus;
+ /** Resource for '/widgets' has methods for the following verbs: get */
+ (path: "/widgets"): ListWidgets;
+}
+
+export type WidgetManagerClient = Client & {
+ path: Routes;
+};
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/index.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/index.ts
new file mode 100644
index 00000000000..c32bbe5e537
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/index.ts
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import WidgetManagerClient from "./widgetManagerClient.js";
+
+export * from "./widgetManagerClient.js";
+export * from "./parameters.js";
+export * from "./responses.js";
+export * from "./clientDefinitions.js";
+export * from "./isUnexpected.js";
+export * from "./models.js";
+export * from "./outputModels.js";
+export * from "./paginateHelper.js";
+export * from "./pollingHelper.js";
+
+export default WidgetManagerClient;
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/isUnexpected.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/isUnexpected.ts
new file mode 100644
index 00000000000..31e42275fa0
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/isUnexpected.ts
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import {
+ GetWidget200Response,
+ GetWidgetDefaultResponse,
+ CreateOrUpdateWidget200Response,
+ CreateOrUpdateWidget201Response,
+ CreateOrUpdateWidgetLogicalResponse,
+ CreateOrUpdateWidgetDefaultResponse,
+ DeleteWidget202Response,
+ DeleteWidgetLogicalResponse,
+ DeleteWidgetDefaultResponse,
+ GetWidgetOperationStatus200Response,
+ GetWidgetOperationStatusDefaultResponse,
+ ListWidgets200Response,
+ ListWidgetsDefaultResponse,
+} from "./responses.js";
+
+const responseMap: Record = {
+ "GET /widgets/{widgetName}": ["200"],
+ "PATCH /widgets/{widgetName}": ["200", "201"],
+ "DELETE /widgets/{widgetName}": ["202"],
+ "GET /widgets/{widgetName}/operations/{operationId}": ["200"],
+ "GET /widgets": ["200"],
+};
+
+export function isUnexpected(
+ response: GetWidget200Response | GetWidgetDefaultResponse,
+): response is GetWidgetDefaultResponse;
+export function isUnexpected(
+ response:
+ | CreateOrUpdateWidget200Response
+ | CreateOrUpdateWidget201Response
+ | CreateOrUpdateWidgetLogicalResponse
+ | CreateOrUpdateWidgetDefaultResponse,
+): response is CreateOrUpdateWidgetDefaultResponse;
+export function isUnexpected(
+ response:
+ | DeleteWidget202Response
+ | DeleteWidgetLogicalResponse
+ | DeleteWidgetDefaultResponse,
+): response is DeleteWidgetDefaultResponse;
+export function isUnexpected(
+ response:
+ | GetWidgetOperationStatus200Response
+ | GetWidgetOperationStatusDefaultResponse,
+): response is GetWidgetOperationStatusDefaultResponse;
+export function isUnexpected(
+ response: ListWidgets200Response | ListWidgetsDefaultResponse,
+): response is ListWidgetsDefaultResponse;
+export function isUnexpected(
+ response:
+ | GetWidget200Response
+ | GetWidgetDefaultResponse
+ | CreateOrUpdateWidget200Response
+ | CreateOrUpdateWidget201Response
+ | CreateOrUpdateWidgetLogicalResponse
+ | CreateOrUpdateWidgetDefaultResponse
+ | DeleteWidget202Response
+ | DeleteWidgetLogicalResponse
+ | DeleteWidgetDefaultResponse
+ | GetWidgetOperationStatus200Response
+ | GetWidgetOperationStatusDefaultResponse
+ | ListWidgets200Response
+ | ListWidgetsDefaultResponse,
+): response is
+ | GetWidgetDefaultResponse
+ | CreateOrUpdateWidgetDefaultResponse
+ | DeleteWidgetDefaultResponse
+ | GetWidgetOperationStatusDefaultResponse
+ | ListWidgetsDefaultResponse {
+ const lroOriginal = response.headers["x-ms-original-url"];
+ const url = new URL(lroOriginal ?? response.request.url);
+ const method = response.request.method;
+ let pathDetails = responseMap[`${method} ${url.pathname}`];
+ if (!pathDetails) {
+ pathDetails = getParametrizedPathSuccess(method, url.pathname);
+ }
+ return !pathDetails.includes(response.status);
+}
+
+function getParametrizedPathSuccess(method: string, path: string): string[] {
+ const pathParts = path.split("/");
+
+ // Traverse list to match the longest candidate
+ // matchedLen: the length of candidate path
+ // matchedValue: the matched status code array
+ let matchedLen = -1,
+ matchedValue: string[] = [];
+
+ // Iterate the responseMap to find a match
+ for (const [key, value] of Object.entries(responseMap)) {
+ // Extracting the path from the map key which is in format
+ // GET /path/foo
+ if (!key.startsWith(method)) {
+ continue;
+ }
+ const candidatePath = getPathFromMapKey(key);
+ // Get each part of the url path
+ const candidateParts = candidatePath.split("/");
+
+ // track if we have found a match to return the values found.
+ let found = true;
+ for (
+ let i = candidateParts.length - 1, j = pathParts.length - 1;
+ i >= 1 && j >= 1;
+ i--, j--
+ ) {
+ if (
+ candidateParts[i]?.startsWith("{") &&
+ candidateParts[i]?.indexOf("}") !== -1
+ ) {
+ const start = candidateParts[i]!.indexOf("}") + 1,
+ end = candidateParts[i]?.length;
+ // If the current part of the candidate is a "template" part
+ // Try to use the suffix of pattern to match the path
+ // {guid} ==> $
+ // {guid}:export ==> :export$
+ const isMatched = new RegExp(
+ `${candidateParts[i]?.slice(start, end)}`,
+ ).test(pathParts[j] || "");
+
+ if (!isMatched) {
+ found = false;
+ break;
+ }
+ continue;
+ }
+
+ // If the candidate part is not a template and
+ // the parts don't match mark the candidate as not found
+ // to move on with the next candidate path.
+ if (candidateParts[i] !== pathParts[j]) {
+ found = false;
+ break;
+ }
+ }
+
+ // We finished evaluating the current candidate parts
+ // Update the matched value if and only if we found the longer pattern
+ if (found && candidatePath.length > matchedLen) {
+ matchedLen = candidatePath.length;
+ matchedValue = value;
+ }
+ }
+
+ return matchedValue;
+}
+
+function getPathFromMapKey(mapKey: string): string {
+ const pathStart = mapKey.indexOf("/");
+ return mapKey.slice(pathStart);
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/logger.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/logger.ts
new file mode 100644
index 00000000000..454be08a1b8
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/logger.ts
@@ -0,0 +1,5 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { createClientLogger } from "@azure/logger";
+export const logger = createClientLogger("contoso-widgetmanager-rest");
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/models.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/models.ts
new file mode 100644
index 00000000000..f99d5eafc7b
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/models.ts
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+/** A widget. */
+export interface WidgetSuite {
+ /** The ID of the widget's manufacturer. */
+ manufacturerId: string;
+ /** The faked shared model. */
+ sharedModel?: FakedSharedModel;
+}
+
+/** Faked shared model */
+export interface FakedSharedModel {
+ /** The tag. */
+ tag: string;
+ /** The created date. */
+ createdAt: Date | string;
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/outputModels.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/outputModels.ts
new file mode 100644
index 00000000000..2cc5e0c3784
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/outputModels.ts
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { Paged } from "@azure/core-paging";
+import { ErrorModel } from "@azure-rest/core-client";
+
+/** A widget. */
+export interface WidgetSuiteOutput {
+ /** The widget name. */
+ readonly name: string;
+ /** The ID of the widget's manufacturer. */
+ manufacturerId: string;
+ /** The faked shared model. */
+ sharedModel?: FakedSharedModelOutput;
+}
+
+/** Faked shared model */
+export interface FakedSharedModelOutput {
+ /** The tag. */
+ tag: string;
+ /** The created date. */
+ createdAt: string;
+}
+
+/** Provides status details for long running operations. */
+export interface ResourceOperationStatusOutput {
+ /** The unique ID of the operation. */
+ id: string;
+ /** The status of the operation */
+ status: OperationStateOutput;
+ /** Error object that describes the error when status is "Failed". */
+ error?: ErrorModel;
+ /** The result of the operation. */
+ result?: WidgetSuiteOutput;
+}
+
+/** Provides status details for long running operations. */
+export interface OperationStatusOutput {
+ /** The unique ID of the operation. */
+ id: string;
+ /** The status of the operation */
+ status: OperationStateOutput;
+ /** Error object that describes the error when status is "Failed". */
+ error?: ErrorModel;
+}
+
+/** Alias for OperationStateOutput */
+export type OperationStateOutput = string;
+/** Paged collection of WidgetSuite items */
+export type PagedWidgetSuiteOutput = Paged;
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/paginateHelper.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/paginateHelper.ts
new file mode 100644
index 00000000000..3aac141db0e
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/paginateHelper.ts
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import {
+ getPagedAsyncIterator,
+ PagedAsyncIterableIterator,
+ PagedResult,
+} from "@azure/core-paging";
+import {
+ Client,
+ createRestError,
+ PathUncheckedResponse,
+} from "@azure-rest/core-client";
+
+/**
+ * Helper type to extract the type of an array
+ */
+export type GetArrayType = T extends Array ? TData : never;
+
+/**
+ * The type of a custom function that defines how to get a page and a link to the next one if any.
+ */
+export type GetPage = (
+ pageLink: string,
+ maxPageSize?: number,
+) => Promise<{
+ page: TPage;
+ nextPageLink?: string;
+}>;
+
+/**
+ * Options for the paging helper
+ */
+export interface PagingOptions {
+ /**
+ * Custom function to extract pagination details for crating the PagedAsyncIterableIterator
+ */
+ customGetPage?: GetPage[]>;
+}
+
+/**
+ * Helper type to infer the Type of the paged elements from the response type
+ * This type is generated based on the swagger information for x-ms-pageable
+ * specifically on the itemName property which indicates the property of the response
+ * where the page items are found. The default value is `value`.
+ * This type will allow us to provide strongly typed Iterator based on the response we get as second parameter
+ */
+export type PaginateReturn = TResult extends {
+ body: { value?: infer TPage };
+}
+ ? GetArrayType
+ : Array;
+
+/**
+ * Helper to paginate results from an initial response that follows the specification of Autorest `x-ms-pageable` extension
+ * @param client - Client to use for sending the next page requests
+ * @param initialResponse - Initial response containing the nextLink and current page of elements
+ * @param customGetPage - Optional - Function to define how to extract the page and next link to be used to paginate the results
+ * @returns - PagedAsyncIterableIterator to iterate the elements
+ */
+export function paginate(
+ client: Client,
+ initialResponse: TResponse,
+ options: PagingOptions = {},
+): PagedAsyncIterableIterator> {
+ // Extract element type from initial response
+ type TElement = PaginateReturn;
+ let firstRun = true;
+ const itemName = "value";
+ const nextLinkName = "nextLink";
+ const { customGetPage } = options;
+ const pagedResult: PagedResult = {
+ firstPageLink: "",
+ getPage:
+ typeof customGetPage === "function"
+ ? customGetPage
+ : async (pageLink: string) => {
+ const result = firstRun
+ ? initialResponse
+ : await client.pathUnchecked(pageLink).get();
+ firstRun = false;
+ checkPagingRequest(result);
+ const nextLink = getNextLink(result.body, nextLinkName);
+ const values = getElements(result.body, itemName);
+ return {
+ page: values,
+ nextPageLink: nextLink,
+ };
+ },
+ };
+
+ return getPagedAsyncIterator(pagedResult);
+}
+
+/**
+ * Gets for the value of nextLink in the body
+ */
+function getNextLink(body: unknown, nextLinkName?: string): string | undefined {
+ if (!nextLinkName) {
+ return undefined;
+ }
+
+ const nextLink = (body as Record)[nextLinkName];
+
+ if (typeof nextLink !== "string" && typeof nextLink !== "undefined") {
+ throw new Error(
+ `Body Property ${nextLinkName} should be a string or undefined`,
+ );
+ }
+
+ return nextLink;
+}
+
+/**
+ * Gets the elements of the current request in the body.
+ */
+function getElements(body: unknown, itemName: string): T[] {
+ const value = (body as Record)[itemName] as T[];
+
+ // value has to be an array according to the x-ms-pageable extension.
+ // The fact that this must be an array is used above to calculate the
+ // type of elements in the page in PaginateReturn
+ if (!Array.isArray(value)) {
+ throw new Error(
+ `Couldn't paginate response\n Body doesn't contain an array property with name: ${itemName}`,
+ );
+ }
+
+ return value ?? [];
+}
+
+/**
+ * Checks if a request failed
+ */
+function checkPagingRequest(response: PathUncheckedResponse): void {
+ const Http2xxStatusCodes = [
+ "200",
+ "201",
+ "202",
+ "203",
+ "204",
+ "205",
+ "206",
+ "207",
+ "208",
+ "226",
+ ];
+ if (!Http2xxStatusCodes.includes(response.status)) {
+ throw createRestError(
+ `Pagination failed with unexpected statusCode ${response.status}`,
+ response,
+ );
+ }
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/parameters.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/parameters.ts
new file mode 100644
index 00000000000..091b2502f5d
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/parameters.ts
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { RequestParameters } from "@azure-rest/core-client";
+import { WidgetSuite } from "./models.js";
+
+export type GetWidgetParameters = RequestParameters;
+export type GetWidgetOperationStatusParameters = RequestParameters;
+/** The resource instance. */
+export type WidgetSuiteResourceMergeAndPatch = Partial;
+
+export interface CreateOrUpdateWidgetBodyParam {
+ /** The resource instance. */
+ body: WidgetSuiteResourceMergeAndPatch;
+}
+
+export interface CreateOrUpdateWidgetMediaTypesParam {
+ /** This request has a JSON Merge Patch body. */
+ contentType: "application/merge-patch+json";
+}
+
+export type CreateOrUpdateWidgetParameters =
+ CreateOrUpdateWidgetMediaTypesParam &
+ CreateOrUpdateWidgetBodyParam &
+ RequestParameters;
+export type DeleteWidgetParameters = RequestParameters;
+export type ListWidgetsParameters = RequestParameters;
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/pollingHelper.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/pollingHelper.ts
new file mode 100644
index 00000000000..76542ba5d65
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/pollingHelper.ts
@@ -0,0 +1,233 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { Client, HttpResponse } from "@azure-rest/core-client";
+import { AbortSignalLike } from "@azure/abort-controller";
+import {
+ CancelOnProgress,
+ CreateHttpPollerOptions,
+ RunningOperation,
+ OperationResponse,
+ OperationState,
+ createHttpPoller,
+} from "@azure/core-lro";
+import {
+ CreateOrUpdateWidget200Response,
+ CreateOrUpdateWidget201Response,
+ CreateOrUpdateWidgetDefaultResponse,
+ CreateOrUpdateWidgetLogicalResponse,
+ DeleteWidget202Response,
+ DeleteWidgetDefaultResponse,
+ DeleteWidgetLogicalResponse,
+} from "./responses.js";
+
+/**
+ * A simple poller that can be used to poll a long running operation.
+ */
+export interface SimplePollerLike<
+ TState extends OperationState,
+ TResult,
+> {
+ /**
+ * Returns true if the poller has finished polling.
+ */
+ isDone(): boolean;
+ /**
+ * Returns the state of the operation.
+ */
+ getOperationState(): TState;
+ /**
+ * Returns the result value of the operation,
+ * regardless of the state of the poller.
+ * It can return undefined or an incomplete form of the final TResult value
+ * depending on the implementation.
+ */
+ getResult(): TResult | undefined;
+ /**
+ * Returns a promise that will resolve once a single polling request finishes.
+ * It does this by calling the update method of the Poller's operation.
+ */
+ poll(options?: { abortSignal?: AbortSignalLike }): Promise;
+ /**
+ * Returns a promise that will resolve once the underlying operation is completed.
+ */
+ pollUntilDone(pollOptions?: {
+ abortSignal?: AbortSignalLike;
+ }): Promise;
+ /**
+ * Invokes the provided callback after each polling is completed,
+ * sending the current state of the poller's operation.
+ *
+ * It returns a method that can be used to stop receiving updates on the given callback function.
+ */
+ onProgress(callback: (state: TState) => void): CancelOnProgress;
+
+ /**
+ * Returns a promise that could be used for serialized version of the poller's operation
+ * by invoking the operation's serialize method.
+ */
+ serialize(): Promise;
+
+ /**
+ * Wait the poller to be submitted.
+ */
+ submitted(): Promise;
+
+ /**
+ * Returns a string representation of the poller's operation. Similar to serialize but returns a string.
+ * @deprecated Use serialize() instead.
+ */
+ toString(): string;
+
+ /**
+ * Stops the poller from continuing to poll. Please note this will only stop the client-side polling
+ * @deprecated Use abortSignal to stop polling instead.
+ */
+ stopPolling(): void;
+
+ /**
+ * Returns true if the poller is stopped.
+ * @deprecated Use abortSignal status to track this instead.
+ */
+ isStopped(): boolean;
+}
+
+/**
+ * Helper function that builds a Poller object to help polling a long running operation.
+ * @param client - Client to use for sending the request to get additional pages.
+ * @param initialResponse - The initial response.
+ * @param options - Options to set a resume state or custom polling interval.
+ * @returns - A poller object to poll for operation state updates and eventually get the final response.
+ */
+export async function getLongRunningPoller<
+ TResult extends
+ | CreateOrUpdateWidgetLogicalResponse
+ | CreateOrUpdateWidgetDefaultResponse,
+>(
+ client: Client,
+ initialResponse:
+ | CreateOrUpdateWidget200Response
+ | CreateOrUpdateWidget201Response
+ | CreateOrUpdateWidgetDefaultResponse,
+ options?: CreateHttpPollerOptions>,
+): Promise, TResult>>;
+export async function getLongRunningPoller<
+ TResult extends DeleteWidgetLogicalResponse | DeleteWidgetDefaultResponse,
+>(
+ client: Client,
+ initialResponse: DeleteWidget202Response | DeleteWidgetDefaultResponse,
+ options?: CreateHttpPollerOptions>,
+): Promise, TResult>>;
+export async function getLongRunningPoller(
+ client: Client,
+ initialResponse: TResult,
+ options: CreateHttpPollerOptions> = {},
+): Promise, TResult>> {
+ const abortController = new AbortController();
+ const poller: RunningOperation = {
+ sendInitialRequest: async () => {
+ // In the case of Rest Clients we are building the LRO poller object from a response that's the reason
+ // we are not triggering the initial request here, just extracting the information from the
+ // response we were provided.
+ return getLroResponse(initialResponse);
+ },
+ sendPollRequest: async (
+ path: string,
+ pollOptions?: { abortSignal?: AbortSignalLike },
+ ) => {
+ // This is the callback that is going to be called to poll the service
+ // to get the latest status. We use the client provided and the polling path
+ // which is an opaque URL provided by caller, the service sends this in one of the following headers: operation-location, azure-asyncoperation or location
+ // depending on the lro pattern that the service implements. If non is provided we default to the initial path.
+ function abortListener(): void {
+ abortController.abort();
+ }
+ const inputAbortSignal = pollOptions?.abortSignal;
+ const abortSignal = abortController.signal;
+ if (inputAbortSignal?.aborted) {
+ abortController.abort();
+ } else if (!abortSignal.aborted) {
+ inputAbortSignal?.addEventListener("abort", abortListener, {
+ once: true,
+ });
+ }
+ let response;
+ try {
+ response = await client
+ .pathUnchecked(path ?? initialResponse.request.url)
+ .get({ abortSignal });
+ } finally {
+ inputAbortSignal?.removeEventListener("abort", abortListener);
+ }
+ const lroResponse = getLroResponse(response as TResult);
+ lroResponse.rawResponse.headers["x-ms-original-url"] =
+ initialResponse.request.url;
+ return lroResponse;
+ },
+ };
+
+ options.resolveOnUnsuccessful = options.resolveOnUnsuccessful ?? true;
+ const httpPoller = createHttpPoller(poller, options);
+ const simplePoller: SimplePollerLike, TResult> = {
+ isDone() {
+ return httpPoller.isDone;
+ },
+ isStopped() {
+ return abortController.signal.aborted;
+ },
+ getOperationState() {
+ if (!httpPoller.operationState) {
+ throw new Error(
+ "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().",
+ );
+ }
+ return httpPoller.operationState;
+ },
+ getResult() {
+ return httpPoller.result;
+ },
+ toString() {
+ if (!httpPoller.operationState) {
+ throw new Error(
+ "Operation state is not available. The poller may not have been started and you could await submitted() before calling getOperationState().",
+ );
+ }
+ return JSON.stringify({
+ state: httpPoller.operationState,
+ });
+ },
+ stopPolling() {
+ abortController.abort();
+ },
+ onProgress: httpPoller.onProgress,
+ poll: httpPoller.poll,
+ pollUntilDone: httpPoller.pollUntilDone,
+ serialize: httpPoller.serialize,
+ submitted: httpPoller.submitted,
+ };
+ return simplePoller;
+}
+
+/**
+ * Converts a Rest Client response to a response that the LRO implementation understands
+ * @param response - a rest client http response
+ * @returns - An LRO response that the LRO implementation understands
+ */
+function getLroResponse(
+ response: TResult,
+): OperationResponse {
+ if (Number.isNaN(response.status)) {
+ throw new TypeError(
+ `Status code of the response is not a number. Value: ${response.status}`,
+ );
+ }
+
+ return {
+ flatResponse: response,
+ rawResponse: {
+ ...response,
+ statusCode: Number.parseInt(response.status),
+ body: response.body,
+ },
+ };
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/responses.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/responses.ts
new file mode 100644
index 00000000000..618feaa0504
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/responses.ts
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { RawHttpHeaders } from "@azure/core-rest-pipeline";
+import { HttpResponse, ErrorResponse } from "@azure-rest/core-client";
+import {
+ WidgetSuiteOutput,
+ ResourceOperationStatusOutput,
+ OperationStatusOutput,
+ PagedWidgetSuiteOutput,
+} from "./outputModels.js";
+
+/** The request has succeeded. */
+export interface GetWidget200Response extends HttpResponse {
+ status: "200";
+ body: WidgetSuiteOutput;
+}
+
+export interface GetWidgetDefaultHeaders {
+ /** String error code indicating what went wrong. */
+ "x-ms-error-code"?: string;
+}
+
+export interface GetWidgetDefaultResponse extends HttpResponse {
+ status: string;
+ body: ErrorResponse;
+ headers: RawHttpHeaders & GetWidgetDefaultHeaders;
+}
+
+/** The request has succeeded. */
+export interface GetWidgetOperationStatus200Response extends HttpResponse {
+ status: "200";
+ body: ResourceOperationStatusOutput;
+}
+
+export interface GetWidgetOperationStatusDefaultHeaders {
+ /** String error code indicating what went wrong. */
+ "x-ms-error-code"?: string;
+}
+
+export interface GetWidgetOperationStatusDefaultResponse extends HttpResponse {
+ status: string;
+ body: ErrorResponse;
+ headers: RawHttpHeaders & GetWidgetOperationStatusDefaultHeaders;
+}
+
+export interface CreateOrUpdateWidget200Headers {
+ /** The location for monitoring the operation state. */
+ "operation-location": string;
+}
+
+/** The request has succeeded. */
+export interface CreateOrUpdateWidget200Response extends HttpResponse {
+ status: "200";
+ body: WidgetSuiteOutput;
+ headers: RawHttpHeaders & CreateOrUpdateWidget200Headers;
+}
+
+export interface CreateOrUpdateWidget201Headers {
+ /** The location for monitoring the operation state. */
+ "operation-location": string;
+}
+
+/** The request has succeeded and a new resource has been created as a result. */
+export interface CreateOrUpdateWidget201Response extends HttpResponse {
+ status: "201";
+ body: WidgetSuiteOutput;
+ headers: RawHttpHeaders & CreateOrUpdateWidget201Headers;
+}
+
+export interface CreateOrUpdateWidgetDefaultHeaders {
+ /** String error code indicating what went wrong. */
+ "x-ms-error-code"?: string;
+}
+
+export interface CreateOrUpdateWidgetDefaultResponse extends HttpResponse {
+ status: string;
+ body: ErrorResponse;
+ headers: RawHttpHeaders & CreateOrUpdateWidgetDefaultHeaders;
+}
+
+/** The final response for long-running createOrUpdateWidget operation */
+export interface CreateOrUpdateWidgetLogicalResponse extends HttpResponse {
+ status: "200";
+ body: WidgetSuiteOutput;
+}
+
+export interface DeleteWidget202Headers {
+ /** The location for monitoring the operation state. */
+ "operation-location": string;
+}
+
+/** The request has been accepted for processing, but processing has not yet completed. */
+export interface DeleteWidget202Response extends HttpResponse {
+ status: "202";
+ body: OperationStatusOutput;
+ headers: RawHttpHeaders & DeleteWidget202Headers;
+}
+
+export interface DeleteWidgetDefaultHeaders {
+ /** String error code indicating what went wrong. */
+ "x-ms-error-code"?: string;
+}
+
+export interface DeleteWidgetDefaultResponse extends HttpResponse {
+ status: string;
+ body: ErrorResponse;
+ headers: RawHttpHeaders & DeleteWidgetDefaultHeaders;
+}
+
+/** The final response for long-running deleteWidget operation */
+export interface DeleteWidgetLogicalResponse extends HttpResponse {
+ status: "200";
+ body: OperationStatusOutput;
+}
+
+/** The request has succeeded. */
+export interface ListWidgets200Response extends HttpResponse {
+ status: "200";
+ body: PagedWidgetSuiteOutput;
+}
+
+export interface ListWidgetsDefaultHeaders {
+ /** String error code indicating what went wrong. */
+ "x-ms-error-code"?: string;
+}
+
+export interface ListWidgetsDefaultResponse extends HttpResponse {
+ status: string;
+ body: ErrorResponse;
+ headers: RawHttpHeaders & ListWidgetsDefaultHeaders;
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/widgetManagerClient.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/widgetManagerClient.ts
new file mode 100644
index 00000000000..f03d9aeefd0
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/src/widgetManagerClient.ts
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { getClient, ClientOptions } from "@azure-rest/core-client";
+import { logger } from "./logger.js";
+import { TokenCredential } from "@azure/core-auth";
+import { WidgetManagerClient } from "./clientDefinitions.js";
+
+/** The optional parameters for the client */
+export interface WidgetManagerClientOptions extends ClientOptions {
+ /** The api version option of the client */
+ apiVersion?: string;
+}
+
+/**
+ * Initialize a new instance of `WidgetManagerClient`
+ * @param endpointParam - The parameter endpointParam
+ * @param credentials - uniquely identify client credential
+ * @param options - the parameter for all optional parameters
+ */
+export default function createClient(
+ endpointParam: string,
+ credentials: TokenCredential,
+ {
+ apiVersion = "2022-11-01-preview",
+ ...options
+ }: WidgetManagerClientOptions = {},
+): WidgetManagerClient {
+ const endpointUrl = options.endpoint ?? options.baseUrl ?? `${endpointParam}`;
+ const userAgentInfo = `azsdk-js-contoso-widgetmanager-rest/1.0.0-beta.1`;
+ const userAgentPrefix =
+ options.userAgentOptions && options.userAgentOptions.userAgentPrefix
+ ? `${options.userAgentOptions.userAgentPrefix} ${userAgentInfo}`
+ : `${userAgentInfo}`;
+ options = {
+ ...options,
+ userAgentOptions: {
+ userAgentPrefix,
+ },
+ loggingOptions: {
+ logger: options.loggingOptions?.logger ?? logger.info,
+ },
+ credentials: {
+ scopes: options.credentials?.scopes ?? [
+ "https://contoso.azure.com/.default",
+ ],
+ },
+ };
+ const client = getClient(
+ endpointUrl,
+ credentials,
+ options,
+ ) as WidgetManagerClient;
+
+ client.pipeline.removePolicy({ name: "ApiVersionPolicy" });
+ client.pipeline.addPolicy({
+ name: "ClientApiVersionPolicy",
+ sendRequest: (req, next) => {
+ // Use the apiVersion defined in request url directly
+ // Append one if there is no apiVersion and we have one at client options
+ const url = new URL(req.url);
+ if (!url.searchParams.get("api-version") && apiVersion) {
+ req.url = `${req.url}${
+ Array.from(url.searchParams.keys()).length > 0 ? "&" : "?"
+ }api-version=${apiVersion}`;
+ }
+
+ return next(req);
+ },
+ });
+
+ return client;
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/test/public/sampleTest.spec.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/test/public/sampleTest.spec.ts
new file mode 100644
index 00000000000..344657d6f17
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/test/public/sampleTest.spec.ts
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { createRecorder } from "./utils/recordedClient.js";
+import { assert, beforeEach, afterEach, it, describe } from "vitest";
+
+describe("My test", () => {
+ // let recorder: Recorder;
+
+ beforeEach(async function () {
+ // recorder = await createRecorder(this);
+ });
+
+ afterEach(async function () {
+ // await recorder.stop();
+ });
+
+ it("sample test", async function () {
+ assert.equal(1, 1);
+ });
+});
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/test/public/utils/recordedClient.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/test/public/utils/recordedClient.ts
new file mode 100644
index 00000000000..231e0b7c22f
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/test/public/utils/recordedClient.ts
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { Context } from "mocha";
+import { Recorder, RecorderStartOptions } from "@azure-tools/test-recorder";
+
+const envSetupForPlayback: Record = {
+ ENDPOINT: "https://endpoint",
+ AZURE_CLIENT_ID: "azure_client_id",
+ AZURE_CLIENT_SECRET: "azure_client_secret",
+ AZURE_TENANT_ID: "88888888-8888-8888-8888-888888888888",
+ SUBSCRIPTION_ID: "azure_subscription_id",
+};
+
+const recorderEnvSetup: RecorderStartOptions = {
+ envSetupForPlayback,
+};
+
+/**
+ * creates the recorder and reads the environment variables from the `.env` file.
+ * Should be called first in the test suite to make sure environment variables are
+ * read before they are being used.
+ */
+export async function createRecorder(context: Context): Promise {
+ const recorder = new Recorder(context.currentTest);
+ await recorder.start(recorderEnvSetup);
+ return recorder;
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsconfig.browser.config.json b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsconfig.browser.config.json
new file mode 100644
index 00000000000..1b37aebc545
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsconfig.browser.config.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./.tshy/build.json",
+ "include": ["./src/**/*.ts", "./src/**/*.mts", "./test/**/*.spec.ts"],
+ "exclude": ["./test/**/node/**/*.ts"],
+ "compilerOptions": {
+ "outDir": "./dist-test/browser",
+ "rootDir": ".",
+ "skipLibCheck": true
+ }
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsconfig.json b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsconfig.json
new file mode 100644
index 00000000000..7483ff67760
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../../tsconfig.package",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "rootDir": "."
+ },
+ "include": [
+ "./src/**/*.ts",
+ "./src/**/*.mts",
+ "./src/**/*.cts",
+ "test/**/*.ts",
+ "./test/**/*.ts"
+ ]
+}
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsp-location.yaml b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsp-location.yaml
new file mode 100644
index 00000000000..f98623d725c
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/tsp-location.yaml
@@ -0,0 +1,6 @@
+directory: tools/tsp-client/test/examples/specification/contosowidgetmanager/Contoso.WidgetManager
+commit: db63bea839f5648462c94e685d5cc96f8e8b38ba
+repo: Azure/azure-sdk-tools
+additionalDirectories:
+ - tools/tsp-client/test/examples/specification/contosowidgetmanager/Contoso.WidgetManager.Shared/
+entrypointFile: foo.tsp
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/vitest.browser.config.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/vitest.browser.config.ts
new file mode 100644
index 00000000000..cc2bbfc362c
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/vitest.browser.config.ts
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { defineConfig } from "vitest/config";
+import { relativeRecordingsPath } from "@azure-tools/test-recorder";
+
+process.env.RECORDINGS_RELATIVE_PATH = relativeRecordingsPath();
+
+export default defineConfig({
+ define: {
+ "process.env": process.env,
+ },
+ test: {
+ reporters: ["basic", "junit"],
+ outputFile: {
+ junit: "test-results.browser.xml",
+ },
+ browser: {
+ enabled: true,
+ headless: true,
+ name: "chromium",
+ provider: "playwright",
+ },
+ fakeTimers: {
+ toFake: ["setTimeout", "Date"],
+ },
+ watch: false,
+ include: ["dist-test/browser/**/*.spec.js"],
+ coverage: {
+ include: ["dist-test/browser/**/*.spec.js"],
+ provider: "istanbul",
+ reporter: ["text", "json", "html"],
+ reportsDirectory: "coverage-browser",
+ },
+ },
+});
diff --git a/tools/tsp-client/test/examples/sdk/alternate-entrypoint/vitest.config.ts b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/vitest.config.ts
new file mode 100644
index 00000000000..367c90fa4e7
--- /dev/null
+++ b/tools/tsp-client/test/examples/sdk/alternate-entrypoint/vitest.config.ts
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+import { defineConfig } from "vitest/config";
+import { relativeRecordingsPath } from "@azure-tools/test-recorder";
+
+export default defineConfig({
+ test: {
+ reporters: ["basic", "junit"],
+ outputFile: {
+ junit: "test-results.browser.xml",
+ },
+ fakeTimers: {
+ toFake: ["setTimeout", "Date"],
+ },
+ watch: false,
+ include: ["test/**/*.spec.ts"],
+ exclude: ["test/**/browser/*.spec.ts"],
+ coverage: {
+ include: ["src/**/*.ts"],
+ exclude: [
+ "src/**/*-browser.mts",
+ "src/**/*-react-native.mts",
+ "vitest*.config.ts",
+ "samples-dev/**/*.ts",
+ ],
+ provider: "istanbul",
+ reporter: ["text", "json", "html"],
+ reportsDirectory: "coverage",
+ },
+ },
+});
diff --git a/tools/tsp-client/test/examples/sdk/local-spec-sdk/tsp-location.yaml b/tools/tsp-client/test/examples/sdk/local-spec-sdk/tsp-location.yaml
index 219fbbbc9b1..f98623d725c 100644
--- a/tools/tsp-client/test/examples/sdk/local-spec-sdk/tsp-location.yaml
+++ b/tools/tsp-client/test/examples/sdk/local-spec-sdk/tsp-location.yaml
@@ -3,3 +3,4 @@ commit: db63bea839f5648462c94e685d5cc96f8e8b38ba
repo: Azure/azure-sdk-tools
additionalDirectories:
- tools/tsp-client/test/examples/specification/contosowidgetmanager/Contoso.WidgetManager.Shared/
+entrypointFile: foo.tsp
diff --git a/tools/tsp-client/test/examples/specification/contosowidgetmanager/Contoso.WidgetManager/foo.tsp b/tools/tsp-client/test/examples/specification/contosowidgetmanager/Contoso.WidgetManager/foo.tsp
new file mode 100644
index 00000000000..ad741769102
--- /dev/null
+++ b/tools/tsp-client/test/examples/specification/contosowidgetmanager/Contoso.WidgetManager/foo.tsp
@@ -0,0 +1 @@
+import "./main.tsp";
diff --git a/tools/tsp-client/test/typespec.spec.ts b/tools/tsp-client/test/typespec.spec.ts
index 6a7f5bbe9e1..e1bd0ebdce3 100644
--- a/tools/tsp-client/test/typespec.spec.ts
+++ b/tools/tsp-client/test/typespec.spec.ts
@@ -1,11 +1,11 @@
import { assert } from "chai";
import { describe, it } from "vitest";
-import { compileTsp, discoverMainFile } from "../src/typespec.js";
+import { compileTsp, discoverEntrypointFile } from "../src/typespec.js";
import { joinPaths, resolvePath } from "@typespec/compiler";
describe("Check diagnostic reporting", function () {
it("Check diagnostic format", async function () {
- const mainFile = await discoverMainFile(
+ const mainFile = await discoverEntrypointFile(
resolvePath(process.cwd(), "test", "examples", "specification", "diagnostics"),
);
try {
@@ -24,4 +24,27 @@ describe("Check diagnostic reporting", function () {
assert.fail("Unexpected failure.");
}
});
+
+ it("Check discoverEntrypointFile()", async function () {
+ let entrypointFile = await discoverEntrypointFile(
+ joinPaths(process.cwd(), "test", "examples", "specification", "convert"),
+ "Catalog.tsp",
+ );
+ assert.equal(entrypointFile, "Catalog.tsp");
+ entrypointFile = await discoverEntrypointFile(
+ joinPaths(process.cwd(), "test", "examples", "specification", "convert"),
+ );
+ assert.equal(entrypointFile, "client.tsp");
+ entrypointFile = await discoverEntrypointFile(
+ joinPaths(
+ process.cwd(),
+ "test",
+ "examples",
+ "specification",
+ "contosowidgetmanager",
+ "Contoso.WidgetManager",
+ ),
+ );
+ assert.equal(entrypointFile, "main.tsp");
+ });
});