Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/angular/build/src/builders/dev-server/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,16 @@ export async function normalizeOptions(
sslKey,
prebundle,
allowedHosts,
disableHostCheck,
} = options;

let normalizedAllowedHosts: true | string[];
if (disableHostCheck === true) {
normalizedAllowedHosts = true;
} else {
normalizedAllowedHosts = Array.isArray(allowedHosts) ? allowedHosts : [];
}

// Return all the normalized options
return {
buildTarget,
Expand All @@ -152,6 +160,6 @@ export async function normalizeOptions(
// Prebundling defaults to true but requires caching to function
prebundle: cacheOptions.enabled && !optimization.scripts && prebundle,
inspect,
allowedHosts: allowedHosts ? allowedHosts : [],
allowedHosts: normalizedAllowedHosts,
};
}
24 changes: 10 additions & 14 deletions packages/angular/build/src/builders/dev-server/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,17 @@
"description": "SSL certificate to use for serving HTTPS."
},
"allowedHosts": {
"description": "The hosts that the development server will respond to. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts",
"type": "array",
"description": "List of hosts that are allowed to access the dev server.",
"default": [],
"oneOf": [
{
"type": "array",
"description": "A list of hosts that the development server will respond to.",
"items": {
"type": "string"
}
},
{
"type": "boolean",
"description": "Indicates that all hosts are allowed. This is not recommended and a security risk."
}
]
"items": {
"type": "string"
}
},
"disableHostCheck": {
"type": "boolean",
"description": "Don't verify connected clients are part of allowed hosts.",
"default": false
},
"define": {
"description": "Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries. The value will be used directly. String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,19 +65,5 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
expect(result?.success).toBeTrue();
expect(content).toContain('<title>');
});

it('allows a host when option is true', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
allowedHosts: true,
});

const { result, content } = await executeOnceAndGet(harness, '/', {
request: { headers: FETCH_HEADERS },
});

expect(result?.success).toBeTrue();
expect(content).toContain('<title>');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @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 { executeDevServer } from '../../index';
import { executeOnceAndGet } from '../execute-fetch';
import { describeServeBuilder } from '../jasmine-helpers';
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
import { text } from 'node:stream/consumers';

const FETCH_HEADERS = Object.freeze({ Host: 'example.com' });

describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
describe('option: "disableHostCheck"', () => {
beforeEach(async () => {
setupTarget(harness);

// Application code is not needed for these tests
await harness.writeFile('src/main.ts', '');
});

it('does not allow an invalid host when option is not present', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
});

const { result, response } = await executeOnceAndGet(harness, '/', {
request: { headers: FETCH_HEADERS },
});

expect(result?.success).toBeTrue();
expect(response?.statusCode).toBe(403);
expect(response && (await text(response))).toContain('angular.json');
});

it('does not allow an invalid host when option is false', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
disableHostCheck: false,
});

const { result, response } = await executeOnceAndGet(harness, '/', {
request: { headers: FETCH_HEADERS },
});

expect(result?.success).toBeTrue();
expect(response?.statusCode).toBe(403);
expect(response && (await text(response))).toContain('angular.json');
});

it('allows a host when option is true', async () => {
harness.useTarget('serve', {
...BASE_OPTIONS,
disableHostCheck: true,
});

const { result, content } = await executeOnceAndGet(harness, '/', {
request: { headers: FETCH_HEADERS },
});

expect(result?.success).toBeTrue();
expect(content).toContain('<title>');
});
});
});
1 change: 1 addition & 0 deletions packages/angular/cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ jasmine_test(
"package.json",
"test-esm-loader.mjs",
":angular-cli_test_lib",
"//packages/angular/build:schemas",
],
node_options = [
"--import=./test-esm-loader.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/

import { JsonObject, schema } from '@angular-devkit/core';
import { readFileSync } from 'node:fs';
import path from 'node:path';
Comment on lines +10 to +11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import fileURLToPath from node:url to resolve the schema path robustly using import.meta.url.

Suggested change
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import type { Argv } from 'yargs';
import yargs from 'yargs';

Expand Down Expand Up @@ -389,4 +391,42 @@ describe('parseJsonSchemaToOptions', () => {
]);
});
});

describe('@angular/build dev-server schema', () => {
const devServerSchemaPath = path.join(
import.meta.dirname,
'../../../../build/src/builders/dev-server/schema.json',
);
Comment on lines +396 to +399

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using process.cwd() to resolve the schema path is fragile because it depends on the current working directory of the test runner (which can vary between Bazel, local IDE test runners, or running from the workspace root). Using import.meta.url with fileURLToPath is a robust, location-independent way to resolve paths relative to the test file.

    const devServerSchemaPath = fileURLToPath(
      new URL('../../../../build/src/builders/dev-server/schema.json', import.meta.url),
    );


const parse = async (args: string[]) => {
return localYargs.parseAsync(args);
};

let localYargs: Argv<unknown>;

beforeEach(async () => {
localYargs = yargs().exitProcess(false).strict().fail(false).wrap(1_000);

const devServerSchema = JSON.parse(readFileSync(devServerSchemaPath, 'utf8')) as JsonObject;
const registry = new schema.CoreSchemaRegistry();
const options = await parseJsonSchemaToOptions(registry, devServerSchema, false);
addSchemaOptionsToCommand(localYargs, options, true);
});

it('parses --allowed-hosts with host names', async () => {
expect(await parse(['--allowed-hosts', 'example.com'])).toEqual(
jasmine.objectContaining({
allowedHosts: ['example.com'],
}),
);
});

it('parses --disable-host-check', async () => {
expect(await parse(['--disable-host-check'])).toEqual(
jasmine.objectContaining({
disableHostCheck: true,
}),
);
});
});
});
Loading