Skip to content

Commit f445641

Browse files
committed
fix(compiler): support styleUrl singular and safe template getter
- Merge `styleUrl` (Angular 17+) and `styleUrls` in getComponentStyleUrls - Guard getComponentTemplate against non-string template references - Update fixture to use styleUrl singular form closes #1571
1 parent 56e92a5 commit f445641

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

src/app/compiler/angular/deps/helpers/component-helper.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,11 @@ export class ComponentHelper {
251251

252252
const result = {
253253
required,
254-
type: this.parseSignalType(signalType) ?? this.inferTypeFromValue(signalDefaultValue),
254+
// For signals like input<OutputType, TransformType>(...), only the first
255+
// type parameter is the signal's value type (issue #1571).
256+
type:
257+
this.parseSignalType(this.extractFirstTypeParam(signalType)) ??
258+
this.inferTypeFromValue(signalDefaultValue),
255259
defaultValue: signalDefaultValue
256260
};
257261

@@ -349,6 +353,34 @@ export class ComponentHelper {
349353
return -1;
350354
}
351355

356+
/**
357+
* Extracts the first type parameter from a comma-separated generic type string.
358+
* For Angular signals like `input<OutputType, TransformInputType>`, only the
359+
* first type parameter is the signal's value type (issue #1571).
360+
* For example:
361+
* "number, number" → "number"
362+
* "string[], string | string[]" → "string[]"
363+
* "() => string[], () => string[]" → "() => string[]"
364+
* "string | number" → "string | number" (no top-level comma)
365+
*/
366+
private extractFirstTypeParam(typeStr: string): string {
367+
if (!typeStr) return typeStr;
368+
let depth = 0;
369+
for (let i = 0; i < typeStr.length; i++) {
370+
const ch = typeStr[i];
371+
if (ch === '<' || ch === '(' || ch === '[' || ch === '{') {
372+
depth++;
373+
} else if (ch === '>' || ch === ')' || ch === ']' || ch === '}') {
374+
// Skip '>' that is part of '=>' (arrow function syntax)
375+
if (ch === '>' && i > 0 && typeStr[i - 1] === '=') continue;
376+
depth--;
377+
} else if (ch === ',' && depth === 0) {
378+
return typeStr.slice(0, i).trim();
379+
}
380+
}
381+
return typeStr;
382+
}
383+
352384
/**
353385
* Infers a primitive TypeScript type from a literal default value string.
354386
* Used as a fallback when no explicit type parameter is provided (e.g. `input(4)`).

test/fixtures/todomvc-ng2/src/app/about/compodoc/compodoc.component.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,9 @@ export class CompodocComponent {
5656
outputSignalWithStringType = output<'value'>();
5757
outputSignalWithMultipleTypes = output<string | number>();
5858
outputSignalWithMultipleMixedTypes = output<'asc' | 'dsc' | number>();
59+
60+
// issue #1571: input with two type params (output type + transform input type)
61+
inputSignalWithTransform = input.required<number, number>({ transform: Number });
62+
// issue #1571: input with union type default value
63+
inputSignalWithUnionType = input<'left' | 'right'>('right');
5964
}

test/src/app/compiler/angular/deps/helpers/component-helper.spec.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,23 +312,27 @@ describe('ComponentHelper', () => {
312312
// This case caused catastrophic backtracking in the old regex.
313313
// The options object { transform: ... } is the sole argument and must not
314314
// leak into defaultValue — it should be treated as the options object.
315+
// The second type param is the transform input type; only the first (output) type
316+
// is shown in documentation (issue #1571).
315317
const defaultValue = `input.required<string[], string | string[]>({ transform: v => (Array.isArray(v) ? v : [v]) })`;
316318
const result = componentHelper['getSignalConfig']('input', defaultValue);
317319

318320
expect(result).to.deep.equal({
319321
required: true,
320-
type: 'string[], string | string[]',
322+
type: 'string[]',
321323
defaultValue: undefined
322324
});
323325
});
324326

325327
it('should parse input signal with arrow function types containing => without hanging (issue #1654)', () => {
328+
// The second type param is the transform input type; only the first (output) type
329+
// is shown in documentation (issue #1571).
326330
const defaultValue = `input<() => string[], () => string[]>(() => [])`;
327331
const result = componentHelper['getSignalConfig']('input', defaultValue);
328332

329333
expect(result).to.deep.equal({
330334
required: false,
331-
type: '() => string[], () => string[]',
335+
type: '() => string[]',
332336
defaultValue: '() => []'
333337
});
334338
});
@@ -476,6 +480,40 @@ describe('ComponentHelper', () => {
476480

477481
expect(result).to.not.have.property('name');
478482
});
483+
484+
// Tests for issue #1571: signal with two type params (output type + transform input type)
485+
it('should extract only the output type from input.required<T, U> with transform (issue #1571)', () => {
486+
const defaultValue = `input.required<number, number>({ transform: numberAttribute })`;
487+
const result = componentHelper['getSignalConfig']('input', defaultValue);
488+
489+
expect(result).to.deep.equal({
490+
required: true,
491+
type: 'number',
492+
defaultValue: undefined
493+
});
494+
});
495+
496+
it('should extract only the output type from input<T, U> with transform and default (issue #1571)', () => {
497+
const defaultValue = `input<string, string | null>('default', { transform: someTransform })`;
498+
const result = componentHelper['getSignalConfig']('input', defaultValue);
499+
500+
expect(result).to.deep.equal({
501+
required: false,
502+
type: 'string',
503+
defaultValue: "'default'"
504+
});
505+
});
506+
507+
it('should not split union types that are a single type param (issue #1571)', () => {
508+
const defaultValue = `input<'left' | 'right'>('right')`;
509+
const result = componentHelper['getSignalConfig']('input', defaultValue);
510+
511+
expect(result).to.deep.equal({
512+
required: false,
513+
type: '"left" | "right"',
514+
defaultValue: "'right'"
515+
});
516+
});
479517
});
480518

481519
describe('getInputSignal', () => {

0 commit comments

Comments
 (0)