diff --git a/integration/module-tests/BUILD.bazel b/integration/module-tests/BUILD.bazel
new file mode 100644
index 000000000000..ae8882384d44
--- /dev/null
+++ b/integration/module-tests/BUILD.bazel
@@ -0,0 +1,27 @@
+load("//tools:defaults2.bzl", "ts_project")
+load("//integration/module-tests:index.bzl", "module_test")
+
+ts_project(
+    name = "test_lib",
+    testonly = True,
+    srcs = glob(["*.mts"]),
+    source_map = False,
+    tsconfig = "tsconfig.json",
+    deps = [
+        "//:node_modules/@angular/compiler-cli",
+        "//:node_modules/@types/node",
+        "//:node_modules/typescript",
+    ],
+)
+
+module_test(
+    name = "test",
+    npm_packages = {
+        "//src/cdk:npm_package": "src/cdk/npm_package",
+        "//src/material:npm_package": "src/material/npm_package",
+    },
+    skipped_entry_points = [
+        # This entry-point is JIT and would fail the AOT test.
+        "@angular/material/dialog/testing",
+    ],
+)
diff --git a/integration/module-tests/find-all-modules.mts b/integration/module-tests/find-all-modules.mts
new file mode 100644
index 000000000000..bc1c8190a4e4
--- /dev/null
+++ b/integration/module-tests/find-all-modules.mts
@@ -0,0 +1,60 @@
+import * as fs from 'node:fs/promises';
+import * as path from 'node:path';
+import * as ts from 'typescript';
+
+export async function findAllEntryPointsAndExportedModules(packagePath: string) {
+  const packageJsonRaw = await fs.readFile(path.join(packagePath, 'package.json'), 'utf8');
+  const packageJson = JSON.parse(packageJsonRaw) as {
+    name: string;
+    exports: Record<string, Record<string, string>>;
+  };
+  const tasks: Promise<{importPath: string; symbolName: string}[]>[] = [];
+
+  for (const [subpath, conditions] of Object.entries(packageJson.exports)) {
+    if (conditions.types === undefined) {
+      continue;
+    }
+
+    tasks.push(
+      (async () => {
+        const dtsFile = path.join(packagePath, conditions.types);
+        const dtsBundleFile = ts.createSourceFile(
+          dtsFile,
+          await fs.readFile(dtsFile, 'utf8'),
+          ts.ScriptTarget.ESNext,
+          false,
+        );
+
+        return scanExportsForModules(dtsBundleFile).map(e => ({
+          importPath: path.posix.join(packageJson.name, subpath),
+          symbolName: e,
+        }));
+      })(),
+    );
+  }
+
+  const moduleExports = (await Promise.all(tasks)).flat();
+
+  return {
+    name: packageJson.name,
+    packagePath,
+    moduleExports,
+  };
+}
+
+function scanExportsForModules(sf: ts.SourceFile): string[] {
+  const moduleExports: string[] = [];
+  const visit = (node: ts.Node) => {
+    if (ts.isExportDeclaration(node) && ts.isNamedExports(node.exportClause)) {
+      moduleExports.push(
+        ...node.exportClause.elements
+          .filter(e => e.name.text.endsWith('Module'))
+          .map(e => e.name.text),
+      );
+    }
+  };
+
+  ts.forEachChild(sf, visit);
+
+  return moduleExports;
+}
diff --git a/integration/module-tests/index.bzl b/integration/module-tests/index.bzl
new file mode 100644
index 000000000000..0db7fe80206a
--- /dev/null
+++ b/integration/module-tests/index.bzl
@@ -0,0 +1,24 @@
+load("@aspect_rules_js//js:defs.bzl", "js_test")
+load("@bazel_skylib//rules:write_file.bzl", "write_file")
+
+def module_test(name, npm_packages, skipped_entry_points = [], additional_deps = []):
+    write_file(
+        name = "%s_config" % name,
+        out = "%s_config.json" % name,
+        content = [json.encode({
+            "packages": [pkg[1] for pkg in npm_packages.items()],
+            "skipEntryPoints": skipped_entry_points,
+        })],
+    )
+
+    js_test(
+        name = "test",
+        data = [
+            ":%s_config" % name,
+            "//integration/module-tests:test_lib_rjs",
+            "//:node_modules/@angular/common",
+            "//:node_modules/@angular/core",
+        ] + additional_deps + [pkg[0] for pkg in npm_packages.items()],
+        entry_point = ":index.mjs",
+        fixed_args = ["$(rootpath :%s_config)" % name],
+    )
diff --git a/integration/module-tests/index.mts b/integration/module-tests/index.mts
new file mode 100644
index 000000000000..f5a587483e8b
--- /dev/null
+++ b/integration/module-tests/index.mts
@@ -0,0 +1,86 @@
+import {performCompilation} from '@angular/compiler-cli';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import * as os from 'os';
+import ts from 'typescript';
+import {findAllEntryPointsAndExportedModules} from './find-all-modules.mjs';
+
+async function main() {
+  const [configPath] = process.argv.slice(2);
+  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ng-module-test-'));
+  const config = JSON.parse(await fs.readFile(configPath, 'utf8')) as {
+    packages: string[];
+    skipEntryPoints: string[];
+  };
+
+  const packages = await Promise.all(
+    config.packages.map(pkgPath => findAllEntryPointsAndExportedModules(pkgPath)),
+  );
+
+  const exports = packages
+    .map(p => p.moduleExports)
+    .flat()
+    .filter(e => !config.skipEntryPoints.includes(e.importPath));
+
+  const testFile = `
+    import {NgModule, Component} from '@angular/core';
+    ${exports.map(e => `import {${e.symbolName}} from '${e.importPath}';`).join('\n')}
+
+    @NgModule({
+      exports: [
+        ${exports.map(e => e.symbolName).join(', ')}
+      ]
+    })
+    export class TestModule {}
+
+    @Component({imports: [TestModule], template: ''})
+    export class TestComponent {}
+  `;
+
+  await fs.writeFile(path.join(tmpDir, 'test.ts'), testFile);
+
+  // Prepare node modules to resolve e.g. `@angular/core`
+  await fs.symlink(path.resolve('./node_modules'), path.join(tmpDir, 'node_modules'));
+  // Prepare node modules to resolve e.g. `@angular/cdk`. This is possible
+  // as we are inside the sandbox, inside our test runfiles directory.
+  for (const {packagePath, name} of packages) {
+    await fs.symlink(path.resolve(packagePath), `./node_modules/${name}`);
+  }
+
+  const result = performCompilation({
+    options: {
+      rootDir: tmpDir,
+      skipLibCheck: true,
+      noEmit: true,
+      module: ts.ModuleKind.ESNext,
+      moduleResolution: ts.ModuleResolutionKind.Bundler,
+      strictTemplates: true,
+      preserveSymlinks: true,
+      strict: true,
+      // Note: HMR is needed as it will disable the Angular compiler's tree-shaking of used
+      // directives/components. This is critical for this test as it allows us to simply all
+      // modules and automatically validate that all symbols are reachable/importable.
+      _enableHmr: true,
+    },
+    rootNames: [path.join(tmpDir, 'test.ts')],
+  });
+
+  console.error(
+    ts.formatDiagnosticsWithColorAndContext(result.diagnostics, {
+      getCanonicalFileName: f => f,
+      getCurrentDirectory: () => '/',
+      getNewLine: () => '\n',
+    }),
+  );
+
+  await fs.rm(tmpDir, {recursive: true, force: true, maxRetries: 2});
+
+  if (result.diagnostics.length > 0) {
+    process.exitCode = 1;
+  }
+}
+
+main().catch(e => {
+  console.error('Error', e);
+  process.exitCode = 1;
+});
diff --git a/integration/module-tests/tsconfig.json b/integration/module-tests/tsconfig.json
new file mode 100644
index 000000000000..cb1f72085b50
--- /dev/null
+++ b/integration/module-tests/tsconfig.json
@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "declaration": true,
+    "types": ["node"]
+  }
+}
diff --git a/src/cdk/dialog/dialog-module.ts b/src/cdk/dialog/dialog-module.ts
index 4c800bc30304..bda86b4c4698 100644
--- a/src/cdk/dialog/dialog-module.ts
+++ b/src/cdk/dialog/dialog-module.ts
@@ -24,3 +24,14 @@ import {CdkDialogContainer} from './dialog-container';
   providers: [Dialog],
 })
 export class DialogModule {}
