Repository: Azure/azure-sdk-tools
Pull request #10023: [Part 1] Add kebab case style option 'is-modular-library'
Author: login: v-jiaodi
Created: 2025-03-11T03:13:48Z | Merged: 2025-03-14T02:54:29Z

fixes #9956 

Linked issue #9956: [JS] Update all options to kebab-case-style ones for automation scripts 
Related issue : https://github.com/Azure/autorest.typescript/issues/3067
Linked issue comments: user: login: wanlwanl
created_at: 2025-03-06T03:42:28Z
body: For step 1: Update TSV rules:
a. add new sub rules for new styles
b. change logic: validate old rule OR new rule
c. add unit test
d. test in prod
author_association: CONTRIBUTOR
user: login: MaryGao
created_at: 2025-03-10T06:19:07Z
body: Before switching to new format we need to keep compatible for automation script also like https://github.com/Azure/azure-sdk-tools/blob/main/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts#L24
author_association: MEMBER
user: login: wanlwanl
created_at: 2025-03-10T06:19:17Z
body: For step 2: 
a. search all old option names in js sdk release tool
b. change the logic to: read the new option's value first, if not found, read old option

please change only one options and create a PR first to make sure the method is correct. @v-jiaodi 

author_association: CONTRIBUTOR

Diff:
diff --git a/tools/js-sdk-release-tools/src/common/utils.ts b/tools/js-sdk-release-tools/src/common/utils.ts
index 0d043541c37..43e322eea2c 100644
--- a/tools/js-sdk-release-tools/src/common/utils.ts
+++ b/tools/js-sdk-release-tools/src/common/utils.ts
@@ -151,6 +151,15 @@ export function tryReadNpmPackageChangelog(changelogPath: string): string {
     }
 }
 
+export async function isMgmtPackage(typeSpecDirectory: string): Promise<Boolean> {
+    const mainTspPath = join(typeSpecDirectory, 'main.tsp');
+    const content = await readFile(mainTspPath, { encoding: 'utf-8' });
+    if (!content) {
+        throw new Error(`Failed to get main.tsp in ${typeSpecDirectory}`);
+    }
+    return content.includes("armProviderNamespace");
+}
+
 export async function loadTspConfig(typeSpecDirectory: string): Promise<Exclude<any, null | undefined>> {
     const configPath = join(typeSpecDirectory, 'tspconfig.yaml');
     const content = await readFile(configPath, { encoding: 'utf-8' });
diff --git a/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts b/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts
index 76fb3c92e6f..6e805dabc31 100644
--- a/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts
+++ b/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts
@@ -40,10 +40,63 @@ describe('Auto SDK generation path test', () => {
         }
     });
 
