mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-18 03:23:57 +08:00
feat(@angular-devkit/build-angular): move Vite-based dev-server for application builder to new build system package
With the `application` builder already within the new `@angular/build` package, the Vite-based `dev-server` builder is now also contained within this package. Only the Vite-based aspects of the `dev-server` have been moved and only the support for the `application` builder. The compatibility builder `browser-esbuild` is not supported with `@angular/build:dev-server`. The existing `dev-server` builder found within `@angular-devkit/build-angular` should continue to be used for both the Webpack-based `browser` builder and the esbuild-based compatibility `browser-esbuild` builder. To maintain backwards compatibility, the existing `@angular-devkit/build-angular:dev-server` builder continues to support builders it has previously. No change to existing applications is required.
This commit is contained in:
parent
c4cd1ed0bf
commit
4ffe07aa24
@ -8,6 +8,7 @@
|
||||
|
||||
import { BuilderContext } from '@angular-devkit/architect';
|
||||
import { BuilderOutput } from '@angular-devkit/architect';
|
||||
import type http from 'node:http';
|
||||
import { OutputFile } from 'esbuild';
|
||||
import type { Plugin as Plugin_2 } from 'esbuild';
|
||||
|
||||
@ -107,6 +108,51 @@ export enum BuildOutputFileType {
|
||||
Server = 3
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface DevServerBuilderOptions {
|
||||
allowedHosts?: string[];
|
||||
// @deprecated
|
||||
browserTarget?: string;
|
||||
buildTarget?: string;
|
||||
disableHostCheck?: boolean;
|
||||
forceEsbuild?: boolean;
|
||||
headers?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
hmr?: boolean;
|
||||
host?: string;
|
||||
liveReload?: boolean;
|
||||
open?: boolean;
|
||||
poll?: number;
|
||||
port?: number;
|
||||
prebundle?: PrebundleUnion;
|
||||
proxyConfig?: string;
|
||||
publicHost?: string;
|
||||
servePath?: string;
|
||||
ssl?: boolean;
|
||||
sslCert?: string;
|
||||
sslKey?: string;
|
||||
verbose?: boolean;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface DevServerBuilderOutput extends BuilderOutput {
|
||||
// (undocumented)
|
||||
address?: string;
|
||||
// (undocumented)
|
||||
baseUrl: string;
|
||||
// (undocumented)
|
||||
port?: number;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function executeDevServerBuilder(options: DevServerBuilderOptions, context: BuilderContext, extensions?: {
|
||||
buildPlugins?: Plugin_2[];
|
||||
middleware?: ((req: http.IncomingMessage, res: http.ServerResponse, next: (err?: unknown) => void) => void)[];
|
||||
indexHtmlTransformer?: IndexHtmlTransform;
|
||||
}): AsyncIterable<DevServerBuilderOutput>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
```
|
||||
|
@ -12,6 +12,7 @@ import { BuilderContext } from '@angular-devkit/architect';
|
||||
import { BuilderOutput } from '@angular-devkit/architect';
|
||||
import type { ConfigOptions } from 'karma';
|
||||
import { Configuration } from 'webpack';
|
||||
import { DevServerBuilderOutput } from '@angular/build';
|
||||
import type http from 'node:http';
|
||||
import { IndexHtmlTransform } from '@angular/build/private';
|
||||
import { json } from '@angular-devkit/core';
|
||||
@ -142,15 +143,7 @@ export interface DevServerBuilderOptions {
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface DevServerBuilderOutput extends BuilderOutput {
|
||||
// (undocumented)
|
||||
address?: string;
|
||||
// (undocumented)
|
||||
baseUrl: string;
|
||||
// (undocumented)
|
||||
port?: number;
|
||||
}
|
||||
export { DevServerBuilderOutput }
|
||||
|
||||
// @public
|
||||
export function executeBrowserBuilder(options: BrowserBuilderOptions, context: BuilderContext, transforms?: {
|
||||
|
@ -12,6 +12,11 @@ ts_json_schema(
|
||||
src = "src/builders/application/schema.json",
|
||||
)
|
||||
|
||||
ts_json_schema(
|
||||
name = "dev-server_schema",
|
||||
src = "src/builders/dev-server/schema.json",
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "build",
|
||||
package_name = "@angular/build",
|
||||
@ -28,6 +33,7 @@ ts_library(
|
||||
],
|
||||
) + [
|
||||
"//packages/angular/build:src/builders/application/schema.ts",
|
||||
"//packages/angular/build:src/builders/dev-server/schema.ts",
|
||||
],
|
||||
data = glob(
|
||||
include = [
|
||||
@ -57,11 +63,13 @@ ts_library(
|
||||
"@npm//@babel/helper-split-export-declaration",
|
||||
"@npm//@types/babel__core",
|
||||
"@npm//@types/browserslist",
|
||||
"@npm//@types/inquirer",
|
||||
"@npm//@types/less",
|
||||
"@npm//@types/node",
|
||||
"@npm//@types/picomatch",
|
||||
"@npm//@types/semver",
|
||||
"@npm//@types/watchpack",
|
||||
"@npm//@vitejs/plugin-basic-ssl",
|
||||
"@npm//ansi-colors",
|
||||
"@npm//autoprefixer",
|
||||
"@npm//browserslist",
|
||||
@ -69,6 +77,7 @@ ts_library(
|
||||
"@npm//esbuild",
|
||||
"@npm//fast-glob",
|
||||
"@npm//https-proxy-agent",
|
||||
"@npm//inquirer",
|
||||
"@npm//less",
|
||||
"@npm//magic-string",
|
||||
"@npm//mrmime",
|
||||
@ -81,6 +90,7 @@ ts_library(
|
||||
"@npm//tslib",
|
||||
"@npm//typescript",
|
||||
"@npm//undici",
|
||||
"@npm//vite",
|
||||
"@npm//watchpack",
|
||||
],
|
||||
)
|
||||
@ -132,6 +142,11 @@ ts_library(
|
||||
"//packages/angular_devkit/core",
|
||||
"//packages/angular_devkit/core/node",
|
||||
|
||||
# dev server only test deps
|
||||
"@npm//@types/http-proxy",
|
||||
"@npm//http-proxy",
|
||||
"@npm//puppeteer",
|
||||
|
||||
# Base dependencies for the application in hello-world-app.
|
||||
"@npm//@angular/common",
|
||||
"@npm//@angular/compiler",
|
||||
|
@ -1,9 +1,14 @@
|
||||
{
|
||||
"builders": {
|
||||
"application": {
|
||||
"implementation": "./src/builders/application",
|
||||
"implementation": "./src/builders/application/index",
|
||||
"schema": "./src/builders/application/schema.json",
|
||||
"description": "Build an application."
|
||||
},
|
||||
"dev-server": {
|
||||
"implementation": "./src/builders/dev-server/index",
|
||||
"schema": "./src/builders/dev-server/schema.json",
|
||||
"description": "Execute a development server for an application."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,11 +23,13 @@
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/helper-annotate-as-pure": "7.22.5",
|
||||
"@babel/helper-split-export-declaration": "7.22.6",
|
||||
"@vitejs/plugin-basic-ssl": "1.1.0",
|
||||
"browserslist": "^4.23.0",
|
||||
"critters": "0.0.22",
|
||||
"esbuild": "0.20.2",
|
||||
"fast-glob": "3.3.2",
|
||||
"https-proxy-agent": "7.0.4",
|
||||
"inquirer": "9.2.19",
|
||||
"less": "4.2.0",
|
||||
"magic-string": "0.30.10",
|
||||
"mrmime": "2.0.0",
|
||||
@ -38,6 +40,7 @@
|
||||
"sass": "1.75.0",
|
||||
"semver": "7.6.0",
|
||||
"undici": "6.13.0",
|
||||
"vite": "5.2.10",
|
||||
"watchpack": "2.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
117
packages/angular/build/src/builders/dev-server/builder.ts
Normal file
117
packages/angular/build/src/builders/dev-server/builder.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import type { BuilderContext } from '@angular-devkit/architect';
|
||||
import type { Plugin } from 'esbuild';
|
||||
import type http from 'node:http';
|
||||
import { checkPort } from '../../utils/check-port';
|
||||
import {
|
||||
type IndexHtmlTransform,
|
||||
buildApplicationInternal,
|
||||
purgeStaleBuildCache,
|
||||
} from './internal';
|
||||
import { normalizeOptions } from './options';
|
||||
import type { DevServerBuilderOutput } from './output';
|
||||
import type { Schema as DevServerBuilderOptions } from './schema';
|
||||
import { serveWithVite } from './vite-server';
|
||||
|
||||
/**
|
||||
* A Builder that executes a development server based on the provided browser target option.
|
||||
*
|
||||
* Usage of the `transforms` and/or `extensions` parameters is NOT supported and may cause
|
||||
* unexpected build output or build failures.
|
||||
*
|
||||
* @param options Dev Server options.
|
||||
* @param context The build context.
|
||||
* @param extensions An optional object containing an array of build plugins (esbuild-based)
|
||||
* and/or HTTP request middleware.
|
||||
*
|
||||
* @experimental Direct usage of this function is considered experimental.
|
||||
*/
|
||||
export async function* execute(
|
||||
options: DevServerBuilderOptions,
|
||||
context: BuilderContext,
|
||||
extensions?: {
|
||||
buildPlugins?: Plugin[];
|
||||
middleware?: ((
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
next: (err?: unknown) => void,
|
||||
) => void)[];
|
||||
indexHtmlTransformer?: IndexHtmlTransform;
|
||||
},
|
||||
): AsyncIterable<DevServerBuilderOutput> {
|
||||
// Determine project name from builder context target
|
||||
const projectName = context.target?.project;
|
||||
if (!projectName) {
|
||||
context.logger.error(`The "dev-server" builder requires a target to be specified.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { builderName, normalizedOptions } = await initialize(options, projectName, context);
|
||||
|
||||
// Warn if the initial options provided by the user enable prebundling but caching is disabled
|
||||
if (options.prebundle && !normalizedOptions.cacheOptions.enabled) {
|
||||
context.logger.warn(
|
||||
`Prebundling has been configured but will not be used because caching has been disabled.`,
|
||||
);
|
||||
}
|
||||
|
||||
yield* serveWithVite(
|
||||
normalizedOptions,
|
||||
builderName,
|
||||
(options, context, plugins) =>
|
||||
buildApplicationInternal(options, context, { write: false }, { codePlugins: plugins }),
|
||||
context,
|
||||
{ indexHtml: extensions?.indexHtmlTransformer },
|
||||
extensions,
|
||||
);
|
||||
}
|
||||
|
||||
async function initialize(
|
||||
initialOptions: DevServerBuilderOptions,
|
||||
projectName: string,
|
||||
context: BuilderContext,
|
||||
) {
|
||||
// Purge old build disk cache.
|
||||
await purgeStaleBuildCache(context);
|
||||
|
||||
const normalizedOptions = await normalizeOptions(context, projectName, initialOptions);
|
||||
const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget);
|
||||
|
||||
if (
|
||||
!normalizedOptions.disableHostCheck &&
|
||||
!/^127\.\d+\.\d+\.\d+/g.test(normalizedOptions.host) &&
|
||||
normalizedOptions.host !== 'localhost'
|
||||
) {
|
||||
context.logger.warn(`
|
||||
Warning: This is a simple server for use in testing or debugging Angular applications
|
||||
locally. It hasn't been reviewed for security issues.
|
||||
|
||||
Binding this server to an open connection can result in compromising your application or
|
||||
computer. Using a different host than the one passed to the "--host" flag might result in
|
||||
websocket connection issues. You might need to use "--disable-host-check" if that's the
|
||||
case.
|
||||
`);
|
||||
}
|
||||
|
||||
if (normalizedOptions.disableHostCheck) {
|
||||
context.logger.warn(
|
||||
'Warning: Running a server with --disable-host-check is a security risk. ' +
|
||||
'See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a for more information.',
|
||||
);
|
||||
}
|
||||
|
||||
normalizedOptions.port = await checkPort(normalizedOptions.port, normalizedOptions.host);
|
||||
|
||||
return {
|
||||
builderName,
|
||||
normalizedOptions,
|
||||
};
|
||||
}
|
18
packages/angular/build/src/builders/dev-server/index.ts
Normal file
18
packages/angular/build/src/builders/dev-server/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { createBuilder } from '@angular-devkit/architect';
|
||||
import { execute } from './builder';
|
||||
import { DevServerBuilderOutput } from './output';
|
||||
import { Schema as DevServerBuilderOptions } from './schema';
|
||||
|
||||
export { DevServerBuilderOptions, DevServerBuilderOutput, execute as executeDevServerBuilder };
|
||||
export default createBuilder<DevServerBuilderOptions, DevServerBuilderOutput>(execute);
|
||||
|
||||
// Temporary export to support specs
|
||||
export { execute as executeDevServer };
|
20
packages/angular/build/src/builders/dev-server/internal.ts
Normal file
20
packages/angular/build/src/builders/dev-server/internal.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
export { BuildOutputFile, BuildOutputFileType } from '@angular/build';
|
||||
export { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
|
||||
export { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
|
||||
export { getFeatureSupport, isZonelessApp } from '../../tools/esbuild/utils';
|
||||
export { renderPage } from '../../utils/server-rendering/render-page';
|
||||
export { type IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
|
||||
export { purgeStaleBuildCache } from '../../utils/purge-cache';
|
||||
export { getSupportedBrowsers } from '../../utils/supported-browsers';
|
||||
export { transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils';
|
||||
export { buildApplicationInternal } from '../../builders/application';
|
||||
export { ApplicationBuilderInternalOptions } from '../../builders/application/options';
|
||||
export { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result';
|
91
packages/angular/build/src/builders/dev-server/options.ts
Normal file
91
packages/angular/build/src/builders/dev-server/options.ts
Normal file
@ -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.io/license
|
||||
*/
|
||||
|
||||
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
|
||||
import path from 'node:path';
|
||||
import { normalizeCacheOptions } from '../../utils/normalize-cache';
|
||||
import { Schema as DevServerOptions } from './schema';
|
||||
|
||||
export type NormalizedDevServerOptions = Awaited<ReturnType<typeof normalizeOptions>>;
|
||||
|
||||
/**
|
||||
* Normalize the user provided options by creating full paths for all path based options
|
||||
* and converting multi-form options into a single form that can be directly used
|
||||
* by the build process.
|
||||
*
|
||||
* @param context The context for current builder execution.
|
||||
* @param projectName The name of the project for the current execution.
|
||||
* @param options An object containing the options to use for the build.
|
||||
* @returns An object containing normalized options required to perform the build.
|
||||
*/
|
||||
export async function normalizeOptions(
|
||||
context: BuilderContext,
|
||||
projectName: string,
|
||||
options: DevServerOptions,
|
||||
) {
|
||||
const workspaceRoot = context.workspaceRoot;
|
||||
const projectMetadata = await context.getProjectMetadata(projectName);
|
||||
const projectRoot = path.join(workspaceRoot, (projectMetadata.root as string | undefined) ?? '');
|
||||
|
||||
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
|
||||
|
||||
// Target specifier defaults to the current project's build target using a development configuration
|
||||
const buildTargetSpecifier = options.buildTarget ?? options.browserTarget ?? `::development`;
|
||||
const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build');
|
||||
|
||||
// Initial options to keep
|
||||
const {
|
||||
host,
|
||||
port,
|
||||
poll,
|
||||
open,
|
||||
verbose,
|
||||
watch,
|
||||
allowedHosts,
|
||||
disableHostCheck,
|
||||
liveReload,
|
||||
hmr,
|
||||
headers,
|
||||
proxyConfig,
|
||||
servePath,
|
||||
publicHost,
|
||||
ssl,
|
||||
sslCert,
|
||||
sslKey,
|
||||
forceEsbuild,
|
||||
prebundle,
|
||||
} = options;
|
||||
|
||||
// Return all the normalized options
|
||||
return {
|
||||
buildTarget,
|
||||
host: host ?? 'localhost',
|
||||
port: port ?? 4200,
|
||||
poll,
|
||||
open,
|
||||
verbose,
|
||||
watch,
|
||||
liveReload,
|
||||
hmr,
|
||||
headers,
|
||||
workspaceRoot,
|
||||
projectRoot,
|
||||
cacheOptions,
|
||||
allowedHosts,
|
||||
disableHostCheck,
|
||||
proxyConfig,
|
||||
servePath,
|
||||
publicHost,
|
||||
ssl,
|
||||
sslCert,
|
||||
sslKey,
|
||||
forceEsbuild,
|
||||
// Prebundling defaults to true but requires caching to function
|
||||
prebundle: cacheOptions.enabled && (prebundle ?? true),
|
||||
};
|
||||
}
|
131
packages/angular/build/src/builders/dev-server/schema.json
Normal file
131
packages/angular/build/src/builders/dev-server/schema.json
Normal file
@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"title": "Dev Server Target",
|
||||
"description": "Dev Server target options for Build Facade.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"browserTarget": {
|
||||
"type": "string",
|
||||
"description": "A browser builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
|
||||
"pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$",
|
||||
"x-deprecated": "Use 'buildTarget' instead."
|
||||
},
|
||||
"buildTarget": {
|
||||
"type": "string",
|
||||
"description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
|
||||
"pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "Port to listen on.",
|
||||
"default": 4200
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "Host to listen on.",
|
||||
"default": "localhost"
|
||||
},
|
||||
"proxyConfig": {
|
||||
"type": "string",
|
||||
"description": "Proxy configuration file. For more information, see https://angular.io/guide/build#proxying-to-a-backend-server."
|
||||
},
|
||||
"ssl": {
|
||||
"type": "boolean",
|
||||
"description": "Serve using HTTPS.",
|
||||
"default": false
|
||||
},
|
||||
"sslKey": {
|
||||
"type": "string",
|
||||
"description": "SSL key to use for serving HTTPS."
|
||||
},
|
||||
"sslCert": {
|
||||
"type": "string",
|
||||
"description": "SSL certificate to use for serving HTTPS."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Custom HTTP headers to be added to all responses.",
|
||||
"propertyNames": {
|
||||
"pattern": "^[-_A-Za-z0-9]+$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"type": "boolean",
|
||||
"description": "Opens the url in default browser.",
|
||||
"default": false,
|
||||
"alias": "o"
|
||||
},
|
||||
"verbose": {
|
||||
"type": "boolean",
|
||||
"description": "Adds more details to output logging."
|
||||
},
|
||||
"liveReload": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to reload the page on change, using live-reload.",
|
||||
"default": true
|
||||
},
|
||||
"publicHost": {
|
||||
"type": "string",
|
||||
"description": "The URL that the browser client (or live-reload client, if enabled) should use to connect to the development server. Use for a complex dev server setup, such as one with reverse proxies. This option has no effect when using the 'application' or other esbuild-based builders."
|
||||
},
|
||||
"allowedHosts": {
|
||||
"type": "array",
|
||||
"description": "List of hosts that are allowed to access the dev server. This option has no effect when using the 'application' or other esbuild-based builders.",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"servePath": {
|
||||
"type": "string",
|
||||
"description": "The pathname where the application will be served."
|
||||
},
|
||||
"disableHostCheck": {
|
||||
"type": "boolean",
|
||||
"description": "Don't verify connected clients are part of allowed hosts. This option has no effect when using the 'application' or other esbuild-based builders.",
|
||||
"default": false
|
||||
},
|
||||
"hmr": {
|
||||
"type": "boolean",
|
||||
"description": "Enable hot module replacement.",
|
||||
"default": false
|
||||
},
|
||||
"watch": {
|
||||
"type": "boolean",
|
||||
"description": "Rebuild on change.",
|
||||
"default": true
|
||||
},
|
||||
"poll": {
|
||||
"type": "number",
|
||||
"description": "Enable and define the file watching poll time period in milliseconds."
|
||||
},
|
||||
"forceEsbuild": {
|
||||
"type": "boolean",
|
||||
"description": "Force the development server to use the 'browser-esbuild' builder when building. This is a developer preview option for the esbuild-based build system.",
|
||||
"default": false
|
||||
},
|
||||
"prebundle": {
|
||||
"description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled. This option has no effect when using the 'browser' or other Webpack-based builders.",
|
||||
"oneOf": [
|
||||
{ "type": "boolean" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exclude": {
|
||||
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.",
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["exclude"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"anyOf": [{ "required": ["buildTarget"] }, { "required": ["browserTarget"] }]
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget, isVite) => {
|
||||
const javascriptFileContent =
|
||||
"import {foo} from 'unresolved'; /* a comment */const foo = `bar`;\n\n\n";
|
||||
|
||||
describe('Behavior: "browser builder assets"', () => {
|
||||
it('serves a project JavaScript asset unmodified', async () => {
|
||||
await harness.writeFile('src/extra.js', javascriptFileContent);
|
||||
|
||||
setupTarget(harness, {
|
||||
assets: ['src/extra.js'],
|
||||
optimization: {
|
||||
scripts: true,
|
||||
},
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, 'extra.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain(javascriptFileContent);
|
||||
});
|
||||
|
||||
it('serves a project TypeScript asset unmodified', async () => {
|
||||
await harness.writeFile('src/extra.ts', javascriptFileContent);
|
||||
|
||||
setupTarget(harness, {
|
||||
assets: ['src/extra.ts'],
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, 'extra.ts');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain(javascriptFileContent);
|
||||
});
|
||||
|
||||
it('should return 404 for non existing assets', async () => {
|
||||
setupTarget(harness, {
|
||||
assets: [],
|
||||
optimization: {
|
||||
scripts: true,
|
||||
},
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, 'does-not-exist.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.status).toBe(404);
|
||||
});
|
||||
|
||||
it(`should return the asset that matches 'index.html' when path has a trailing '/'`, async () => {
|
||||
await harness.writeFile(
|
||||
'src/login/index.html',
|
||||
'<html><body><h1>Login page</h1></body><html>',
|
||||
);
|
||||
|
||||
setupTarget(harness, {
|
||||
assets: ['src/login'],
|
||||
optimization: {
|
||||
scripts: true,
|
||||
},
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, 'login/');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.status).toBe(200);
|
||||
expect(await response?.text()).toContain('<h1>Login page</h1>');
|
||||
});
|
||||
|
||||
(isVite ? it : xit)(
|
||||
`should return the asset that matches '.html' when path has no trailing '/'`,
|
||||
async () => {
|
||||
await harness.writeFile(
|
||||
'src/login/new.html',
|
||||
'<html><body><h1>Login page</h1></body><html>',
|
||||
);
|
||||
|
||||
setupTarget(harness, {
|
||||
assets: ['src/login'],
|
||||
optimization: {
|
||||
scripts: true,
|
||||
},
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, 'login/new');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.status).toBe(200);
|
||||
expect(await response?.text()).toContain('<h1>Login page</h1>');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
|
||||
describe('Behavior: "buildTarget baseHref"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness, {
|
||||
baseHref: '/test/',
|
||||
});
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', 'console.log("foo");');
|
||||
});
|
||||
|
||||
it('uses the baseHref defined in the "buildTarget" options as the serve path', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/test/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
const baseUrl = new URL(`${result?.baseUrl}/`);
|
||||
expect(baseUrl.pathname).toBe('/test/');
|
||||
expect(await response?.text()).toContain('console.log');
|
||||
});
|
||||
|
||||
it('serves the application from baseHref location without trailing slash', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('<script src="main.js" type="module">');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { BudgetType } from '../../../../utils/bundle-calculator';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// TODO(fix-vite): currently this is broken in vite.
|
||||
(isViteRun ? xdescribe : describe)('Behavior: "browser builder budgets"', () => {
|
||||
beforeEach(() => {
|
||||
setupTarget(harness, {
|
||||
// Add a budget error for any file over 100 bytes
|
||||
budgets: [{ type: BudgetType.All, maximumError: '100b' }],
|
||||
optimization: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore budgets defined in the "buildTarget" options', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result } = await harness.executeOnce();
|
||||
|
||||
expect(result?.success).toBe(true);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
|
||||
describe('Behavior: "browser builder inline critical css"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness, {
|
||||
optimization: {
|
||||
styles: {
|
||||
minify: true,
|
||||
inlineCritical: true,
|
||||
},
|
||||
},
|
||||
styles: ['src/styles.css'],
|
||||
});
|
||||
|
||||
await harness.writeFiles({
|
||||
'src/styles.css': 'body { color: #000 }',
|
||||
});
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', '');
|
||||
});
|
||||
|
||||
it('inlines critical css when enabled in the "browserTarget" options', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('body{color:#000}');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
import { concatMap, count, take, timeout } from 'rxjs';
|
||||
import { URL } from 'url';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// TODO(fix-vite): currently this is broken in vite.
|
||||
(isViteRun ? xdescribe : describe)(
|
||||
'Behavior: "i18n $localize calls are replaced during watching"',
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
harness.useProject('test', {
|
||||
root: '.',
|
||||
sourceRoot: 'src',
|
||||
cli: {
|
||||
cache: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
sourceLocale: {
|
||||
'code': 'fr',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setupTarget(harness, { localize: ['fr'] });
|
||||
});
|
||||
|
||||
it('$localize are replaced in watch', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
watch: true,
|
||||
});
|
||||
|
||||
await harness.writeFile(
|
||||
'src/app/app.component.html',
|
||||
`
|
||||
<p id="hello" i18n="An introduction header for this sample">Hello {{ title }}! </p>
|
||||
`,
|
||||
);
|
||||
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
timeout(BUILD_TIMEOUT * 2),
|
||||
concatMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBe(true);
|
||||
|
||||
const response = await fetch(new URL('main.js', `${result?.baseUrl}`));
|
||||
expect(await response?.text()).not.toContain('$localize`:');
|
||||
|
||||
switch (index) {
|
||||
case 0: {
|
||||
await harness.modifyFile('src/app/app.component.html', (content) =>
|
||||
content.replace('introduction', 'intro'),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
import { concatMap, count, take, timeout } from 'rxjs';
|
||||
import { URL } from 'url';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// TODO(fix-vite): currently this is broken in vite.
|
||||
(isViteRun ? xdescribe : describe)('Behavior: "i18n translation file watching"', () => {
|
||||
beforeEach(() => {
|
||||
harness.useProject('test', {
|
||||
root: '.',
|
||||
sourceRoot: 'src',
|
||||
cli: {
|
||||
cache: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
locales: {
|
||||
'fr': 'src/locales/messages.fr.xlf',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setupTarget(harness, { localize: ['fr'] });
|
||||
});
|
||||
|
||||
it('watches i18n translation files by default', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
watch: true,
|
||||
});
|
||||
|
||||
await harness.writeFile(
|
||||
'src/app/app.component.html',
|
||||
`
|
||||
<p id="hello" i18n="An introduction header for this sample">Hello {{ title }}! </p>
|
||||
`,
|
||||
);
|
||||
|
||||
await harness.writeFile('src/locales/messages.fr.xlf', TRANSLATION_FILE_CONTENT);
|
||||
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
timeout(BUILD_TIMEOUT),
|
||||
concatMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBe(true);
|
||||
|
||||
const mainUrl = new URL('main.js', `${result?.baseUrl}`);
|
||||
|
||||
switch (index) {
|
||||
case 0: {
|
||||
const response = await fetch(mainUrl);
|
||||
expect(await response?.text()).toContain('Bonjour');
|
||||
|
||||
await harness.modifyFile('src/locales/messages.fr.xlf', (content) =>
|
||||
content.replace('Bonjour', 'Salut'),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
const response = await fetch(mainUrl);
|
||||
expect(await response?.text()).toContain('Salut');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const TRANSLATION_FILE_CONTENT = `
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file target-language="en-US" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="4286451273117902052" datatype="html">
|
||||
<target>Bonjour <x id="INTERPOLATION" equiv-text="{{ title }}"/>! </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="targetfile">src/app/app.component.html</context>
|
||||
<context context-type="linenumber">2,3</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">An introduction header for this sample</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
@ -0,0 +1,321 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { tags } from '@angular-devkit/core';
|
||||
import { createServer } from 'http';
|
||||
import { createProxyServer } from 'http-proxy';
|
||||
import { AddressInfo } from 'net';
|
||||
import puppeteer, { Browser, Page } from 'puppeteer';
|
||||
import { count, debounceTime, finalize, switchMap, take, timeout } from 'rxjs';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
declare const document: any;
|
||||
|
||||
interface ProxyInstance {
|
||||
server: typeof createProxyServer extends () => infer R ? R : never;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function findFreePort(): Promise<number> {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const server = createServer();
|
||||
server.once('listening', () => {
|
||||
const port = (server.address() as AddressInfo).port;
|
||||
server.close((e) => (e ? reject(e) : resolve(port)));
|
||||
});
|
||||
server.once('error', (e) => server.close(() => reject(e)));
|
||||
server.listen();
|
||||
});
|
||||
}
|
||||
|
||||
async function createProxy(target: string, secure: boolean, ws = true): Promise<ProxyInstance> {
|
||||
const proxyPort = await findFreePort();
|
||||
const server = createProxyServer({
|
||||
ws,
|
||||
target,
|
||||
secure,
|
||||
ssl: secure && {
|
||||
key: tags.stripIndents`
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEBRUsUz4rdcMt
|
||||
CQGLvG3SzUinsmgdgOyTNQNA0eOMyRSrmS8L+F/kSLUnqqu4mzdeqDzo2Xj553jK
|
||||
dRqMCRFGJuGnQ/VIbW2A+ywgrqILuDyF5i4PL1aQW4yJ7TnXfONKfpswQArlN6DF
|
||||
gBYJtoJlf8XD1sOeJpsv/O46/ix/wngQ+GwQQ2cfqxQT0fE9SBCY23VNt3SPUJ3k
|
||||
9etJMvJ9U9GHSb1CFdNQe7Gyx7xdKf1TazB27ElNZEg2aF99if47uRskYjvvFivy
|
||||
7nxGx/ccIwjwNMpk29AsKG++0sn1yTK7tD5Px6aCSVK0BKbdXZS2euJor8hASGBJ
|
||||
3GpVGJvdAgMBAAECggEAapYo8TVCdPdP7ckb4hPP0/R0MVu9aW2VNmZ5ImH+zar5
|
||||
ZmWhQ20HF2bBupP/VB5yeTIaDLNUKO9Iqy4KBWNY1UCHKyC023FFPgFV+V98FctU
|
||||
faqwGOmwtEZToRwxe48ZOISndhEc247oCPyg/x8SwIY9z0OUkwaDFBEAqWtUXxM3
|
||||
/SPpCT5ilLgxnRgVB8Fj5Z0q7ThnxNVOmVC1OSIakEj46PzmMXn1pCKLOCUmAAOQ
|
||||
BnrOZuty2b8b2M/GHsktLZwojQQJmArnIBymTXQTVhaGgKSyOv1qvHLp9L1OJf0/
|
||||
Xm+/TqT6ztzhzlftcObdfQZZ5JuoEwlvyrsGFlA3MQKBgQDiQC3KYMG8ViJkWrv6
|
||||
XNAFEoAjVEKrtirGWJ66YfQ9KSJ7Zttrd1Y1V1OLtq3z4YMH39wdQ8rOD+yR8mWV
|
||||
6Tnsxma6yJXAH8uan8iVbxjIZKF1hnvNCxUoxYmWOmTLcEQMzmxvTzAiR+s6R6Uj
|
||||
9LgGqppt30nM4wnOhOJU6UxqbwKBgQDdy03KidbPZuycJSy1C9AIt0jlrxDsYm+U
|
||||
fZrB6mHEZcgoZS5GbLKinQCdGcgERa05BXvJmNbfZtT5a37YEnbjsTImIhDiBP5P
|
||||
nW36/9a3Vg1svd1KP2206/Bh3gfZbgTsQg4YogXgjf0Uzuvw18btgTtLVpVyeuqz
|
||||
TU3eeF30cwKBgQCN6lvOmapsDEs+T3uhqx4AUH53qp63PmjOSUAnANJGmsq6ROZV
|
||||
HmHAy6nn9Qpf85BRHCXhZWiMoIhvc3As/EINNtWxS6hC/q6jqp4SvcD50cVFBroY
|
||||
/16iWGXZCX+37A+DSOfTWgSDPEFcKRx41UOpStHbITgVgEPieo/NWxlHmQKBgQDX
|
||||
JOLs2RB6V0ilnpnjdPXzvncD9fHgmwvJap24BPeZX3HtXViqD76oZsu1mNCg9EW3
|
||||
zk3pnEyyoDlvSIreZerVq4kN3HWsCVP3Pqr0kz9g0CRtmy8RWr28hjHDfXD3xPUZ
|
||||
iGnMEz7IOHOKv722/liFAprV1cNaLUmFbDNg3jmlaQKBgQDG5WwngPhOHmjTnSml
|
||||
amfEz9a4yEhQqpqgVNW5wwoXOf6DbjL2m/maJh01giThj7inMcbpkZlIclxD0Eu6
|
||||
Lof+ctCeqSAJvaVPmd+nv8Yp26zsF1yM8ax9xXjrIvv9fSbycNveGTDCsNNTiYoW
|
||||
QyvMqmN1kGy20SZbQDD/fLfqBQ==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`,
|
||||
cert: tags.stripIndents`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJALz8gD/gAt0OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMTgyMTQ5WhcNMTkxMDIzMTgyMTQ5WjBF
|
||||
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEAxAUVLFM+K3XDLQkBi7xt0s1Ip7JoHYDskzUDQNHjjMkUq5kvC/hf5Ei1
|
||||
J6qruJs3Xqg86Nl4+ed4ynUajAkRRibhp0P1SG1tgPssIK6iC7g8heYuDy9WkFuM
|
||||
ie0513zjSn6bMEAK5TegxYAWCbaCZX/Fw9bDniabL/zuOv4sf8J4EPhsEENnH6sU
|
||||
E9HxPUgQmNt1Tbd0j1Cd5PXrSTLyfVPRh0m9QhXTUHuxsse8XSn9U2swduxJTWRI
|
||||
NmhffYn+O7kbJGI77xYr8u58Rsf3HCMI8DTKZNvQLChvvtLJ9ckyu7Q+T8emgklS
|
||||
tASm3V2UtnriaK/IQEhgSdxqVRib3QIDAQABo1AwTjAdBgNVHQ4EFgQUDZBhVKdb
|
||||
3BRhLIhuuE522Vsul0IwHwYDVR0jBBgwFoAUDZBhVKdb3BRhLIhuuE522Vsul0Iw
|
||||
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABh9WWZwWLgb9/DcTxL72
|
||||
6pI96t4jiF79Q+pPefkaIIi0mE6yodWrTAsBQu9I6bNRaEcCSoiXkP2bqskD/UGg
|
||||
LwUFgSrDOAA3UjdHw3QU5g2NocduG7mcFwA40TB98sOsxsUyYlzSyWzoiQWwPYwb
|
||||
hek1djuWkqPXsTjlj54PTPN/SjTFmo4p5Ip6nbRf2nOREl7v0rJpGbJvXiCMYyd+
|
||||
Zv+j4mRjCGo8ysMR2HjCUGkYReLAgKyyz3M7i8vevJhKslyOmy6Txn4F0nPVumaU
|
||||
DDIy4xXPW1STWfsmSYJfYW3wa0wk+pJQ3j2cTzkPQQ8gwpvM3U9DJl43uwb37v6I
|
||||
7Q==
|
||||
-----END CERTIFICATE-----
|
||||
`,
|
||||
},
|
||||
}).listen(proxyPort);
|
||||
|
||||
return {
|
||||
server,
|
||||
url: `${secure ? 'https' : 'http'}://localhost:${proxyPort}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function goToPageAndWaitForWS(page: Page, url: string): Promise<void> {
|
||||
const baseUrl = url.replace(/^http/, 'ws');
|
||||
const socksRequest =
|
||||
baseUrl[baseUrl.length - 1] === '/' ? `${baseUrl}ng-cli-ws` : `${baseUrl}/ng-cli-ws`;
|
||||
// Create a Chrome dev tools session so that we can capturing websocket request.
|
||||
// https://github.com/puppeteer/puppeteer/issues/2974
|
||||
|
||||
// We do this, to ensure that we make the right request with the expected host, port etc...
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Network.enable');
|
||||
await client.send('Page.enable');
|
||||
|
||||
await Promise.all([
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error(`A Websocket connected to ${socksRequest} was not established.`)),
|
||||
2000,
|
||||
);
|
||||
client.on('Network.webSocketCreated', ({ url }) => {
|
||||
if (url.startsWith(socksRequest)) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}),
|
||||
page.goto(url),
|
||||
]);
|
||||
|
||||
await client.detach();
|
||||
}
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// TODO(fix-vite): currently this is broken in vite.
|
||||
(isViteRun ? xdescribe : describe)(
|
||||
'Behavior: "Dev-server builder live-reload with proxies"',
|
||||
() => {
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
|
||||
const SERVE_OPTIONS = Object.freeze({
|
||||
...BASE_OPTIONS,
|
||||
hmr: false,
|
||||
watch: true,
|
||||
liveReload: true,
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await puppeteer.launch({
|
||||
// MacOSX users need to set the local binary manually because Chrome has lib files with
|
||||
// spaces in them which Bazel does not support in runfiles
|
||||
// See: https://github.com/angular/angular-cli/pull/17624
|
||||
// eslint-disable-next-line max-len
|
||||
// executablePath: '/Users/<USERNAME>/git/angular-cli/node_modules/puppeteer/.local-chromium/mac-818858/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
|
||||
ignoreHTTPSErrors: true,
|
||||
args: ['--no-sandbox', '--disable-gpu'],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness, {
|
||||
polyfills: ['src/polyfills.ts'],
|
||||
});
|
||||
|
||||
page = await browser.newPage();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
it('works without proxy', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...SERVE_OPTIONS,
|
||||
});
|
||||
|
||||
await harness.writeFile('src/app/app.component.html', '<p>{{ title }}</p>');
|
||||
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
debounceTime(1000),
|
||||
timeout(BUILD_TIMEOUT * 2),
|
||||
switchMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBeTrue();
|
||||
if (typeof result?.baseUrl !== 'string') {
|
||||
throw new Error('Expected "baseUrl" to be a string.');
|
||||
}
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
await goToPageAndWaitForWS(page, result.baseUrl);
|
||||
await harness.modifyFile('src/app/app.component.ts', (content) =>
|
||||
content.replace(`'app'`, `'app-live-reload'`),
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
const innerText = await page.evaluate(
|
||||
() => document.querySelector('p').innerText,
|
||||
);
|
||||
expect(innerText).toBe('app-live-reload');
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
|
||||
it('works without http -> http proxy', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...SERVE_OPTIONS,
|
||||
});
|
||||
|
||||
await harness.writeFile('src/app/app.component.html', '<p>{{ title }}</p>');
|
||||
|
||||
let proxy: ProxyInstance | undefined;
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
debounceTime(1000),
|
||||
timeout(BUILD_TIMEOUT * 2),
|
||||
switchMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBeTrue();
|
||||
if (typeof result?.baseUrl !== 'string') {
|
||||
throw new Error('Expected "baseUrl" to be a string.');
|
||||
}
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
proxy = await createProxy(result.baseUrl, false);
|
||||
await goToPageAndWaitForWS(page, proxy.url);
|
||||
await harness.modifyFile('src/app/app.component.ts', (content) =>
|
||||
content.replace(`'app'`, `'app-live-reload'`),
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
const innerText = await page.evaluate(
|
||||
() => document.querySelector('p').innerText,
|
||||
);
|
||||
expect(innerText).toBe('app-live-reload');
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
finalize(() => {
|
||||
proxy?.server.close();
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
|
||||
it('works without https -> http proxy', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...SERVE_OPTIONS,
|
||||
});
|
||||
|
||||
await harness.writeFile('src/app/app.component.html', '<p>{{ title }}</p>');
|
||||
|
||||
let proxy: ProxyInstance | undefined;
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
debounceTime(1000),
|
||||
timeout(BUILD_TIMEOUT * 2),
|
||||
switchMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBeTrue();
|
||||
if (typeof result?.baseUrl !== 'string') {
|
||||
throw new Error('Expected "baseUrl" to be a string.');
|
||||
}
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
proxy = await createProxy(result.baseUrl, true);
|
||||
await goToPageAndWaitForWS(page, proxy.url);
|
||||
await harness.modifyFile('src/app/app.component.ts', (content) =>
|
||||
content.replace(`'app'`, `'app-live-reload'`),
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
const innerText = await page.evaluate(
|
||||
() => document.querySelector('p').innerText,
|
||||
);
|
||||
expect(innerText).toBe('app-live-reload');
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
finalize(() => {
|
||||
proxy?.server.close();
|
||||
}),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { concatMap, count, take, timeout } from 'rxjs';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
const manifest = {
|
||||
index: '/index.html',
|
||||
assetGroups: [
|
||||
{
|
||||
name: 'app',
|
||||
installMode: 'prefetch',
|
||||
resources: {
|
||||
files: ['/favicon.ico', '/index.html'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'assets',
|
||||
installMode: 'lazy',
|
||||
updateMode: 'prefetch',
|
||||
resources: {
|
||||
files: [
|
||||
'/media/**',
|
||||
'/assets/**',
|
||||
'/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
describe('Behavior: "dev-server builder serves service worker"', () => {
|
||||
beforeEach(async () => {
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', '');
|
||||
await harness.writeFile('src/polyfills.ts', '');
|
||||
|
||||
harness.useProject('test', {
|
||||
root: '.',
|
||||
sourceRoot: 'src',
|
||||
cli: {
|
||||
cache: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
sourceLocale: {
|
||||
'code': 'fr',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('works with service worker', async () => {
|
||||
setupTarget(harness, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serviceWorker: (isViteRun ? 'ngsw-config.json' : true) as any,
|
||||
assets: ['src/favicon.ico', 'src/assets'],
|
||||
styles: ['src/styles.css'],
|
||||
});
|
||||
|
||||
await harness.writeFiles({
|
||||
'ngsw-config.json': JSON.stringify(manifest),
|
||||
'src/assets/folder-asset.txt': 'folder-asset.txt',
|
||||
'src/styles.css': `body { background: url(./spectrum.png); }`,
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/ngsw.json');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
|
||||
expect(await response?.json()).toEqual(
|
||||
jasmine.objectContaining({
|
||||
configVersion: 1,
|
||||
index: '/index.html',
|
||||
navigationUrls: [
|
||||
{ positive: true, regex: '^\\/.*$' },
|
||||
{ positive: false, regex: '^\\/(?:.+\\/)?[^/]*\\.[^/]*$' },
|
||||
{ positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*$' },
|
||||
{ positive: false, regex: '^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$' },
|
||||
],
|
||||
assetGroups: [
|
||||
{
|
||||
name: 'app',
|
||||
installMode: 'prefetch',
|
||||
updateMode: 'prefetch',
|
||||
urls: ['/favicon.ico', '/index.html'],
|
||||
cacheQueryOptions: {
|
||||
ignoreVary: true,
|
||||
},
|
||||
patterns: [],
|
||||
},
|
||||
{
|
||||
name: 'assets',
|
||||
installMode: 'lazy',
|
||||
updateMode: 'prefetch',
|
||||
urls: ['/assets/folder-asset.txt', '/media/spectrum.png'],
|
||||
cacheQueryOptions: {
|
||||
ignoreVary: true,
|
||||
},
|
||||
patterns: [],
|
||||
},
|
||||
],
|
||||
dataGroups: [],
|
||||
hashTable: {
|
||||
'/favicon.ico': '84161b857f5c547e3699ddfbffc6d8d737542e01',
|
||||
'/assets/folder-asset.txt': '617f202968a6a81050aa617c2e28e1dca11ce8d4',
|
||||
'/index.html': isViteRun
|
||||
? 'e5b73e6798d2782bf59dd5272d254d5bde364695'
|
||||
: '9d232e3e13b4605d197037224a2a6303dd337480',
|
||||
'/media/spectrum.png': '8d048ece46c0f3af4b598a95fd8e4709b631c3c0',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('works with localize', async () => {
|
||||
setupTarget(harness, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serviceWorker: (isViteRun ? 'ngsw-config.json' : true) as any,
|
||||
assets: ['src/favicon.ico', 'src/assets'],
|
||||
styles: ['src/styles.css'],
|
||||
localize: ['fr'],
|
||||
});
|
||||
|
||||
await harness.writeFiles({
|
||||
'ngsw-config.json': JSON.stringify(manifest),
|
||||
'src/assets/folder-asset.txt': 'folder-asset.txt',
|
||||
'src/styles.css': `body { background: url(./spectrum.png); }`,
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/ngsw.json');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
|
||||
expect(await response?.json()).toBeDefined();
|
||||
});
|
||||
|
||||
// TODO(fix-vite): currently this is broken in vite due to watcher never terminates.
|
||||
(isViteRun ? xit : it)('works in watch mode', async () => {
|
||||
setupTarget(harness, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
serviceWorker: (isViteRun ? 'ngsw-config.json' : true) as any,
|
||||
assets: ['src/favicon.ico', 'src/assets'],
|
||||
styles: ['src/styles.css'],
|
||||
});
|
||||
|
||||
await harness.writeFiles({
|
||||
'ngsw-config.json': JSON.stringify(manifest),
|
||||
'src/assets/folder-asset.txt': 'folder-asset.txt',
|
||||
'src/styles.css': `body { background: url(./spectrum.png); }`,
|
||||
});
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
watch: true,
|
||||
});
|
||||
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
timeout(BUILD_TIMEOUT),
|
||||
concatMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBeTrue();
|
||||
const response = await fetch(new URL('ngsw.json', `${result?.baseUrl}`));
|
||||
const { hashTable } = (await response.json()) as { hashTable: object };
|
||||
const hashTableEntries = Object.keys(hashTable);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
expect(hashTableEntries).toEqual([
|
||||
'/assets/folder-asset.txt',
|
||||
'/favicon.ico',
|
||||
'/index.html',
|
||||
'/media/spectrum.png',
|
||||
]);
|
||||
|
||||
await harness.writeFile(
|
||||
'src/assets/folder-new-asset.txt',
|
||||
harness.readFile('src/assets/folder-asset.txt'),
|
||||
);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
expect(hashTableEntries).toEqual([
|
||||
'/assets/folder-asset.txt',
|
||||
'/assets/folder-new-asset.txt',
|
||||
'/favicon.ico',
|
||||
'/index.html',
|
||||
'/media/spectrum.png',
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { lastValueFrom, mergeMap, take, timeout } from 'rxjs';
|
||||
import { URL } from 'url';
|
||||
import {
|
||||
BuilderHarness,
|
||||
BuilderHarnessExecutionOptions,
|
||||
BuilderHarnessExecutionResult,
|
||||
} from './setup';
|
||||
|
||||
export async function executeOnceAndFetch<T>(
|
||||
harness: BuilderHarness<T>,
|
||||
url: string,
|
||||
options?: Partial<BuilderHarnessExecutionOptions> & { request?: RequestInit },
|
||||
): Promise<BuilderHarnessExecutionResult & { response?: Response; content?: string }> {
|
||||
return lastValueFrom(
|
||||
harness.execute().pipe(
|
||||
timeout(30000),
|
||||
mergeMap(async (executionResult) => {
|
||||
let response = undefined;
|
||||
let content = undefined;
|
||||
if (executionResult.result?.success) {
|
||||
let baseUrl = `${executionResult.result.baseUrl}`;
|
||||
baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`;
|
||||
const resolvedUrl = new URL(url, baseUrl);
|
||||
const originalResponse = await fetch(resolvedUrl, options?.request);
|
||||
response = originalResponse.clone();
|
||||
// Ensure all data is available before stopping server
|
||||
content = await originalResponse.text();
|
||||
}
|
||||
|
||||
return { ...executionResult, response, content };
|
||||
}),
|
||||
take(1),
|
||||
),
|
||||
);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { BuilderHandlerFn } from '@angular-devkit/architect';
|
||||
import { json } from '@angular-devkit/core';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { JasmineBuilderHarness, host, setupApplicationTarget } from './setup';
|
||||
|
||||
const optionSchemaCache = new Map<string, json.schema.JsonSchema>();
|
||||
|
||||
export function describeServeBuilder<T>(
|
||||
builderHandler: BuilderHandlerFn<T & json.JsonObject>,
|
||||
options: { name?: string; schemaPath: string },
|
||||
specDefinitions: (
|
||||
harness: JasmineBuilderHarness<T>,
|
||||
setupTarget: typeof setupApplicationTarget,
|
||||
isViteRun: true,
|
||||
) => void,
|
||||
): void {
|
||||
let optionSchema = optionSchemaCache.get(options.schemaPath);
|
||||
if (optionSchema === undefined) {
|
||||
optionSchema = JSON.parse(readFileSync(options.schemaPath, 'utf8')) as json.schema.JsonSchema;
|
||||
optionSchemaCache.set(options.schemaPath, optionSchema);
|
||||
}
|
||||
const harness = new JasmineBuilderHarness<T>(builderHandler, host, {
|
||||
builderName: options.name,
|
||||
optionSchema,
|
||||
});
|
||||
|
||||
describe(options.name || builderHandler.name, () => {
|
||||
beforeEach(() => host.initialize().toPromise());
|
||||
afterEach(() => host.restore().toPromise());
|
||||
|
||||
specDefinitions(harness, setupApplicationTarget, true);
|
||||
});
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
const FETCH_HEADERS = Object.freeze({ host: 'example.com' });
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// TODO(fix-vite): currently this is broken in vite.
|
||||
(isViteRun ? xdescribe : xdescribe)('option: "allowedHosts"', () => {
|
||||
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 executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toBe('Invalid Host header');
|
||||
});
|
||||
|
||||
it('does not allow an invalid host when option is an empty array', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
allowedHosts: [],
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toBe('Invalid Host header');
|
||||
});
|
||||
|
||||
it('allows a host when specified in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
allowedHosts: ['example.com'],
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
const FETCH_HEADERS = Object.freeze({ host: 'example.com' });
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// This option is not used when using vite.
|
||||
(isViteRun ? xdescribe : xdescribe)('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 executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toBe('Invalid Host header');
|
||||
});
|
||||
|
||||
it('does not allow an invalid host when option is false', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
disableHostCheck: false,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toBe('Invalid Host header');
|
||||
});
|
||||
|
||||
it('allows a host when option is true', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
disableHostCheck: true,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
const ESBUILD_LOG_TEXT = 'Application bundle generation complete.';
|
||||
const WEBPACK_LOG_TEXT = 'Compiled successfully.';
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
describe('option: "forceEsbuild"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness, {});
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', 'console.log("foo");');
|
||||
});
|
||||
|
||||
it('should use build target specified build system when not present', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
forceEsbuild: undefined,
|
||||
});
|
||||
|
||||
const { result, response, logs } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('console.log');
|
||||
expect(logs).toContain(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(isViteRun ? ESBUILD_LOG_TEXT : WEBPACK_LOG_TEXT),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should use build target specified build system when false', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
forceEsbuild: false,
|
||||
});
|
||||
|
||||
const { result, response, logs } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('console.log');
|
||||
expect(logs).toContain(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(isViteRun ? ESBUILD_LOG_TEXT : WEBPACK_LOG_TEXT),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should always use the esbuild build system with Vite when true', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
forceEsbuild: true,
|
||||
});
|
||||
|
||||
const { result, response, logs } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('console.log');
|
||||
expect(logs).toContain(
|
||||
jasmine.objectContaining({ message: jasmine.stringMatching(ESBUILD_LOG_TEXT) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { URL } from 'url';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
function getResultPort(result: Record<string, unknown> | undefined): string | undefined {
|
||||
if (typeof result?.baseUrl !== 'string') {
|
||||
fail(`Expected builder result with a string 'baseUrl' property. Received: ${result?.baseUrl}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(result.baseUrl).port;
|
||||
} catch {
|
||||
fail(`Expected a valid URL in builder result 'baseUrl' property. Received: ${result.baseUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
describe('option: "port"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness);
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', '');
|
||||
});
|
||||
|
||||
it('uses default port (4200) when not present', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
// Base options set port to zero
|
||||
port: undefined,
|
||||
});
|
||||
|
||||
const { result, response, logs } = await executeOnceAndFetch(harness, '/');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(getResultPort(result)).toBe('4200');
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
|
||||
if (!isViteRun) {
|
||||
expect(logs).toContain(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(/:4200/),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses a random free port when set to 0 (zero)', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
const { result, response, logs } = await executeOnceAndFetch(harness, '/');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
const port = getResultPort(result);
|
||||
expect(port).not.toBe('4200');
|
||||
if (isViteRun) {
|
||||
// Should not be default Vite port either
|
||||
expect(port).not.toBe('5173');
|
||||
}
|
||||
|
||||
expect(port).toMatch(/\d{4,6}/);
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
|
||||
if (!isViteRun) {
|
||||
expect(logs).toContain(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(':' + port),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('uses specific port when a non-zero number is specified', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
port: 8000,
|
||||
});
|
||||
|
||||
const { result, response, logs } = await executeOnceAndFetch(harness, '/');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(getResultPort(result)).toBe('8000');
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
if (!isViteRun) {
|
||||
expect(logs).toContain(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching(':8000'),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
// TODO: Temporarily disabled pending investigation into test-only Vite not stopping when caching is enabled
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// prebundling is not available in webpack
|
||||
(isViteRun ? xdescribe : xdescribe)('option: "prebundle"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness);
|
||||
|
||||
harness.useProject('test', {
|
||||
cli: {
|
||||
cache: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile(
|
||||
'src/main.ts',
|
||||
`
|
||||
import { VERSION as coreVersion } from '@angular/core';
|
||||
import { VERSION as platformVersion } from '@angular/platform-browser';
|
||||
|
||||
console.log(coreVersion);
|
||||
console.log(platformVersion);
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should prebundle dependencies when option is not present', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(content).toContain('vite/deps/@angular_core.js');
|
||||
expect(content).not.toContain('node_modules/@angular/core/');
|
||||
});
|
||||
|
||||
it('should prebundle dependencies when option is set to true', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
prebundle: true,
|
||||
});
|
||||
|
||||
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(content).toContain('vite/deps/@angular_core.js');
|
||||
expect(content).not.toContain('node_modules/@angular/core/');
|
||||
});
|
||||
|
||||
it('should not prebundle dependencies when option is set to false', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
prebundle: false,
|
||||
});
|
||||
|
||||
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(content).not.toContain('vite/deps/@angular_core.js');
|
||||
expect(content).toContain('node_modules/@angular/core/');
|
||||
});
|
||||
|
||||
it('should not prebundle specified dependency if added to exclude list', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
prebundle: { exclude: ['@angular/platform-browser'] },
|
||||
});
|
||||
|
||||
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(content).toContain('vite/deps/@angular_core.js');
|
||||
expect(content).not.toContain('node_modules/@angular/core/');
|
||||
expect(content).not.toContain('vite/deps/@angular_platform-browser.js');
|
||||
expect(content).toContain('node_modules/@angular/platform-browser/');
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,323 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { createServer } from 'node:http';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO, BuilderHarness } from '../setup';
|
||||
|
||||
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget, isVite) => {
|
||||
describe('option: "proxyConfig"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness);
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', '');
|
||||
});
|
||||
|
||||
it('proxies requests based on the `.json` proxy configuration file provided in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `{ "/api/*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('proxies requests based on the `.json` (with comments) proxy configuration file provided in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `
|
||||
// JSON file with comments
|
||||
{ "/api/*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }
|
||||
`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('proxies requests based on the `.js` (CommonJS) proxy configuration file provided in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.js',
|
||||
});
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.js': `module.exports = { "/api/*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('proxies requests based on the `.js` (ESM) proxy configuration file provided in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.js',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.js': `export default { "/api/*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }`,
|
||||
'package.json': '{ "type": "module" }',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('proxies requests based on the `.cjs` proxy configuration file provided in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.cjs',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
const proxyAddress = proxyServer.address;
|
||||
|
||||
await harness.writeFiles({
|
||||
'proxy.config.cjs': `module.exports = { "/api/*": { "target": "http://127.0.0.1:${proxyAddress.port}" } }`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('proxies requests based on the `.mjs` proxy configuration file provided in the option', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.mjs',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.mjs': `export default { "/api/*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('supports the Webpack array form of the configuration file', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `[ { "context": ["/api", "/abc"], "target": "http://127.0.0.1:${proxyServer.address.port}" } ]`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error when proxy configuration file cannot be found', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'INVALID.json',
|
||||
});
|
||||
|
||||
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(error).toEqual(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching('INVALID\\.json does not exist'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when JSON proxy configuration file cannot be parsed', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
// Create a JSON file with a parse error (target property has no value)
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `
|
||||
// JSON file with comments
|
||||
{ "/api/*": { "target": } }
|
||||
`,
|
||||
});
|
||||
|
||||
const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(error).toEqual(
|
||||
jasmine.objectContaining({
|
||||
message: jasmine.stringMatching('contains parse errors:\\n\\[3, 35\\] ValueExpected'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('supports negation of globs', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `
|
||||
{ "!something/**/*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }
|
||||
`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ****************************************************************************************************
|
||||
* ********************************** Below only Vite specific tests **********************************
|
||||
* ****************************************************************************************************
|
||||
*/
|
||||
if (isVite) {
|
||||
viteOnlyTests(harness);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates an HTTP Server used for proxy testing that provides a `/test` endpoint
|
||||
* that returns a 200 response with a body of `TEST_API_RETURN`. All other requests
|
||||
* will return a 404 response.
|
||||
*/
|
||||
async function createProxyServer() {
|
||||
const proxyServer = createServer((request, response) => {
|
||||
if (request.url?.endsWith('/test')) {
|
||||
response.writeHead(200);
|
||||
response.end('TEST_API_RETURN');
|
||||
} else {
|
||||
response.writeHead(404);
|
||||
response.end();
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => proxyServer.listen(0, '127.0.0.1', resolve));
|
||||
|
||||
return {
|
||||
address: proxyServer.address() as import('net').AddressInfo,
|
||||
close: () => new Promise<void>((resolve) => proxyServer.close(() => resolve())),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite specific tests
|
||||
*/
|
||||
function viteOnlyTests(harness: BuilderHarness<unknown>): void {
|
||||
it('proxies support regexp as context', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `
|
||||
{ "^/api/.*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }
|
||||
`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('proxies support negated regexp as context', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
proxyConfig: 'proxy.config.json',
|
||||
});
|
||||
|
||||
const proxyServer = await createProxyServer();
|
||||
try {
|
||||
await harness.writeFiles({
|
||||
'proxy.config.json': `
|
||||
{ "^\\/(?!something).*": { "target": "http://127.0.0.1:${proxyServer.address.port}" } }
|
||||
`,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/test');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('TEST_API_RETURN');
|
||||
} finally {
|
||||
await proxyServer.close();
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
const FETCH_HEADERS = Object.freeze({ host: 'example.com' });
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
// This option is not used when using vite.
|
||||
(isViteRun ? xdescribe : xdescribe)('option: "publicHost"', () => {
|
||||
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 executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toBe('Invalid Host header');
|
||||
});
|
||||
|
||||
it('does not allow an invalid host when option is a different host', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
publicHost: 'example.net',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toBe('Invalid Host header');
|
||||
});
|
||||
|
||||
it('allows a host when option is set to used host', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
publicHost: 'example.com',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
request: { headers: FETCH_HEADERS },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { URL } from 'url';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { executeOnceAndFetch } from '../execute-fetch';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(
|
||||
executeDevServer,
|
||||
DEV_SERVER_BUILDER_INFO,
|
||||
(harness, setupTarget, isViteRun) => {
|
||||
describe('option: "servePath"', () => {
|
||||
beforeEach(async () => {
|
||||
setupTarget(harness, {
|
||||
assets: ['src/assets'],
|
||||
});
|
||||
|
||||
// Application code is not needed for these tests
|
||||
await harness.writeFile('src/main.ts', 'console.log("foo");');
|
||||
});
|
||||
|
||||
it('serves application at the root when option is not present', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
const baseUrl = new URL(`${result?.baseUrl}`);
|
||||
expect(baseUrl.pathname).toBe('/');
|
||||
expect(await response?.text()).toContain('console.log');
|
||||
});
|
||||
|
||||
it('serves application at specified path when option is used', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
servePath: 'test',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/test/main.js');
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
const baseUrl = new URL(`${result?.baseUrl}/`);
|
||||
expect(baseUrl.pathname).toBe('/test/');
|
||||
expect(await response?.text()).toContain('console.log');
|
||||
});
|
||||
|
||||
// TODO(fix-vite): currently this is broken in vite.
|
||||
(isViteRun ? xit : it)('does not rewrite from root when option is used', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
servePath: 'test',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/', {
|
||||
// fallback processing requires an accept header
|
||||
request: { headers: { accept: 'text/html' } },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(response?.status).toBe(404);
|
||||
});
|
||||
|
||||
it('does not rewrite from path outside serve path when option is used', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
servePath: 'test',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/api/', {
|
||||
// fallback processing requires an accept header
|
||||
request: { headers: { accept: 'text/html' } },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(response?.status).toBe(404);
|
||||
});
|
||||
|
||||
it('rewrites from path inside serve path when option is used', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
servePath: 'test',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/test/inside', {
|
||||
// fallback processing requires an accept header
|
||||
request: { headers: { accept: 'text/html' } },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('<title>');
|
||||
});
|
||||
|
||||
it('serves assets at specified path when option is used', async () => {
|
||||
await harness.writeFile('src/assets/test.txt', 'hello world!');
|
||||
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
servePath: 'test',
|
||||
});
|
||||
|
||||
const { result, response } = await executeOnceAndFetch(harness, '/test/assets/test.txt', {
|
||||
// fallback processing requires an accept header
|
||||
request: { headers: { accept: 'text/html' } },
|
||||
});
|
||||
|
||||
expect(result?.success).toBeTrue();
|
||||
expect(await response?.text()).toContain('hello world');
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { TimeoutError, concatMap, count, take, timeout } from 'rxjs';
|
||||
import { executeDevServer } from '../../index';
|
||||
import { describeServeBuilder } from '../jasmine-helpers';
|
||||
import { BASE_OPTIONS, BUILD_TIMEOUT, DEV_SERVER_BUILDER_INFO } from '../setup';
|
||||
|
||||
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
|
||||
describe('Option: "watch"', () => {
|
||||
beforeEach(() => {
|
||||
setupTarget(harness);
|
||||
});
|
||||
|
||||
it('does not wait for file changes when false', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
watch: false,
|
||||
});
|
||||
|
||||
await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
timeout(BUILD_TIMEOUT),
|
||||
concatMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBe(true);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
await harness.modifyFile(
|
||||
'src/main.ts',
|
||||
(content) => content + 'console.log("abcd1234");',
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
fail('Expected files to not be watched.');
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
)
|
||||
.toPromise()
|
||||
.catch((error) => {
|
||||
// Timeout is expected if watching is disabled
|
||||
if (error instanceof TimeoutError) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
|
||||
it('watches for file changes when not present', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
watch: undefined,
|
||||
});
|
||||
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
timeout(BUILD_TIMEOUT),
|
||||
concatMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBe(true);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
await harness.modifyFile(
|
||||
'src/main.ts',
|
||||
(content) => content + 'console.log("abcd1234");',
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
|
||||
it('watches for file changes when true', async () => {
|
||||
harness.useTarget('serve', {
|
||||
...BASE_OPTIONS,
|
||||
watch: true,
|
||||
});
|
||||
|
||||
const buildCount = await harness
|
||||
.execute()
|
||||
.pipe(
|
||||
timeout(BUILD_TIMEOUT),
|
||||
concatMap(async ({ result }, index) => {
|
||||
expect(result?.success).toBe(true);
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
await harness.modifyFile(
|
||||
'src/main.ts',
|
||||
(content) => content + 'console.log("abcd1234");',
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
}
|
||||
}),
|
||||
take(2),
|
||||
count(),
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
expect(buildCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
104
packages/angular/build/src/builders/dev-server/tests/setup.ts
Normal file
104
packages/angular/build/src/builders/dev-server/tests/setup.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { json } from '@angular-devkit/core';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { BuilderHarness } from '../../../../../../../modules/testing/builder/src';
|
||||
import { ApplicationBuilderOptions as AppilicationSchema, buildApplication } from '@angular/build';
|
||||
import { Schema } from '../schema';
|
||||
|
||||
// TODO: Consider using package.json imports field instead of relative path
|
||||
// after the switch to rules_js.
|
||||
export * from '../../../../../../../modules/testing/builder/src';
|
||||
|
||||
// TODO: Remove and use import after Vite-based dev server is moved to new package
|
||||
export const APPLICATION_BUILDER_INFO = Object.freeze({
|
||||
name: '@angular-devkit/build-angular:application',
|
||||
schemaPath: path.join(
|
||||
path.dirname(require.resolve('@angular/build/package.json')),
|
||||
'src/builders/application/schema.json',
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Contains all required application builder fields.
|
||||
* Also disables progress reporting to minimize logging output.
|
||||
*/
|
||||
export const APPLICATION_BASE_OPTIONS = Object.freeze<AppilicationSchema>({
|
||||
index: 'src/index.html',
|
||||
browser: 'src/main.ts',
|
||||
outputPath: 'dist',
|
||||
tsConfig: 'src/tsconfig.app.json',
|
||||
progress: false,
|
||||
|
||||
// Disable optimizations
|
||||
optimization: false,
|
||||
|
||||
// Enable polling (if a test enables watch mode).
|
||||
// This is a workaround for bazel isolation file watch not triggering in tests.
|
||||
poll: 100,
|
||||
});
|
||||
|
||||
export const DEV_SERVER_BUILDER_INFO = Object.freeze({
|
||||
name: '@angular-devkit/build-angular:dev-server',
|
||||
schemaPath: __dirname + '/../schema.json',
|
||||
});
|
||||
|
||||
/**
|
||||
* Contains all required dev-server builder fields.
|
||||
* The port is also set to zero to ensure a free port is used for each test which
|
||||
* supports parallel test execution.
|
||||
*/
|
||||
export const BASE_OPTIONS = Object.freeze<Schema>({
|
||||
buildTarget: 'test:build',
|
||||
port: 0,
|
||||
|
||||
// Watch is not supported for testing in vite as currently there is no teardown logic to stop the watchers.
|
||||
watch: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Maximum time for single build/rebuild
|
||||
* This accounts for CI variability.
|
||||
*/
|
||||
export const BUILD_TIMEOUT = 25_000;
|
||||
|
||||
/**
|
||||
* Cached application builder option schema
|
||||
*/
|
||||
let applicationSchema: json.schema.JsonSchema | undefined;
|
||||
|
||||
/**
|
||||
* Adds a `build` target to a builder test harness for the application builder with the base options
|
||||
* used by the application builder tests.
|
||||
*
|
||||
* @param harness The builder harness to use when setting up the application builder target
|
||||
* @param extraOptions The additional options that should be used when executing the target.
|
||||
*/
|
||||
export function setupApplicationTarget<T>(
|
||||
harness: BuilderHarness<T>,
|
||||
extraOptions?: Partial<AppilicationSchema>,
|
||||
): void {
|
||||
applicationSchema ??= JSON.parse(
|
||||
readFileSync(APPLICATION_BUILDER_INFO.schemaPath, 'utf8'),
|
||||
) as json.schema.JsonSchema;
|
||||
|
||||
harness.withBuilderTarget(
|
||||
'build',
|
||||
buildApplication,
|
||||
{
|
||||
...APPLICATION_BASE_OPTIONS,
|
||||
...extraOptions,
|
||||
},
|
||||
{
|
||||
builderName: APPLICATION_BUILDER_INFO.name,
|
||||
optionSchema: applicationSchema,
|
||||
},
|
||||
);
|
||||
}
|
@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import type { BuilderContext } from '@angular-devkit/architect';
|
||||
import type { json } from '@angular-devkit/core';
|
||||
import type { Plugin } from 'esbuild';
|
||||
import assert from 'node:assert';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
@ -17,15 +16,13 @@ import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugi
|
||||
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
|
||||
import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
|
||||
import { loadEsmModule } from '../../utils/load-esm';
|
||||
import { buildEsbuildBrowser } from '../browser-esbuild';
|
||||
import { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema';
|
||||
import { ApplicationBuilderOutput } from '../application';
|
||||
import {
|
||||
type ApplicationBuilderInternalOptions,
|
||||
type BuildOutputFile,
|
||||
BuildOutputFileType,
|
||||
type ExternalResultMetadata,
|
||||
JavaScriptTransformer,
|
||||
buildApplicationInternal,
|
||||
createRxjsEsmResolutionPlugin,
|
||||
getFeatureSupport,
|
||||
getSupportedBrowsers,
|
||||
@ -43,6 +40,12 @@ interface OutputFileRecord {
|
||||
servable: boolean;
|
||||
}
|
||||
|
||||
export type BuilderAction = (
|
||||
options: ApplicationBuilderInternalOptions,
|
||||
context: BuilderContext,
|
||||
plugins?: Plugin[],
|
||||
) => AsyncIterable<ApplicationBuilderOutput>;
|
||||
|
||||
/**
|
||||
* Build options that are also present on the dev server but are only passed
|
||||
* to the build.
|
||||
@ -53,6 +56,7 @@ const CONVENIENCE_BUILD_OPTIONS = ['watch', 'poll', 'verbose'] as const;
|
||||
export async function* serveWithVite(
|
||||
serverOptions: NormalizedDevServerOptions,
|
||||
builderName: string,
|
||||
builderAction: BuilderAction,
|
||||
context: BuilderContext,
|
||||
transformers?: {
|
||||
indexHtml?: (content: string) => Promise<string>;
|
||||
@ -76,10 +80,11 @@ export async function* serveWithVite(
|
||||
}
|
||||
}
|
||||
|
||||
const browserOptions = await context.validateOptions<json.JsonObject & BrowserBuilderOptions>(
|
||||
// TODO: Adjust architect to not force a JsonObject derived return type
|
||||
const browserOptions = (await context.validateOptions(
|
||||
rawBrowserOptions,
|
||||
builderName,
|
||||
);
|
||||
)) as unknown as ApplicationBuilderInternalOptions;
|
||||
|
||||
if (browserOptions.prerender || browserOptions.ssr) {
|
||||
// Disable prerendering if enabled and force SSR.
|
||||
@ -94,7 +99,7 @@ export async function* serveWithVite(
|
||||
}
|
||||
|
||||
// Set all packages as external to support Vite's prebundle caching
|
||||
browserOptions.externalPackages = serverOptions.prebundle as json.JsonValue;
|
||||
browserOptions.externalPackages = serverOptions.prebundle;
|
||||
|
||||
const baseHref = browserOptions.baseHref;
|
||||
if (serverOptions.servePath === undefined && baseHref !== undefined) {
|
||||
@ -161,25 +166,8 @@ export async function* serveWithVite(
|
||||
deferred?.();
|
||||
});
|
||||
|
||||
const build =
|
||||
builderName === '@angular-devkit/build-angular:browser-esbuild'
|
||||
? buildEsbuildBrowser.bind(
|
||||
undefined,
|
||||
browserOptions,
|
||||
context,
|
||||
{ write: false },
|
||||
extensions?.buildPlugins,
|
||||
)
|
||||
: buildApplicationInternal.bind(
|
||||
undefined,
|
||||
browserOptions as ApplicationBuilderInternalOptions,
|
||||
context,
|
||||
{ write: false },
|
||||
{ codePlugins: extensions?.buildPlugins },
|
||||
);
|
||||
|
||||
// TODO: Switch this to an architect schedule call when infrastructure settings are supported
|
||||
for await (const result of build()) {
|
||||
for await (const result of builderAction(browserOptions, context, extensions?.buildPlugins)) {
|
||||
assert(result.outputFiles, 'Builder did not provide result files.');
|
||||
|
||||
// If build failed, nothing to serve
|
@ -13,3 +13,9 @@ export {
|
||||
} from './builders/application';
|
||||
export { type BuildOutputFile, BuildOutputFileType } from './tools/esbuild/bundler-context';
|
||||
export type { BuildOutputAsset } from './tools/esbuild/bundler-execution-result';
|
||||
|
||||
export {
|
||||
executeDevServerBuilder,
|
||||
DevServerBuilderOptions,
|
||||
DevServerBuilderOutput,
|
||||
} from './builders/dev-server';
|
||||
|
@ -16,6 +16,7 @@
|
||||
// Builders
|
||||
export { buildApplicationInternal } from './builders/application';
|
||||
export { ApplicationBuilderInternalOptions } from './builders/application/options';
|
||||
export { serveWithVite } from './builders/dev-server/vite-server';
|
||||
|
||||
// Tools
|
||||
export * from './tools/babel/plugins';
|
||||
@ -26,6 +27,7 @@ export { SassWorkerImplementation } from './tools/sass/sass-service';
|
||||
|
||||
// Utilities
|
||||
export * from './utils/bundle-calculator';
|
||||
export { checkPort } from './utils/check-port';
|
||||
export { deleteOutputDir } from './utils/delete-output-dir';
|
||||
export { I18nOptions, createI18nOptions, loadTranslations } from './utils/i18n-options';
|
||||
export {
|
||||
@ -40,15 +42,10 @@ export {
|
||||
InlineCriticalCssProcessor,
|
||||
type InlineCriticalCssProcessorOptions,
|
||||
} from './utils/index-file/inline-critical-css';
|
||||
export { loadProxyConfiguration } from './utils/load-proxy-config';
|
||||
export { type TranslationLoader, createTranslationLoader } from './utils/load-translations';
|
||||
export { purgeStaleBuildCache } from './utils/purge-cache';
|
||||
export { augmentAppWithServiceWorker } from './utils/service-worker';
|
||||
export { BundleStats, generateBuildStatsTable } from './utils/stats-table';
|
||||
export { getSupportedBrowsers } from './utils/supported-browsers';
|
||||
export { assertCompatibleAngularVersion } from './utils/version';
|
||||
|
||||
// Required for Vite-based dev server only
|
||||
export { createRxjsEsmResolutionPlugin } from './tools/esbuild/rxjs-esm-resolution-plugin';
|
||||
export { JavaScriptTransformer } from './tools/esbuild/javascript-transformer';
|
||||
export { getFeatureSupport, isZonelessApp } from './tools/esbuild/utils';
|
||||
export { renderPage } from './utils/server-rendering/render-page';
|
||||
|
@ -7,13 +7,13 @@
|
||||
*/
|
||||
|
||||
import remapping, { SourceMapInput } from '@ampproject/remapping';
|
||||
import { renderPage } from '@angular/build/private';
|
||||
import { lookup as lookupMimeType } from 'mrmime';
|
||||
import assert from 'node:assert';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { ServerResponse } from 'node:http';
|
||||
import { dirname, extname, join, relative } from 'node:path';
|
||||
import type { Connect, Plugin } from 'vite';
|
||||
import { renderPage } from '../../utils/server-rendering/render-page';
|
||||
|
||||
export interface AngularMemoryPluginOptions {
|
||||
workspaceRoot: string;
|
@ -9,3 +9,4 @@
|
||||
export * from './normalize-asset-patterns';
|
||||
export * from './normalize-optimization';
|
||||
export * from './normalize-source-maps';
|
||||
export * from './load-proxy-config';
|
||||
|
@ -6,15 +6,14 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import type { DevServerBuilderOutput } from '@angular/build';
|
||||
import { type IndexHtmlTransform, checkPort, purgeStaleBuildCache } from '@angular/build/private';
|
||||
import type { BuilderContext } from '@angular-devkit/architect';
|
||||
import type { Plugin } from 'esbuild';
|
||||
import type http from 'node:http';
|
||||
import { EMPTY, Observable, defer, switchMap } from 'rxjs';
|
||||
import type { ExecutionTransformer } from '../../transforms';
|
||||
import { checkPort } from '../../utils/check-port';
|
||||
import { type IndexHtmlTransform, purgeStaleBuildCache } from './internal';
|
||||
import { normalizeOptions } from './options';
|
||||
import type { DevServerBuilderOutput } from './output';
|
||||
import type { Schema as DevServerBuilderOptions } from './schema';
|
||||
|
||||
/**
|
||||
@ -93,9 +92,23 @@ export function execute(
|
||||
);
|
||||
}
|
||||
|
||||
return defer(() => import('./vite-server')).pipe(
|
||||
switchMap(({ serveWithVite }) =>
|
||||
serveWithVite(normalizedOptions, builderName, context, transforms, extensions),
|
||||
return defer(() =>
|
||||
Promise.all([import('@angular/build/private'), import('../browser-esbuild')]),
|
||||
).pipe(
|
||||
switchMap(([{ serveWithVite, buildApplicationInternal }, { buildEsbuildBrowser }]) =>
|
||||
serveWithVite(
|
||||
normalizedOptions,
|
||||
builderName,
|
||||
(options, context, codePlugins) => {
|
||||
return builderName === '@angular-devkit/build-angular:browser-esbuild'
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
buildEsbuildBrowser(options as any, context, { write: false }, codePlugins)
|
||||
: buildApplicationInternal(options, context, { write: false }, { codePlugins });
|
||||
},
|
||||
context,
|
||||
transforms,
|
||||
extensions,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -178,9 +191,11 @@ case.
|
||||
function isEsbuildBased(
|
||||
builderName: string,
|
||||
): builderName is
|
||||
| '@angular/build:application'
|
||||
| '@angular-devkit/build-angular:application'
|
||||
| '@angular-devkit/build-angular:browser-esbuild' {
|
||||
if (
|
||||
builderName === '@angular/build:application' ||
|
||||
builderName === '@angular-devkit/build-angular:application' ||
|
||||
builderName === '@angular-devkit/build-angular:browser-esbuild'
|
||||
) {
|
||||
|
@ -6,9 +6,9 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import { DevServerBuilderOutput } from '@angular/build';
|
||||
import { createBuilder } from '@angular-devkit/architect';
|
||||
import { execute } from './builder';
|
||||
import { DevServerBuilderOutput } from './output';
|
||||
import { Schema as DevServerBuilderOptions } from './schema';
|
||||
|
||||
export { DevServerBuilderOptions, DevServerBuilderOutput, execute as executeDevServerBuilder };
|
||||
|
@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
export { BuildOutputFile, BuildOutputFileType } from '@angular/build';
|
||||
export {
|
||||
type ApplicationBuilderInternalOptions,
|
||||
type ExternalResultMetadata,
|
||||
JavaScriptTransformer,
|
||||
buildApplicationInternal,
|
||||
createRxjsEsmResolutionPlugin,
|
||||
getFeatureSupport,
|
||||
getSupportedBrowsers,
|
||||
isZonelessApp,
|
||||
transformSupportedBrowsersToTargets,
|
||||
type IndexHtmlTransform,
|
||||
purgeStaleBuildCache,
|
||||
} from '@angular/build/private';
|
@ -6,6 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import { loadProxyConfiguration } from '@angular/build/private';
|
||||
import {
|
||||
BuilderContext,
|
||||
BuilderOutput,
|
||||
@ -41,7 +42,6 @@ import {
|
||||
zip,
|
||||
} from 'rxjs';
|
||||
import * as url from 'url';
|
||||
import { loadProxyConfiguration } from '../../utils';
|
||||
import { Schema } from './schema';
|
||||
|
||||
import { getAvailablePort, spawnAsObservable, waitUntilServerIsListening } from './utils';
|
||||
|
@ -7,9 +7,8 @@
|
||||
*/
|
||||
|
||||
export * from './default-progress';
|
||||
export { deleteOutputDir } from '@angular/build/private';
|
||||
export { deleteOutputDir, loadProxyConfiguration } from '@angular/build/private';
|
||||
export * from './run-module-as-observable-fork';
|
||||
export * from './load-proxy-config';
|
||||
export * from './normalize-file-replacements';
|
||||
export * from './normalize-asset-patterns';
|
||||
export * from './normalize-source-maps';
|
||||
|
17
yarn.lock
17
yarn.lock
@ -130,8 +130,7 @@
|
||||
tslib "^2.3.0"
|
||||
|
||||
"@angular/bazel@https://github.com/angular/bazel-builds.git#d4cd1ac66633528be9ad967b06a00d326446729e":
|
||||
version "18.0.0-next.5+sha-e1eae84"
|
||||
uid d4cd1ac66633528be9ad967b06a00d326446729e
|
||||
version "18.0.0-next.5"
|
||||
resolved "https://github.com/angular/bazel-builds.git#d4cd1ac66633528be9ad967b06a00d326446729e"
|
||||
dependencies:
|
||||
"@microsoft/api-extractor" "^7.24.2"
|
||||
@ -148,7 +147,6 @@
|
||||
|
||||
"@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#ba23169e282840cfac66488ff05c4258e20d4abc":
|
||||
version "0.0.0-10f65a101457091d4bc3500963d3a9630d55f7bd"
|
||||
uid ba23169e282840cfac66488ff05c4258e20d4abc
|
||||
resolved "https://github.com/angular/dev-infra-private-build-tooling-builds.git#ba23169e282840cfac66488ff05c4258e20d4abc"
|
||||
dependencies:
|
||||
"@angular-devkit/build-angular" "17.3.3"
|
||||
@ -316,7 +314,6 @@
|
||||
|
||||
"@angular/ng-dev@https://github.com/angular/dev-infra-private-ng-dev-builds.git#5f1f27c5ad034501ba7de855c0e2579bd131d43f":
|
||||
version "0.0.0-10f65a101457091d4bc3500963d3a9630d55f7bd"
|
||||
uid "5f1f27c5ad034501ba7de855c0e2579bd131d43f"
|
||||
resolved "https://github.com/angular/dev-infra-private-ng-dev-builds.git#5f1f27c5ad034501ba7de855c0e2579bd131d43f"
|
||||
dependencies:
|
||||
"@yarnpkg/lockfile" "^1.1.0"
|
||||
@ -12300,7 +12297,6 @@ sass@1.75.0, sass@^1.69.5:
|
||||
|
||||
"sauce-connect-proxy@https://saucelabs.com/downloads/sc-4.9.1-linux.tar.gz":
|
||||
version "0.0.0"
|
||||
uid "9310bc860f7870a1f872b11c4dc6073a1ad34e5e"
|
||||
resolved "https://saucelabs.com/downloads/sc-4.9.1-linux.tar.gz#9310bc860f7870a1f872b11c4dc6073a1ad34e5e"
|
||||
|
||||
saucelabs@^1.5.0:
|
||||
@ -13871,6 +13867,17 @@ vite@5.1.5:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vite@5.2.10:
|
||||
version "5.2.10"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.10.tgz#2ac927c91e99d51b376a5c73c0e4b059705f5bd7"
|
||||
integrity sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==
|
||||
dependencies:
|
||||
esbuild "^0.20.1"
|
||||
postcss "^8.4.38"
|
||||
rollup "^4.13.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vite@5.2.9:
|
||||
version "5.2.9"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.9.tgz#cd9a356c6ff5f7456c09c5ce74068ffa8df743d9"
|
||||
|
Loading…
x
Reference in New Issue
Block a user