@angular/ssr 19.0.0-next.0 → 19.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- export { CommonEngine, } from './src/common-engine/common-engine';
9
- export { getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig } from './src/routes/ng-routes';
@@ -1,106 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- import { Hooks } from './hooks';
9
- import { getPotentialLocaleIdFromUrl } from './i18n';
10
- import { getAngularAppEngineManifest } from './manifest';
11
- /**
12
- * Angular server application engine.
13
- * Manages Angular server applications (including localized ones), handles rendering requests,
14
- * and optionally transforms index HTML before rendering.
15
- */
16
- export class AngularAppEngine {
17
- /**
18
- * Hooks for extending or modifying the behavior of the server application.
19
- * @internal This property is accessed by the Angular CLI when running the dev-server.
20
- */
21
- static hooks = new Hooks();
22
- /**
23
- * Hooks for extending or modifying the behavior of the server application.
24
- * This instance can be used to attach custom functionality to various events in the server application lifecycle.
25
- * @internal
26
- */
27
- get hooks() {
28
- return AngularAppEngine.hooks;
29
- }
30
- /**
31
- * Specifies if the application is operating in development mode.
32
- * This property controls the activation of features intended for production, such as caching mechanisms.
33
- * @internal
34
- */
35
- static isDevMode = false;
36
- /**
37
- * The manifest for the server application.
38
- */
39
- manifest = getAngularAppEngineManifest();
40
- /**
41
- * Map of locale strings to corresponding `AngularServerApp` instances.
42
- * Each instance represents an Angular server application.
43
- */
44
- appsCache = new Map();
45
- /**
46
- * Renders an HTTP request using the appropriate Angular server application and returns a response.
47
- *
48
- * This method determines the entry point for the Angular server application based on the request URL,
49
- * and caches the server application instances for reuse. If the application is in development mode,
50
- * the cache is bypassed and a new instance is created for each request.
51
- *
52
- * If the request URL appears to be for a file (excluding `/index.html`), the method returns `null`.
53
- * A request to `https://www.example.com/page/index.html` will render the Angular route
54
- * corresponding to `https://www.example.com/page`.
55
- *
56
- * @param request - The incoming HTTP request object to be rendered.
57
- * @param requestContext - Optional additional context for the request, such as metadata.
58
- * @returns A promise that resolves to a Response object, or `null` if the request URL represents a file (e.g., `./logo.png`)
59
- * rather than an application route.
60
- */
61
- async render(request, requestContext) {
62
- // Skip if the request looks like a file but not `/index.html`.
63
- const url = new URL(request.url);
64
- const entryPoint = this.getEntryPointFromUrl(url);
65
- if (!entryPoint) {
66
- return null;
67
- }
68
- const [locale, loadModule] = entryPoint;
69
- let serverApp = this.appsCache.get(locale);
70
- if (!serverApp) {
71
- const { AngularServerApp } = await loadModule();
72
- serverApp = new AngularServerApp({
73
- isDevMode: AngularAppEngine.isDevMode,
74
- hooks: this.hooks,
75
- });
76
- if (!AngularAppEngine.isDevMode) {
77
- this.appsCache.set(locale, serverApp);
78
- }
79
- }
80
- return serverApp.render(request, requestContext);
81
- }
82
- /**
83
- * Retrieves the entry point path and locale for the Angular server application based on the provided URL.
84
- *
85
- * This method determines the appropriate entry point and locale for rendering the application by examining the URL.
86
- * If there is only one entry point available, it is returned regardless of the URL.
87
- * Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
88
- *
89
- * @param url - The URL used to derive the locale and determine the entry point.
90
- * @returns An array containing:
91
- * - The first element is the locale extracted from the URL.
92
- * - The second element is a function that returns a promise resolving to an object with the `AngularServerApp` type.
93
- *
94
- * Returns `null` if no matching entry point is found for the extracted locale.
95
- */
96
- getEntryPointFromUrl(url) {
97
- // Find bundle for locale
98
- const { entryPoints, basePath } = this.manifest;
99
- if (entryPoints.size === 1) {
100
- return entryPoints.entries().next().value;
101
- }
102
- const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
103
- const entryPoint = entryPoints.get(potentialLocale);
104
- return entryPoint ? [potentialLocale, entryPoint] : null;
105
- }
106
- }
@@ -1,85 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- import { ServerAssets } from './assets';
9
- import { Hooks } from './hooks';
10
- import { getAngularAppManifest } from './manifest';
11
- import { ServerRenderContext, render } from './render';
12
- import { ServerRouter } from './routes/router';
13
- /**
14
- * Represents a locale-specific Angular server application managed by the server application engine.
15
- *
16
- * The `AngularServerApp` class handles server-side rendering and asset management for a specific locale.
17
- */
18
- export class AngularServerApp {
19
- options;
20
- /**
21
- * The manifest associated with this server application.
22
- * @internal
23
- */
24
- manifest = getAngularAppManifest();
25
- /**
26
- * Hooks for extending or modifying the behavior of the server application.
27
- * This instance can be used to attach custom functionality to various events in the server application lifecycle.
28
- * @internal
29
- */
30
- hooks;
31
- /**
32
- * Specifies if the server application is operating in development mode.
33
- * This property controls the activation of features intended for production, such as caching mechanisms.
34
- * @internal
35
- */
36
- isDevMode;
37
- /**
38
- * An instance of ServerAsset that handles server-side asset.
39
- * @internal
40
- */
41
- assets = new ServerAssets(this.manifest);
42
- /**
43
- * The router instance used for route matching and handling.
44
- */
45
- router;
46
- /**
47
- * Creates a new `AngularServerApp` instance with the provided configuration options.
48
- *
49
- * @param options - The configuration options for the server application.
50
- * - `isDevMode`: Flag indicating if the application is in development mode.
51
- * - `hooks`: Optional hooks for customizing application behavior.
52
- */
53
- constructor(options) {
54
- this.options = options;
55
- this.isDevMode = options.isDevMode ?? false;
56
- this.hooks = options.hooks ?? new Hooks();
57
- }
58
- /**
59
- * Renders a response for the given HTTP request using the server application.
60
- *
61
- * This method processes the request and returns a response based on the specified rendering context.
62
- *
63
- * @param request - The incoming HTTP request to be rendered.
64
- * @param requestContext - Optional additional context for rendering, such as request metadata.
65
- * @param serverContext - The rendering context.
66
- *
67
- * @returns A promise that resolves to the HTTP response object resulting from the rendering, or null if no match is found.
68
- */
69
- async render(request, requestContext, serverContext = ServerRenderContext.SSR) {
70
- const url = new URL(request.url);
71
- this.router ??= await ServerRouter.from(this.manifest, url);
72
- const matchedRoute = this.router.match(url);
73
- if (!matchedRoute) {
74
- // Not a known Angular route.
75
- return null;
76
- }
77
- const { redirectTo } = matchedRoute;
78
- if (redirectTo !== undefined) {
79
- // 302 Found is used by default for redirections
80
- // See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status
81
- return Response.redirect(new URL(redirectTo, url), 302);
82
- }
83
- return render(this, request, serverContext, requestContext);
84
- }
85
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- /**
9
- * Manages server-side assets.
10
- */
11
- export class ServerAssets {
12
- manifest;
13
- /**
14
- * Creates an instance of ServerAsset.
15
- *
16
- * @param manifest - The manifest containing the server assets.
17
- */
18
- constructor(manifest) {
19
- this.manifest = manifest;
20
- }
21
- /**
22
- * Retrieves the content of a server-side asset using its path.
23
- *
24
- * @param path - The path to the server asset.
25
- * @returns A promise that resolves to the asset content as a string.
26
- * @throws Error If the asset path is not found in the manifest, an error is thrown.
27
- */
28
- async getServerAsset(path) {
29
- const asset = this.manifest.assets[path];
30
- if (!asset) {
31
- throw new Error(`Server asset '${path}' does not exist.`);
32
- }
33
- return asset();
34
- }
35
- /**
36
- * Retrieves and caches the content of 'index.server.html'.
37
- *
38
- * @returns A promise that resolves to the content of 'index.server.html'.
39
- * @throws Error If there is an issue retrieving the asset.
40
- */
41
- getIndexServerHtml() {
42
- return this.getServerAsset('index.server.html');
43
- }
44
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- import Critters from 'critters';
9
- import { readFile } from 'node:fs/promises';
10
- /**
11
- * Pattern used to extract the media query set by Critters in an `onload` handler.
12
- */
13
- const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
14
- /**
15
- * Name of the attribute used to save the Critters media query so it can be re-assigned on load.
16
- */
17
- const CSP_MEDIA_ATTR = 'ngCspMedia';
18
- /**
19
- * Script text used to change the media value of the link tags.
20
- *
21
- * NOTE:
22
- * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
23
- * because this does not always fire on Chome.
24
- * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
25
- */
26
- const LINK_LOAD_SCRIPT_CONTENT = [
27
- '(() => {',
28
- ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`,
29
- ' const documentElement = document.documentElement;',
30
- ' const listener = (e) => {',
31
- ' const target = e.target;',
32
- ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`,
33
- ' return;',
34
- ' }',
35
- ' target.media = target.getAttribute(CSP_MEDIA_ATTR);',
36
- ' target.removeAttribute(CSP_MEDIA_ATTR);',
37
- // Remove onload listener when there are no longer styles that need to be loaded.
38
- ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {',
39
- ` documentElement.removeEventListener('load', listener);`,
40
- ' }',
41
- ' };',
42
- // We use an event with capturing (the true parameter) because load events don't bubble.
43
- ` documentElement.addEventListener('load', listener, true);`,
44
- '})();',
45
- ].join('\n');
46
- class CrittersExtended extends Critters {
47
- optionsExtended;
48
- resourceCache;
49
- warnings = [];
50
- errors = [];
51
- initialEmbedLinkedStylesheet;
52
- addedCspScriptsDocuments = new WeakSet();
53
- documentNonces = new WeakMap();
54
- constructor(optionsExtended, resourceCache) {
55
- super({
56
- logger: {
57
- warn: (s) => this.warnings.push(s),
58
- error: (s) => this.errors.push(s),
59
- info: () => { },
60
- },
61
- logLevel: 'warn',
62
- path: optionsExtended.outputPath,
63
- publicPath: optionsExtended.deployUrl,
64
- compress: !!optionsExtended.minify,
65
- pruneSource: false,
66
- reduceInlineStyles: false,
67
- mergeStylesheets: false,
68
- // Note: if `preload` changes to anything other than `media`, the logic in
69
- // `embedLinkedStylesheetOverride` will have to be updated.
70
- preload: 'media',
71
- noscriptFallback: true,
72
- inlineFonts: true,
73
- });
74
- this.optionsExtended = optionsExtended;
75
- this.resourceCache = resourceCache;
76
- // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in
77
- // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't
78
- // allow for `super` to be cast to a different type.
79
- this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet;
80
- this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride;
81
- }
82
- async readFile(path) {
83
- let resourceContent = this.resourceCache.get(path);
84
- if (resourceContent === undefined) {
85
- resourceContent = await readFile(path, 'utf-8');
86
- this.resourceCache.set(path, resourceContent);
87
- }
88
- return resourceContent;
89
- }
90
- /**
91
- * Override of the Critters `embedLinkedStylesheet` method
92
- * that makes it work with Angular's CSP APIs.
93
- */
94
- embedLinkedStylesheetOverride = async (link, document) => {
95
- if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
96
- // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
97
- // NB: this is only needed for the webpack based builders.
98
- const media = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
99
- if (media) {
100
- link.removeAttribute('onload');
101
- link.setAttribute('media', media[1]);
102
- link?.next?.remove();
103
- }
104
- }
105
- const returnValue = await this.initialEmbedLinkedStylesheet(link, document);
106
- const cspNonce = this.findCspNonce(document);
107
- if (cspNonce) {
108
- const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
109
- if (crittersMedia) {
110
- // If there's a Critters-generated `onload` handler and the file has an Angular CSP nonce,
111
- // we have to remove the handler, because it's incompatible with CSP. We save the value
112
- // in a different attribute and we generate a script tag with the nonce that uses
113
- // `addEventListener` to apply the media query instead.
114
- link.removeAttribute('onload');
115
- link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]);
116
- this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
117
- }
118
- // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't
119
- // a way of doing that at the moment so we fall back to doing it any time a `link` tag is
120
- // inserted. We mitigate it by only iterating the direct children of the `<head>` which
121
- // should be pretty shallow.
122
- document.head.children.forEach((child) => {
123
- if (child.tagName === 'style' && !child.hasAttribute('nonce')) {
124
- child.setAttribute('nonce', cspNonce);
125
- }
126
- });
127
- }
128
- return returnValue;
129
- };
130
- /**
131
- * Finds the CSP nonce for a specific document.
132
- */
133
- findCspNonce(document) {
134
- if (this.documentNonces.has(document)) {
135
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
136
- return this.documentNonces.get(document);
137
- }
138
- // HTML attribute are case-insensitive, but the parser used by Critters is case-sensitive.
139
- const nonceElement = document.querySelector('[ngCspNonce], [ngcspnonce]');
140
- const cspNonce = nonceElement?.getAttribute('ngCspNonce') || nonceElement?.getAttribute('ngcspnonce') || null;
141
- this.documentNonces.set(document, cspNonce);
142
- return cspNonce;
143
- }
144
- /**
145
- * Inserts the `script` tag that swaps the critical CSS at runtime,
146
- * if one hasn't been inserted into the document already.
147
- */
148
- conditionallyInsertCspLoadingScript(document, nonce, link) {
149
- if (this.addedCspScriptsDocuments.has(document)) {
150
- return;
151
- }
152
- if (document.head.textContent.includes(LINK_LOAD_SCRIPT_CONTENT)) {
153
- // Script was already added during the build.
154
- this.addedCspScriptsDocuments.add(document);
155
- return;
156
- }
157
- const script = document.createElement('script');
158
- script.setAttribute('nonce', nonce);
159
- script.textContent = LINK_LOAD_SCRIPT_CONTENT;
160
- // Prepend the script to the head since it needs to
161
- // run as early as possible, before the `link` tags.
162
- document.head.insertBefore(script, link);
163
- this.addedCspScriptsDocuments.add(document);
164
- }
165
- }
166
- export class InlineCriticalCssProcessor {
167
- options;
168
- resourceCache = new Map();
169
- constructor(options) {
170
- this.options = options;
171
- }
172
- async process(html, options) {
173
- const critters = new CrittersExtended({ ...this.options, ...options }, this.resourceCache);
174
- const content = await critters.process(html);
175
- return {
176
- content,
177
- errors: critters.errors.length ? critters.errors : undefined,
178
- warnings: critters.warnings.length ? critters.warnings : undefined,
179
- };
180
- }
181
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- const PERFORMANCE_MARK_PREFIX = '🅰️';
9
- export function printPerformanceLogs() {
10
- let maxWordLength = 0;
11
- const benchmarks = [];
12
- for (const { name, duration } of performance.getEntriesByType('measure')) {
13
- if (!name.startsWith(PERFORMANCE_MARK_PREFIX)) {
14
- continue;
15
- }
16
- // `🅰️:Retrieve SSG Page` -> `Retrieve SSG Page:`
17
- const step = name.slice(PERFORMANCE_MARK_PREFIX.length + 1) + ':';
18
- if (step.length > maxWordLength) {
19
- maxWordLength = step.length;
20
- }
21
- benchmarks.push([step, `${duration.toFixed(1)}ms`]);
22
- performance.clearMeasures(name);
23
- }
24
- /* eslint-disable no-console */
25
- console.log('********** Performance results **********');
26
- for (const [step, value] of benchmarks) {
27
- const spaces = maxWordLength - step.length + 5;
28
- console.log(step + ' '.repeat(spaces) + value);
29
- }
30
- console.log('*****************************************');
31
- /* eslint-enable no-console */
32
- }
33
- export async function runMethodAndMeasurePerf(label, asyncMethod) {
34
- const labelName = `${PERFORMANCE_MARK_PREFIX}:${label}`;
35
- const startLabel = `start:${labelName}`;
36
- const endLabel = `end:${labelName}`;
37
- try {
38
- performance.mark(startLabel);
39
- return await asyncMethod();
40
- }
41
- finally {
42
- performance.mark(endLabel);
43
- performance.measure(labelName, startLabel, endLabel);
44
- performance.clearMarks(startLabel);
45
- performance.clearMarks(endLabel);
46
- }
47
- }
48
- export function noopRunMethodAndMeasurePerf(label, asyncMethod) {
49
- return asyncMethod();
50
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- import { ɵConsole } from '@angular/core';
9
- /**
10
- * Custom implementation of the Angular Console service that filters out specific log messages.
11
- *
12
- * This class extends the internal Angular `ɵConsole` class to provide customized logging behavior.
13
- * It overrides the `log` method to suppress logs that match certain predefined messages.
14
- */
15
- export class Console extends ɵConsole {
16
- /**
17
- * A set of log messages that should be ignored and not printed to the console.
18
- */
19
- ignoredLogs = new Set(['Angular is running in development mode.']);
20
- /**
21
- * Logs a message to the console if it is not in the set of ignored messages.
22
- *
23
- * @param message - The message to log to the console.
24
- *
25
- * This method overrides the `log` method of the `ɵConsole` class. It checks if the
26
- * message is in the `ignoredLogs` set. If it is not, it delegates the logging to
27
- * the parent class's `log` method. Otherwise, the message is suppressed.
28
- */
29
- log(message) {
30
- if (!this.ignoredLogs.has(message)) {
31
- super.log(message);
32
- }
33
- }
34
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- /**
9
- * Manages a collection of hooks and provides methods to register and execute them.
10
- * Hooks are functions that can be invoked with specific arguments to allow modifications or enhancements.
11
- */
12
- export class Hooks {
13
- /**
14
- * A map of hook names to arrays of hook functions.
15
- * Each hook name can have multiple associated functions, which are executed in sequence.
16
- */
17
- store = new Map();
18
- /**
19
- * Executes all hooks associated with the specified name, passing the given argument to each hook function.
20
- * The hooks are invoked sequentially, and the argument may be modified by each hook.
21
- *
22
- * @template Hook - The type of the hook name. It should be one of the keys of `HooksMapping`.
23
- * @param name - The name of the hook whose functions will be executed.
24
- * @param context - The input value to be passed to each hook function. The value is mutated by each hook function.
25
- * @returns A promise that resolves once all hook functions have been executed.
26
- *
27
- * @example
28
- * ```typescript
29
- * const hooks = new Hooks();
30
- * hooks.on('html:transform:pre', async (ctx) => {
31
- * ctx.html = ctx.html.replace(/foo/g, 'bar');
32
- * return ctx.html;
33
- * });
34
- * const result = await hooks.run('html:transform:pre', { html: '<div>foo</div>' });
35
- * console.log(result); // '<div>bar</div>'
36
- * ```
37
- * @internal
38
- */
39
- async run(name, context) {
40
- const hooks = this.store.get(name);
41
- switch (name) {
42
- case 'html:transform:pre': {
43
- if (!hooks) {
44
- return context.html;
45
- }
46
- const ctx = { ...context };
47
- for (const hook of hooks) {
48
- ctx.html = await hook(ctx);
49
- }
50
- return ctx.html;
51
- }
52
- default:
53
- throw new Error(`Running hook "${name}" is not supported.`);
54
- }
55
- }
56
- /**
57
- * Registers a new hook function under the specified hook name.
58
- * This function should be a function that takes an argument of type `T` and returns a `string` or `Promise<string>`.
59
- *
60
- * @template Hook - The type of the hook name. It should be one of the keys of `HooksMapping`.
61
- * @param name - The name of the hook under which the function will be registered.
62
- * @param handler - A function to be executed when the hook is triggered. The handler will be called with an argument
63
- * that may be modified by the hook functions.
64
- *
65
- * @remarks
66
- * - If there are existing handlers registered under the given hook name, the new handler will be added to the list.
67
- * - If no handlers are registered under the given hook name, a new list will be created with the handler as its first element.
68
- *
69
- * @example
70
- * ```typescript
71
- * hooks.on('html:transform:pre', async (ctx) => {
72
- * return ctx.html.replace(/foo/g, 'bar');
73
- * });
74
- * ```
75
- */
76
- on(name, handler) {
77
- const hooks = this.store.get(name);
78
- if (hooks) {
79
- hooks.push(handler);
80
- }
81
- else {
82
- this.store.set(name, [handler]);
83
- }
84
- }
85
- /**
86
- * Checks if there are any hooks registered under the specified name.
87
- *
88
- * @param name - The name of the hook to check.
89
- * @returns `true` if there are hooks registered under the specified name, otherwise `false`.
90
- */
91
- has(name) {
92
- return !!this.store.get(name)?.length;
93
- }
94
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- /**
9
- * Extracts a potential locale ID from a given URL based on the specified base path.
10
- *
11
- * This function parses the URL to locate a potential locale identifier that immediately
12
- * follows the base path segment in the URL's pathname. If the URL does not contain a valid
13
- * locale ID, an empty string is returned.
14
- *
15
- * @param url - The full URL from which to extract the locale ID.
16
- * @param basePath - The base path used as the reference point for extracting the locale ID.
17
- * @returns The extracted locale ID if present, or an empty string if no valid locale ID is found.
18
- *
19
- * @example
20
- * ```js
21
- * const url = new URL('https://example.com/base/en/page');
22
- * const basePath = '/base';
23
- * const localeId = getPotentialLocaleIdFromUrl(url, basePath);
24
- * console.log(localeId); // Output: 'en'
25
- * ```
26
- */
27
- export function getPotentialLocaleIdFromUrl(url, basePath) {
28
- const { pathname } = url;
29
- // Move forward of the base path section.
30
- let start = basePath.length;
31
- if (pathname[start] === '/') {
32
- start++;
33
- }
34
- // Find the next forward slash.
35
- let end = pathname.indexOf('/', start);
36
- if (end === -1) {
37
- end = pathname.length;
38
- }
39
- // Extract the potential locale id.
40
- return pathname.slice(start, end);
41
- }