+
+// Re-export needed by the Angular compiler.
+// See: https://github.com/angular/components/issues/30663.
+// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
+// consuming libraries might have references to them in their own partial compilation output.
+export {
+  CdkPortal as ɵɵCdkPortal,
+  CdkPortalOutlet as ɵɵCdkPortalOutlet,
+  TemplatePortalDirective as ɵɵTemplatePortalDirective,
+  PortalHostDirective as ɵɵPortalHostDirective,
+} from '../portal';
diff --git a/src/cdk/drag-drop/drag-drop-module.ts b/src/cdk/drag-drop/drag-drop-module.ts
index 4452a0eb50c3..58032b263cc6 100644
--- a/src/cdk/drag-drop/drag-drop-module.ts
+++ b/src/cdk/drag-drop/drag-drop-module.ts
@@ -31,3 +31,9 @@ const DRAG_DROP_DIRECTIVES = [
   providers: [DragDrop],
 })
 export class DragDropModule {}
+
+// Re-export needed by the Angular compiler.
+// See: https://github.com/angular/components/issues/30663.
+// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
+// consuming libraries might have references to them in their own partial compilation output.
+export {CdkScrollable as ɵɵCdkScrollable} from '../scrolling';
diff --git a/src/cdk/overlay/overlay-module.ts b/src/cdk/overlay/overlay-module.ts
index 0b5721275ec2..fbdc64f710a2 100644
--- a/src/cdk/overlay/overlay-module.ts
+++ b/src/cdk/overlay/overlay-module.ts
@@ -23,3 +23,16 @@ import {
   providers: [Overlay, CDK_CONNECTED_OVERLAY_SCROLL_STRATEGY_PROVIDER],
 })
 export class OverlayModule {}
