Skip to content

Commit

Permalink
Merge pull request #1113 from angular/main
Browse files Browse the repository at this point in the history
Create a new pull request by comparing changes across two branches
  • Loading branch information
GulajavaMinistudio authored Aug 12, 2024
2 parents c71688b + ce11a39 commit 8885ecc
Show file tree
Hide file tree
Showing 30 changed files with 1,793 additions and 316 deletions.
6 changes: 6 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,9 @@ register_toolchains(
load("@npm//@angular/build-tooling/bazel/browsers:browser_repositories.bzl", "browser_repositories")

browser_repositories()

load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories")

esbuild_repositories(
npm_repository = "npm",
)
4 changes: 3 additions & 1 deletion goldens/circular-deps/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@
[
"packages/angular/cli/src/analytics/analytics.ts",
"packages/angular/cli/src/command-builder/command-module.ts"
]
],
["packages/angular/ssr/src/app.ts", "packages/angular/ssr/src/manifest.ts"],
["packages/angular/ssr/src/app.ts", "packages/angular/ssr/src/render.ts"]
]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"@ampproject/remapping": "2.3.0",
"@angular/animations": "18.2.0-rc.0",
"@angular/bazel": "patch:@angular/bazel@https%3A//github.com/angular/bazel-builds.git%23commit=71bd2e043e076365effdb6076f33b2d8d6bd6d02#~/.yarn/patches/@angular-bazel-https-9848736cf4.patch",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#8128c8cc982b49ca12490da8d97692143aefd026",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#77f89d89f65ef2d3f2b33991eff98839c2f9d704",
"@angular/cdk": "18.1.3",
"@angular/common": "18.2.0-rc.0",
"@angular/compiler": "18.2.0-rc.0",
Expand Down
16 changes: 11 additions & 5 deletions packages/angular/ssr/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load("//tools:defaults.bzl", "ng_package", "ts_library")
load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "ssr",
package_name = "@angular/ssr",
srcs = glob([
"*.ts",
"src/**/*.ts",
]),
srcs = glob(
include = [
"*.ts",
"src/**/*.ts",
],
exclude = [
"**/*_spec.ts",
],
),
module_name = "@angular/ssr",
deps = [
"@npm//@angular/core",
"@npm//@angular/platform-server",
"@npm//@types/node",
"@npm//critters",
"@npm//mrmime",
],
)

Expand Down
8 changes: 8 additions & 0 deletions packages/angular/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@
},
"dependencies": {
"critters": "0.0.24",
"mrmime": "2.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^18.0.0 || ^18.2.0-next.0",
"@angular/core": "^18.0.0 || ^18.2.0-next.0"
},
"devDependencies": {
"@angular/compiler": "18.2.0-next.2",
"@angular/platform-browser": "18.2.0-next.2",
"@angular/platform-server": "18.2.0-next.2",
"@angular/router": "18.2.0-next.2",
"zone.js": "^0.14.0"
},
"schematics": "./schematics/collection.json",
"repository": {
"type": "git",
Expand Down
7 changes: 6 additions & 1 deletion packages/angular/ssr/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ export {
CommonEngine,
type CommonEngineRenderOptions,
type CommonEngineOptions,
} from './src/common-engine';
} from './src/common-engine/common-engine';

// TODO(alanagius): enable at a later stage
// export { AngularAppEngine } from './src/app-engine';
// export { AngularServerApp } from './src/app';
// export { REQUEST, REQUEST_CONTEXT, RESPONSE_INIT } from './src/tokens';
150 changes: 150 additions & 0 deletions packages/angular/ssr/src/app-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { lookup as lookupMimeType } from 'mrmime';
import { AngularServerApp } from './app';
import { Hooks } from './hooks';
import { getPotentialLocaleIdFromUrl } from './i18n';
import { getAngularAppEngineManifest } from './manifest';

