Name AMD module `foo` rather than `foo/module_root/index`

We already had logic to name it foo rather than foo/index, introduced in https://github.com/bazelbuild/rules_typescript/commit/c7b6880767eaae44d8777a7a1b137de92b8fd4d9

If the module_root is specified as a directory on the ts_library, it means that the index file is in that subdirectory of the package. We already handled this case correctly in type-checking, but at runtime the module identifiers don't match so the require('foo/module_root/index') fails.

Fixes bazelbuild/rules_nodejs#973

PiperOrigin-RevId: 262663195
diff --git a/internal/tsc_wrapped/compiler_host.ts b/internal/tsc_wrapped/compiler_host.ts
index 9008a08..9cae018 100644
--- a/internal/tsc_wrapped/compiler_host.ts
+++ b/internal/tsc_wrapped/compiler_host.ts
@@ -333,11 +333,14 @@
 
     if (this.bazelOpts.moduleName) {
       const relativeFileName = path.posix.relative(this.bazelOpts.package, fileName);
+      // check that the fileName was actually underneath the package directory
       if (!relativeFileName.startsWith('..')) {
-        if (this.bazelOpts.moduleRoot &&
-            this.bazelOpts.moduleRoot.replace(SOURCE_EXT, '') ===
-                relativeFileName) {
-          return this.bazelOpts.moduleName;
+        if (this.bazelOpts.moduleRoot) {
+          const root = this.bazelOpts.moduleRoot.replace(SOURCE_EXT, '');
+          if (root === relativeFileName ||
+              path.posix.join(root, 'index') === relativeFileName) {
+            return this.bazelOpts.moduleName;
+          }
         }
         // Support the common case of commonjs convention that index is the
         // default module in a directory.
diff --git a/internal/tsc_wrapped/compiler_host_test.ts b/internal/tsc_wrapped/compiler_host_test.ts
new file mode 100644
index 0000000..eaa0ac4
--- /dev/null
+++ b/internal/tsc_wrapped/compiler_host_test.ts
@@ -0,0 +1,62 @@
+import 'jasmine';
+
+import * as ts from 'typescript';
+
+import {CompilerHost} from './compiler_host';
+import {BazelOptions} from './tsconfig';
+
+describe('compiler host', () => {
+  describe('computes the amd module name of a .ts source file', () => {
+    const options: ts.CompilerOptions = {
+      rootDirs: [],
+      rootDir: 'base',
+      outDir: 'out',
+    };
+    const bazelOptions: BazelOptions = {
+      package: 'path/to/package',
+      compilationTargetSrc: [
+        'path/to/package/index.ts',
+        'path/to/package/root_dir/index.ts',
+        'test.ts',
+      ],
+      workspaceName: 'my_wksp',
+    } as any;
+
+    const defaultHost =
+        new CompilerHost([], options, bazelOptions, null as any, null as any);
+    // A module is a file with at least an import or export statement.
+    function createTsModule(filename: string) {
+      return ts.createSourceFile(filename, 'export {}', ts.ScriptTarget.ES2015);
+    }
+
+    it('should name a module after the workspace and filename', () => {
+      expect(defaultHost.amdModuleName(createTsModule('test.ts')))
+          .toBe('my_wksp/test');
+    });
+
+    it('should not provide a name for files that are not in the compilation unit',
+       () => {
+         expect(
+             defaultHost.amdModuleName(createTsModule('some_other_file.d.ts')))
+             .toBeUndefined();
+       });
+
+    it('should name the index file with a short name', () => {
+      const host = new CompilerHost(
+          [], options, {...bazelOptions, moduleName: 'my_lib'}, null as any,
+          null as any);
+      expect(host.amdModuleName(createTsModule('path/to/package/index.ts')))
+          .toBe('my_lib');
+    });
+    it('should name an index file under a module_root with a short name',
+       () => {
+         const host = new CompilerHost(
+             [], options,
+             {...bazelOptions, moduleName: 'my_lib', moduleRoot: 'root_dir'},
+             null as any, null as any);
+         expect(host.amdModuleName(
+                    createTsModule('path/to/package/root_dir/index.ts')))
+             .toBe('my_lib');
+       });
+  });
+});