Skip to content

Commit 6022f21

Browse files
committed
fix: strengthen isOutputPathObject type guard validation
The type guard now properly validates: - base must be a non-empty string (rejects null, numbers, empty string) - browser, if present, must be a string (rejects null, numbers) - value must be an object (rejects arrays) Previously { base: null } or { base: 123 } would pass validation and crash at runtime with path.join() TypeError.
1 parent 5e4bb4e commit 6022f21

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed

src/deploy/actions.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,50 @@ describe('Deploy Angular apps', () => {
132132
).rejects.toThrow(expectedErrorMessage);
133133
});
134134

135+
it('throws if outputPath object has null base', async () => {
136+
context.getTargetOptions = (_: Target) =>
137+
Promise.resolve({
138+
outputPath: { base: null }
139+
} as JsonObject);
140+
141+
await expect(
142+
deploy(mockEngine, context, BUILD_TARGET, { noBuild: false })
143+
).rejects.toThrow(/Unsupported outputPath configuration/);
144+
});
145+
146+
it('throws if outputPath object has empty string base', async () => {
147+
context.getTargetOptions = (_: Target) =>
148+
Promise.resolve({
149+
outputPath: { base: '' }
150+
} as JsonObject);
151+
152+
await expect(
153+
deploy(mockEngine, context, BUILD_TARGET, { noBuild: false })
154+
).rejects.toThrow(/Unsupported outputPath configuration/);
155+
});
156+
157+
it('throws if outputPath object has numeric base', async () => {
158+
context.getTargetOptions = (_: Target) =>
159+
Promise.resolve({
160+
outputPath: { base: 123 }
161+
} as JsonObject);
162+
163+
await expect(
164+
deploy(mockEngine, context, BUILD_TARGET, { noBuild: false })
165+
).rejects.toThrow(/Unsupported outputPath configuration/);
166+
});
167+
168+
it('throws if outputPath object has null browser', async () => {
169+
context.getTargetOptions = (_: Target) =>
170+
Promise.resolve({
171+
outputPath: { base: 'dist/app', browser: null }
172+
} as JsonObject);
173+
174+
await expect(
175+
deploy(mockEngine, context, BUILD_TARGET, { noBuild: false })
176+
).rejects.toThrow(/Unsupported outputPath configuration/);
177+
});
178+
135179
it('uses correct dir when outputPath is object with base and browser (OP1)', async () => {
136180
let capturedDir: string | null = null;
137181

src/interfaces.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,31 @@ export interface AngularOutputPathObject {
1111
export type AngularOutputPath = string | AngularOutputPathObject;
1212

1313
/**
14-
* Type guard to check if outputPath is an object with base/browser properties
14+
* Type guard to check if outputPath is a valid object with base/browser properties.
15+
*
16+
* Validates:
17+
* - value is an object (not null, not array)
18+
* - base property exists and is a non-empty string
19+
* - browser property, if present, is a string (can be empty for Angular 19+ SPA mode)
1520
*/
1621
export function isOutputPathObject(value: unknown): value is AngularOutputPathObject {
17-
return !!value && typeof value === 'object' && 'base' in value;
22+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
23+
return false;
24+
}
25+
26+
const obj = value as Record<string, unknown>;
27+
28+
// base must be a non-empty string
29+
if (typeof obj.base !== 'string' || obj.base === '') {
30+
return false;
31+
}
32+
33+
// browser, if present, must be a string (empty string is valid for SPA mode)
34+
if ('browser' in obj && typeof obj.browser !== 'string') {
35+
return false;
36+
}
37+
38+
return true;
1839
}
1940

2041
/**

0 commit comments

Comments
 (0)