/**
* Angular server application engine.
* Manages Angular server applications (including localized ones), handles rendering requests,
* and optionally transforms index HTML before rendering.
*/
export class AngularAppEngine {
/**
* Hooks for extending or modifying the behavior of the server application.
* @internal This property is accessed by the Angular CLI when running the dev-server.
*/
static hooks = new Hooks();

/**
* Hooks for extending or modifying the behavior of the server application.
* This instance can be used to attach custom functionality to various events in the server application lifecycle.
* @internal
*/
get hooks(): Hooks {
return AngularAppEngine.hooks;
}

/**
* Specifies if the application is operating in development mode.
* This property controls the activation of features intended for production, such as caching mechanisms.
* @internal
*/
static isDevMode = false;

/**
* The manifest for the server application.
*/
private readonly manifest = getAngularAppEngineManifest();

/**
* Map of locale strings to corresponding `AngularServerApp` instances.
* Each instance represents an Angular server application.
*/
private readonly appsCache = new Map<string, AngularServerApp>();

/**
* Renders an HTTP request using the appropriate Angular server application and returns a response.
*
* This method determines the entry point for the Angular server application based on the request URL,
* and caches the server application instances for reuse. If the application is in development mode,
* the cache is bypassed and a new instance is created for each request.
*
* If the request URL appears to be for a file (excluding `/index.html`), the method returns `null`.
* A request to `https://www.example.com/page/index.html` will render the Angular route
* corresponding to `https://www.example.com/page`.
*
* @param request - The incoming HTTP request object to be rendered.
* @param requestContext - Optional additional context for the request, such as metadata.
* @returns A promise that resolves to a Response object, or `null` if the request URL represents a file (e.g., `./logo.png`)
* rather than an application route.
*/
async render(request: Request, requestContext?: unknown): Promise<Response | null> {
// Skip if the request looks like a file but not `/index.html`.
const url = new URL(request.url);
const { pathname } = url;
if (isFileLike(pathname) && !pathname.endsWith('/index.html')) {
return null;
}

const entryPoint = this.getEntryPointFromUrl(url);
if (!entryPoint) {
return null;
}

const [locale, loadModule] = entryPoint;
let serverApp = this.appsCache.get(locale);
if (!serverApp) {
const { AngularServerApp } = await loadModule();
serverApp = new AngularServerApp({
isDevMode: AngularAppEngine.isDevMode,
hooks: this.hooks,
});

if (!AngularAppEngine.isDevMode) {
this.appsCache.set(locale, serverApp);
}
}

return serverApp.render(request, requestContext);
}

/**
* Retrieves the entry point path and locale for the Angular server application based on the provided URL.
*
* This method determines the appropriate entry point and locale for rendering the application by examining the URL.
* If there is only one entry point available, it is returned regardless of the URL.
* Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
*
* @param url - The URL used to derive the locale and determine the entry point.
* @returns An array containing:
* - The first element is the locale extracted from the URL.
* - The second element is a function that returns a promise resolving to an object with the `AngularServerApp` type.
*
* Returns `null` if no matching entry point is found for the extracted locale.
*/
private getEntryPointFromUrl(url: URL):
| [
locale: string,
loadModule: () => Promise<{
AngularServerApp: typeof AngularServerApp;
}>,
]
| null {
// Find bundle for locale
const { entryPoints, basePath } = this.manifest;
if (entryPoints.size === 1) {
return entryPoints.entries().next().value;
}

const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
const entryPoint = entryPoints.get(potentialLocale);

return entryPoint ? [potentialLocale, entryPoint] : null;
}
}

/**
* Determines if the given pathname corresponds to a file-like resource.
*
* @param pathname - The pathname to check.
* @returns True if the pathname appears to be a file, false otherwise.
*/
function isFileLike(pathname: string): boolean {
const dotIndex = pathname.lastIndexOf('.');
if (dotIndex === -1) {
return false;
}

const extension = pathname.slice(dotIndex);

return extension === '.ico' || !!lookupMimeType(extension);
}
104 changes: 104 additions & 0 deletions packages/angular/ssr/src/app.ts
Original file line number Diff line number Diff line change
@@ -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.dev/license
*/

