diff --git a/adev/src/content/reference/extended-diagnostics/NG8114.md b/adev/src/content/reference/extended-diagnostics/NG8114.md
new file mode 100644
index 0000000000000..49d832b7ad9a3
--- /dev/null
+++ b/adev/src/content/reference/extended-diagnostics/NG8114.md
@@ -0,0 +1,61 @@
+# Missing structural directive
+
+This diagnostic ensures that a standalone component using custom structural directives (e.g., `*select` or `*featureFlag`) in a template has the necessary imports for those directives.
+
+
+
+import {Component} from '@angular/core';
+
+@Component({
+ // Template uses `*select`, but no corresponding directive imported.
+ imports: [],
+ template: `{{data}}
`,
+})
+class MyComponent {}
+
+
+
+## What's wrong with that?
+
+Using a structural directive without importing it will fail at runtime, as Angular attempts to bind to a `select` property of the HTML element, which does not exist.
+
+## What should I do instead?
+
+Make sure that the corresponding structural directive is imported into the component:
+
+
+
+import {Component} from '@angular/core';
+import {SelectDirective} from 'my-directives';
+
+@Component({
+ // Add `SelectDirective` to the `imports` array to make it available in the template.
+ imports: [SelectDirective],
+ template: `{{data}}
`,
+})
+class MyComponent {}
+
+
+
+## Configuration requirements
+
+[`strictTemplates`](tools/cli/template-typecheck#strict-mode) must be enabled for any extended diagnostic to emit.
+`missingStructuralDirective` has no additional requirements beyond `strictTemplates`.
+
+## What if I can't avoid this?
+
+This diagnostic can be disabled by editing the project's `tsconfig.json` file:
+
+
+{
+ "angularCompilerOptions": {
+ "extendedDiagnostics": {
+ "checks": {
+ "missingStructuralDirective": "suppress"
+ }
+ }
+ }
+}
+
+
+See [extended diagnostic configuration](extended-diagnostics#configuration) for more info.
diff --git a/adev/src/content/reference/extended-diagnostics/overview.md b/adev/src/content/reference/extended-diagnostics/overview.md
index 94714804e9d96..3e86e0cc872cc 100644
--- a/adev/src/content/reference/extended-diagnostics/overview.md
+++ b/adev/src/content/reference/extended-diagnostics/overview.md
@@ -20,7 +20,8 @@ Currently, Angular supports the following extended diagnostics:
| `NG8108` | [`skipHydrationNotStatic`](extended-diagnostics/NG8108) |
| `NG8109` | [`interpolatedSignalNotInvoked`](extended-diagnostics/NG8109) |
| `NG8111` | [`uninvokedFunctionInEventBinding`](extended-diagnostics/NG8111) |
-| `NG8113` | [`unusedStandaloneImports`](extended-diagnostics/NG8113) |
+| `NG8113` | [`unusedStandaloneImports`](extended-diagnostics/NG8113) |
+| `NG8114` | [`missingStructuralDirective`](extended-diagnostics/NG8114) |
## Configuration
diff --git a/goldens/public-api/compiler-cli/error_code.api.md b/goldens/public-api/compiler-cli/error_code.api.md
index b8412619d50d5..be713a7ebaba4 100644
--- a/goldens/public-api/compiler-cli/error_code.api.md
+++ b/goldens/public-api/compiler-cli/error_code.api.md
@@ -75,6 +75,7 @@ export enum ErrorCode {
MISSING_PIPE = 8004,
MISSING_REFERENCE_TARGET = 8003,
MISSING_REQUIRED_INPUTS = 8008,
+ MISSING_STRUCTURAL_DIRECTIVE = 8114,
NGMODULE_BOOTSTRAP_IS_STANDALONE = 6009,
NGMODULE_DECLARATION_IS_STANDALONE = 6008,
NGMODULE_DECLARATION_NOT_UNIQUE = 6007,
diff --git a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
index ee99d7d584756..78546c0e15d6b 100644
--- a/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
+++ b/goldens/public-api/compiler-cli/extended_template_diagnostic_name.api.md
@@ -17,6 +17,8 @@ export enum ExtendedTemplateDiagnosticName {
// (undocumented)
MISSING_NGFOROF_LET = "missingNgForOfLet",
// (undocumented)
+ MISSING_STRUCTURAL_DIRECTIVE = "missingStructuralDirective",
+ // (undocumented)
NULLISH_COALESCING_NOT_NULLABLE = "nullishCoalescingNotNullable",
// (undocumented)
OPTIONAL_CHAIN_NOT_NULLABLE = "optionalChainNotNullable",
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
index a5013d246f957..e0797eb523525 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts
@@ -518,6 +518,11 @@ export enum ErrorCode {
*/
UNUSED_STANDALONE_IMPORTS = 8113,
+ /**
+ * A structural directive is used in a template, but the directive is not imported.
+ */
+ MISSING_STRUCTURAL_DIRECTIVE = 8114,
+
/**
* The template type-checking engine would need to generate an inline type check block for a
* component, but the current type-checking environment doesn't support it.
diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
index c79de4ce6af19..bec12d76866dd 100644
--- a/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
+++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/extended_template_diagnostic_name.ts
@@ -20,6 +20,7 @@ export enum ExtendedTemplateDiagnosticName {
NULLISH_COALESCING_NOT_NULLABLE = 'nullishCoalescingNotNullable',
OPTIONAL_CHAIN_NOT_NULLABLE = 'optionalChainNotNullable',
MISSING_CONTROL_FLOW_DIRECTIVE = 'missingControlFlowDirective',
+ MISSING_STRUCTURAL_DIRECTIVE = 'missingStructuralDirective',
TEXT_ATTRIBUTE_NOT_BINDING = 'textAttributeNotBinding',
UNINVOKED_FUNCTION_IN_EVENT_BINDING = 'uninvokedFunctionInEventBinding',
MISSING_NGFOROF_LET = 'missingNgForOfLet',
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel
index f4527da5c181c..18cd509c7a824 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/BUILD.bazel
@@ -16,6 +16,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let",
+ "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable",
"//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static",
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/BUILD.bazel
new file mode 100644
index 0000000000000..a8e517f1d7c23
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/BUILD.bazel
@@ -0,0 +1,15 @@
+load("//tools:defaults.bzl", "ts_library")
+
+ts_library(
+ name = "missing_structural_directive",
+ srcs = ["index.ts"],
+ visibility = ["//packages/compiler-cli/src/ngtsc:__subpackages__"],
+ deps = [
+ "//packages/compiler",
+ "//packages/compiler-cli/src/ngtsc/core:api",
+ "//packages/compiler-cli/src/ngtsc/diagnostics",
+ "//packages/compiler-cli/src/ngtsc/typecheck/api",
+ "//packages/compiler-cli/src/ngtsc/typecheck/extended/api",
+ "@npm//typescript",
+ ],
+)
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts
new file mode 100644
index 0000000000000..2fcdeeeffe7e8
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive/index.ts
@@ -0,0 +1,91 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import {AST, TmplAstNode, TmplAstTemplate} from '@angular/compiler';
+import ts from 'typescript';
+
+import {NgCompilerOptions} from '../../../../core/api';
+import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../../diagnostics';
+import {NgTemplateDiagnostic} from '../../../api';
+import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api';
+
+/**
+ * The list of known control flow directives present in the `CommonModule`.
+ *
+ * If these control flow directives are missing they will be reported by a separate diagnostic.
+ */
+export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Set([
+ 'ngIf',
+ 'ngFor',
+ 'ngSwitch',
+ 'ngSwitchCase',
+ 'ngSwitchDefault',
+]);
+
+/**
+ * Ensures that there are no structural directives (something like *select or *featureFlag)
+ * used in a template of a *standalone* component without importing the directive. Returns
+ * diagnostics in case such a directive is detected.
+ *
+ * Note: this check only handles the cases when structural directive syntax is used (e.g. `*featureFlag`).
+ * Regular binding syntax (e.g. `[featureFlag]`) is handled separately in type checker and treated as a
+ * hard error instead of a warning.
+ */
+class MissingStructuralDirectiveCheck extends TemplateCheckWithVisitor {
+ override code = ErrorCode.MISSING_STRUCTURAL_DIRECTIVE as const;
+
+ override run(
+ ctx: TemplateContext,
+ component: ts.ClassDeclaration,
+ template: TmplAstNode[],
+ ) {
+ const componentMetadata = ctx.templateTypeChecker.getDirectiveMetadata(component);
+ // Avoid running this check for non-standalone components.
+ if (!componentMetadata || !componentMetadata.isStandalone) {
+ return [];
+ }
+ return super.run(ctx, component, template);
+ }
+
+ override visitNode(
+ ctx: TemplateContext,
+ component: ts.ClassDeclaration,
+ node: TmplAstNode | AST,
+ ): NgTemplateDiagnostic[] {
+ if (!(node instanceof TmplAstTemplate)) return [];
+
+ const customStructuralDirective = node.templateAttrs.find(
+ (attr) => !KNOWN_CONTROL_FLOW_DIRECTIVES.has(attr.name),
+ );
+ if (!customStructuralDirective) return [];
+
+ const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component);
+ if (symbol === null || symbol.directives.length > 0) {
+ return [];
+ }
+
+ const sourceSpan = customStructuralDirective.keySpan || customStructuralDirective.sourceSpan;
+ const errorMessage =
+ `An unknown structural directive \`${customStructuralDirective.name}\` was used in the template, ` +
+ `without a corresponding import in the component. ` +
+ `Make sure that the directive is included in the \`@Component.imports\` array of this component.`;
+ const diagnostic = ctx.makeTemplateDiagnostic(sourceSpan, errorMessage);
+ return [diagnostic];
+ }
+}
+
+export const factory: TemplateCheckFactory<
+ ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
+ ExtendedTemplateDiagnosticName.MISSING_STRUCTURAL_DIRECTIVE
+> = {
+ code: ErrorCode.MISSING_STRUCTURAL_DIRECTIVE,
+ name: ExtendedTemplateDiagnosticName.MISSING_STRUCTURAL_DIRECTIVE,
+ create: (options: NgCompilerOptions) => {
+ return new MissingStructuralDirectiveCheck();
+ },
+};
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
index 5cb45e946b004..f6553f327f659 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts
@@ -13,6 +13,7 @@ import {factory as interpolatedSignalNotInvoked} from './checks/interpolated_sig
import {factory as invalidBananaInBoxFactory} from './checks/invalid_banana_in_box';
import {factory as missingControlFlowDirectiveFactory} from './checks/missing_control_flow_directive';
import {factory as missingNgForOfLetFactory} from './checks/missing_ngforof_let';
+import {factory as missingStructuralDirectiveFactory} from './checks/missing_structural_directive';
import {factory as nullishCoalescingNotNullableFactory} from './checks/nullish_coalescing_not_nullable';
import {factory as optionalChainNotNullableFactory} from './checks/optional_chain_not_nullable';
import {factory as suffixNotSupportedFactory} from './checks/suffix_not_supported';
@@ -32,6 +33,7 @@ export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory<
missingControlFlowDirectiveFactory,
textAttributeNotBindingFactory,
missingNgForOfLetFactory,
+ missingStructuralDirectiveFactory,
suffixNotSupportedFactory,
interpolatedSignalNotInvoked,
uninvokedFunctionInEventBindingFactory,
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/BUILD.bazel b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/BUILD.bazel
new file mode 100644
index 0000000000000..0b630f9740927
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/BUILD.bazel
@@ -0,0 +1,30 @@
+load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
+
+ts_library(
+ name = "test_lib",
+ testonly = True,
+ srcs = ["missing_structural_directive_spec.ts"],
+ deps = [
+ "//packages/compiler",
+ "//packages/compiler-cli/src/ngtsc/core:api",
+ "//packages/compiler-cli/src/ngtsc/diagnostics",
+ "//packages/compiler-cli/src/ngtsc/file_system",
+ "//packages/compiler-cli/src/ngtsc/file_system/testing",
+ "//packages/compiler-cli/src/ngtsc/testing",
+ "//packages/compiler-cli/src/ngtsc/typecheck/extended",
+ "//packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_structural_directive",
+ "//packages/compiler-cli/src/ngtsc/typecheck/testing",
+ "@npm//typescript",
+ ],
+)
+
+jasmine_node_test(
+ name = "test",
+ bootstrap = ["//tools/testing:node_no_angular"],
+ data = [
+ "//packages/core:npm_package",
+ ],
+ deps = [
+ ":test_lib",
+ ],
+)
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts
new file mode 100644
index 0000000000000..437341009816a
--- /dev/null
+++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_structural_directive/missing_structural_directive_spec.ts
@@ -0,0 +1,218 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.dev/license
+ */
+
+import ts from 'typescript';
+
+import {ErrorCode, ngErrorCode} from '../../../../../diagnostics';
+import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system';
+import {runInEachFileSystem} from '../../../../../file_system/testing';
+import {getClass, setup} from '../../../../testing';
+import {factory as missingStructuralDirectiveCheck} from '../../../checks/missing_structural_directive';
+import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker';
+
+runInEachFileSystem(() => {
+ describe('missingStructuralDirectiveCheck', () => {
+ it('should produce a warning for missing unknown structural directives in standalone components', () => {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {
+ 'TestCmp': ``,
+ },
+ declarations: [
+ {
+ name: 'TestCmp',
+ type: 'directive',
+ selector: `[test-cmp]`,
+ isStandalone: true,
+ },
+ ],
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [missingStructuralDirectiveCheck],
+ {strictNullChecks: true} /* options */,
+ );
+ const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
+
+ expect(diags.length).toBe(1);
+ expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning);
+ expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MISSING_STRUCTURAL_DIRECTIVE));
+ });
+
+ it('should *not* produce a warning for custom structural directives that are imported', () => {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {
+ 'TestCmp': ``,
+ },
+ source: `
+ export class TestCmp {}
+ export class Foo {}
+ `,
+ declarations: [
+ {
+ type: 'directive',
+ name: 'Foo',
+ selector: `[foo]`,
+ },
+ {
+ name: 'TestCmp',
+ type: 'directive',
+ selector: `[test-cmp]`,
+ isStandalone: true,
+ },
+ ],
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [missingStructuralDirectiveCheck],
+ {strictNullChecks: true} /* options */,
+ );
+ const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
+ // No diagnostic messages are expected.
+ expect(diags.length).toBe(0);
+ });
+
+ it('should *not* produce a warning for non-standalone components', () => {
+ const fileName = absoluteFrom('/main.ts');
+
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {
+ 'TestCmp': ``,
+ },
+ declarations: [
+ {
+ name: 'TestCmp',
+ type: 'directive',
+ selector: `[test-cmp]`,
+ isStandalone: false,
+ },
+ ],
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [missingStructuralDirectiveCheck],
+ {strictNullChecks: true} /* options */,
+ );
+ const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
+ // No diagnostic messages are expected.
+ expect(diags.length).toBe(0);
+ });
+
+ it('should *not* produce a warning for non-structural directives in standalone components', () => {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {
+ 'TestCmp': ``,
+ },
+ declarations: [
+ {
+ name: 'TestCmp',
+ type: 'directive',
+ selector: `[test-cmp]`,
+ isStandalone: true,
+ },
+ ],
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [missingStructuralDirectiveCheck],
+ {strictNullChecks: true} /* options */,
+ );
+ const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
+ // No diagnostic messages are expected.
+ expect(diags.length).toBe(0);
+ });
+
+ it('should *not* produce a warning when known control flow directives are used', () => {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {
+ 'TestCmp': ``,
+ },
+ declarations: [
+ {
+ name: 'TestCmp',
+ type: 'directive',
+ selector: `[test-cmp]`,
+ isStandalone: true,
+ },
+ ],
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [missingStructuralDirectiveCheck],
+ {strictNullChecks: true} /* options */,
+ );
+ const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
+ // No diagnostic messages are expected.
+ expect(diags.length).toBe(0);
+ });
+
+ it('should not warn for templates with no structural directives', () => {
+ const fileName = absoluteFrom('/main.ts');
+ const {program, templateTypeChecker} = setup([
+ {
+ fileName,
+ templates: {
+ 'TestCmp': ``,
+ },
+ declarations: [
+ {
+ name: 'TestCmp',
+ type: 'directive',
+ selector: `[test-cmp]`,
+ isStandalone: true,
+ },
+ ],
+ },
+ ]);
+ const sf = getSourceFileOrError(program, fileName);
+ const component = getClass(sf, 'TestCmp');
+ const extendedTemplateChecker = new ExtendedTemplateCheckerImpl(
+ templateTypeChecker,
+ program.getTypeChecker(),
+ [missingStructuralDirectiveCheck],
+ {strictNullChecks: true} /* options */,
+ );
+ const diags = extendedTemplateChecker.getDiagnosticsForComponent(component);
+ // No diagnostic messages are expected.
+ expect(diags.length).toBe(0);
+ });
+ });
+});