+
+// Re-export needed by the Angular compiler.
+// See: https://github.com/angular/components/issues/30663.
+// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
+// consuming libraries might have references to them in their own partial compilation output.
+export {
+  CdkScrollableModule as ɵɵCdkScrollableModule,
+  CdkFixedSizeVirtualScroll as ɵɵCdkFixedSizeVirtualScroll,
+  CdkVirtualForOf as ɵɵCdkVirtualForOf,
+  CdkVirtualScrollViewport as ɵɵCdkVirtualScrollViewport,
+  CdkVirtualScrollableWindow as ɵɵCdkVirtualScrollableWindow,
+  CdkVirtualScrollableElement as ɵɵCdkVirtualScrollableElement,
+} from '../scrolling';
diff --git a/src/cdk/scrolling/scrolling-module.ts b/src/cdk/scrolling/scrolling-module.ts
index 276aa83a9121..d30f03537aa5 100644
--- a/src/cdk/scrolling/scrolling-module.ts
+++ b/src/cdk/scrolling/scrolling-module.ts
@@ -45,3 +45,9 @@ export class CdkScrollableModule {}
   ],
 })
 export class ScrollingModule {}
+
+// Re-export needed by the Angular compiler.
+// See: https://github.com/angular/components/issues/30663.
+// Note: These exports need to be stable and shouldn't be renamed unnecessarily because
+// consuming libraries might have references to them in their own partial compilation output.
+export {Dir as ɵɵDir} from '../bidi';