import { Hooks } from './hooks';
import { getAngularAppManifest } from './manifest';
import { ServerRenderContext, render } from './render';

/**
* Configuration options for initializing a `AngularServerApp` instance.
*/
export interface AngularServerAppOptions {
/**
* Indicates whether the application is in development mode.
*
* When set to `true`, the application runs in development mode with additional debugging features.
*/
isDevMode?: boolean;

/**
* Optional hooks for customizing the server application's behavior.
*/
hooks?: Hooks;
}

/**
* Represents a locale-specific Angular server application managed by the server application engine.
*
* The `AngularServerApp` class handles server-side rendering and asset management for a specific locale.
*/
export class AngularServerApp {
/**
* The manifest associated with this server application.
* @internal
*/
readonly manifest = getAngularAppManifest();

/**
* Hooks for extending or modifying the behavior of the server application.
* This instance can be used to attach custom functionality to various events in the server application lifecycle.
* @internal
*/
readonly hooks: Hooks;

/**
* Specifies if the server application is operating in development mode.
* This property controls the activation of features intended for production, such as caching mechanisms.
* @internal
*/
readonly isDevMode: boolean;

/**
* Creates a new `AngularServerApp` instance with the provided configuration options.
*
* @param options - The configuration options for the server application.
* - `isDevMode`: Flag indicating if the application is in development mode.
* - `hooks`: Optional hooks for customizing application behavior.
*/
constructor(options: AngularServerAppOptions) {
this.isDevMode = options.isDevMode ?? false;
this.hooks = options.hooks ?? new Hooks();
}

/**
* Renders a response for the given HTTP request using the server application.
*
* This method processes the request and returns a response based on the specified rendering context.
*
* @param request - The incoming HTTP request to be rendered.
* @param requestContext - Optional additional context for rendering, such as request metadata.
* @param serverContext - The rendering context.
*
* @returns A promise that resolves to the HTTP response object resulting from the rendering.
*/
render(
request: Request,
requestContext?: unknown,
serverContext: ServerRenderContext = ServerRenderContext.SSR,
): Promise<Response> {
return render(this, request, serverContext, requestContext);
}

/**
* Retrieves the content of a server-side asset using its path.
*
* This method fetches the content of a specific asset defined in the server application's manifest.
*
* @param path - The path to the server asset.
* @returns A promise that resolves to the asset content as a string.
* @throws Error If the asset path is not found in the manifest, an error is thrown.
*/
async getServerAsset(path: string): Promise<string> {
const asset = this.manifest.assets[path];
if (!asset) {
throw new Error(`Server asset '${path}' does not exist.`);
}

return asset();
}
}
File renamed without changes.
37 changes: 37 additions & 0 deletions packages/angular/ssr/src/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { ɵConsole } from '@angular/core';

/**
* Custom implementation of the Angular Console service that filters out specific log messages.
*
* This class extends the internal Angular `ɵConsole` class to provide customized logging behavior.
* It overrides the `log` method to suppress logs that match certain predefined messages.
*/
export class Console extends ɵConsole {
/**
* A set of log messages that should be ignored and not printed to the console.
*/
private readonly ignoredLogs = new Set(['Angular is running in development mode.']);

/**
* Logs a message to the console if it is not in the set of ignored messages.
*
* @param message - The message to log to the console.
*
* This method overrides the `log` method of the `ɵConsole` class. It checks if the
* message is in the `ignoredLogs` set. If it is not, it delegates the logging to
* the parent class's `log` method. Otherwise, the message is suppressed.
*/
override log(message: string): void {
if (!this.ignoredLogs.has(message)) {
super.log(message);
}
}
}
Loading

0 comments on commit 8885ecc

Please sign in to comment.