-    test('modular client generation', async () => {
+    test('modular client generation kebab case option', async () => {
         const fakeTspConfig = {
             options: {
                 '@azure-tools/typespec-ts': {
+                    'is-modular-library': true
+                }
+            }
+        };
+        const tempSpecFolder = path.join(__dirname, `tmp/spec-${getRandomInt(10000)}`);
+        const inputJson = {
+            relatedTypeSpecProjectFolder: ['tsp'],
+            specFolder: tempSpecFolder
+        };
+        try {
+            await ensureDir(path.join(tempSpecFolder, 'tsp'));
+            await writeFile(path.join(tempSpecFolder, 'tsp/tspconfig.yaml'), stringify(fakeTspConfig), {
+                encoding: 'utf8',
+                flush: true
+            });
+            const { sdkType } = await parseInputJson(inputJson);
+            expect(sdkType).toBe(SDKType.ModularClient);
+        } finally {
+            await remove(tempSpecFolder);
+        }
+    });
+
+    test('modular client generation camel case option', async () => {
+        const fakeTspConfig = {
+            options: {
+                '@azure-tools/typespec-ts': {
+                    isModularLibrary: true
+                }
+            }
+        };
+        const tempSpecFolder = path.join(__dirname, `tmp/spec-${getRandomInt(10000)}`);
+        const inputJson = {
+            relatedTypeSpecProjectFolder: ['tsp'],
+            specFolder: tempSpecFolder
+        };
+        try {
+            await ensureDir(path.join(tempSpecFolder, 'tsp'));
+            await writeFile(path.join(tempSpecFolder, 'tsp/tspconfig.yaml'), stringify(fakeTspConfig), {
+                encoding: 'utf8',
+                flush: true
+            });
+            const { sdkType } = await parseInputJson(inputJson);
+            expect(sdkType).toBe(SDKType.ModularClient);
+        } finally {
+            await remove(tempSpecFolder);
+        }
+    });
+
+    test('modular client generation both options', async () => {
+        const fakeTspConfig = {
+            options: {
+                '@azure-tools/typespec-ts': {
+                    'is-modular-library': true,
                     isModularLibrary: true
                 }
             }
@@ -65,4 +118,31 @@ describe('Auto SDK generation path test', () => {
             await remove(tempSpecFolder);
         }
     });
+    test('mgmt client generation', async () => {
+        const fakeTspConfig = {
+            options: {
+                '@azure-tools/typespec-ts': {}
+            }
+        };
+        const tempSpecFolder = path.join(__dirname, `tmp/spec-${getRandomInt(10000)}`);
+        const inputJson = {
+            relatedTypeSpecProjectFolder: ['tsp'],
+            specFolder: tempSpecFolder
+        };
+        try {
+            await ensureDir(path.join(tempSpecFolder, 'tsp'));
+            await writeFile(path.join(tempSpecFolder, 'tsp/tspconfig.yaml'), stringify(fakeTspConfig), {
+                encoding: 'utf8',
+                flush: true
+            });
+            await writeFile(path.join(tempSpecFolder, 'tsp/main.tsp'), "armProviderNamespace", {
+                encoding: 'utf8',
+                flush: true
+            });
+            const { sdkType } = await parseInputJson(inputJson);
+            expect(sdkType).toBe(SDKType.ModularClient);
+        } finally {
+            await remove(tempSpecFolder);
+        }
+    });
 });
diff --git a/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts b/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts
index 3704b44eff2..60b49ee8c35 100644
--- a/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts
+++ b/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts
@@ -1,6 +1,6 @@
 import path from "path";
 import { SDKType } from "../common/types";
-import { loadTspConfig } from "../common/utils";
+import { loadTspConfig, isMgmtPackage } from "../common/utils";
 import { RunningEnvironment } from "./runningEnvironment";
 import { exists } from "fs-extra";
 
@@ -16,12 +16,18 @@ async function isManagementPlaneModularClient(specFolder: string, typespecProjec
     const resolvedRelativeTspFolder = Array.isArray(typespecProjectFolder) ? typespecProjectFolder[0] : typespecProjectFolder as string;
     const tspFolderFromSpecRoot = path.join(specFolder, resolvedRelativeTspFolder);
     const tspConfigPath = path.join(tspFolderFromSpecRoot, 'tspconfig.yaml');
+    const mainTspFliePath = path.join(tspFolderFromSpecRoot, 'main.tsp');
     if (!(await exists(tspConfigPath))) {
         return false;
     }
-
     const tspConfig = await loadTspConfig(tspFolderFromSpecRoot);
-    if (tspConfig?.options?.['@azure-tools/typespec-ts']?.['isModularLibrary'] !== true) {
+    const isMgmtPackageResult = await exists(mainTspFliePath)? await isMgmtPackage(tspFolderFromSpecRoot): false;
+    const isModularLibrary = tspConfig?.options?.['@azure-tools/typespec-ts']?.['is-modular-library'] ?? 
+    tspConfig?.options?.['@azure-tools/typespec-ts']?.['isModularLibrary'];
+    if (isMgmtPackageResult) {
+        return isModularLibrary?? true;
+    }
+    if (isModularLibrary !== true) {
         return false;
     }
     return true;


Reviews:
user: login: wanlwanl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T07:19:01Z
user: login: wanlwanl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T07:26:00Z
user: login: wanlwanl
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T10:00:07Z
user: login: MaryGao
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T02:53:55Z

Review comments:
diff_hunk: @@ -44,6 +44,7 @@ describe('Auto SDK generation path test', () => {
         const fakeTspConfig = {
             options: {
                 '@azure-tools/typespec-ts': {
+                    'is-modular-library': true,
path: tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts
user: login: wanlwanl
body: add more tests: 
1. only contains `isModularLibrary`
2. only contains `is-modular-library` 
3. contains both
created_at: 2025-03-11T07:19:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,7 +21,8 @@ async function isManagementPlaneModularClient(specFolder: string, typespecProjec
     }
 
     const tspConfig = await loadTspConfig(tspFolderFromSpecRoot);
-    if (tspConfig?.options?.['@azure-tools/typespec-ts']?.['isModularLibrary'] !== true) {
+    if ((tspConfig?.options?.['@azure-tools/typespec-ts']?.['is-modular-library'] ||
path: tools/js-sdk-release-tools/src/utils/generateInputUtils.ts
user: login: wanlwanl
body: `is-modular-library` has higher priority. 
In current code: 
if `is-modular-library` == false, `isModularLibrary == true`, then it indicates "not a modular client". but I think it should be "modular client". 

You only need to check "isModularLibrary" when "is-modular-library" is "undefined"

created_at: 2025-03-11T07:26:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: wanlwanl
created_at: 2025-03-13T01:30:26Z
body: cc @MaryGao 
author_association: CONTRIBUTOR
user: login: wanlwanl
created_at: 2025-03-14T02:10:51Z
body: LGTM
author_association: CONTRIBUTOR