docs(docs-infra): pull version picker info from shared versions.json

fixes #64842
This commit is contained in:
Matthieu Riegler 2025-11-02 00:53:32 +01:00 committed by Andrew Scott
parent b2c79793c8
commit 05ab1dd76e
11 changed files with 368 additions and 330 deletions

View file

@ -37,6 +37,7 @@ TEST_FILES = APPLICATION_FILES + [
APPLICATION_ASSETS = [
"//adev/src/assets/images",
"//adev/src/assets/others",
"//adev/src/assets/previews",
"//adev/src/assets:tutorials",
"//adev/src/assets/icons",

View file

@ -11,7 +11,6 @@ import {AppComponent} from './app.component';
import {provideRouter, withComponentInputBinding} from '@angular/router';
import {routes} from './routes';
import {Search, WINDOW} from '@angular/docs';
import {CURRENT_MAJOR_VERSION} from './core/providers/current-version';
describe('AppComponent', () => {
const fakeSearch = {};
@ -30,10 +29,6 @@ describe('AppComponent', () => {
provide: Search,
useValue: fakeSearch,
},
{
provide: CURRENT_MAJOR_VERSION,
useValue: fakeCurrentMajorVersion,
},
],
});
const fixture = TestBed.createComponent(AppComponent);

View file

@ -45,7 +45,6 @@ import {CustomErrorHandler} from './core/services/errors-handling/error-handler'
import {ExampleContentLoader} from './core/services/example-content-loader.service';
import {ReuseTutorialsRouteStrategy} from './features/tutorial/tutorials-route-reuse-strategy';
import {routes} from './routes';
import {CURRENT_MAJOR_VERSION} from './core/providers/current-version';
import {AppScroller} from './app-scroller';
export const appConfig: ApplicationConfig = {
@ -78,10 +77,6 @@ export const appConfig: ApplicationConfig = {
provideEnvironmentInitializer(() => inject(AppScroller)),
provideEnvironmentInitializer(() => inject(AnalyticsService)),
provideAlgoliaSearchClient(environment),
{
provide: CURRENT_MAJOR_VERSION,
useValue: Number(VERSION.major),
},
{provide: ENVIRONMENT, useValue: environment},
{provide: ErrorHandler, useClass: CustomErrorHandler},
{provide: PREVIEWS_COMPONENTS, useValue: PREVIEWS_COMPONENTS_MAP},

View file

@ -1,77 +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.dev/license
*/
export const VERSIONS_CONFIG = {
aDevVersionsLinkPattern: 'https://{{prefix}}{{version}}angular.dev',
aioVersions: [
{
version: 'v18',
url: 'https://v18.angular.dev/overview',
},
{
version: 'v17',
url: 'https://v17.angular.io/docs',
},
{
version: 'v16',
url: 'https://v16.angular.io/docs',
},
{
version: 'v15',
url: 'https://v15.angular.io/docs',
},
{
version: 'v14',
url: 'https://v14.angular.io/docs',
},
{
version: 'v13',
url: 'https://v13.angular.io/docs',
},
{
version: 'v12',
url: 'https://v12.angular.io/docs',
},
{
version: 'v11',
url: 'https://v11.angular.io/docs',
},
{
version: 'v10',
url: 'https://v10.angular.io/docs',
},
{
version: 'v9',
url: 'https://v9.angular.io/docs',
},
{
version: 'v8',
url: 'https://v8.angular.io/docs',
},
{
version: 'v7',
url: 'https://v7.angular.io/docs',
},
{
version: 'v6',
url: 'https://v6.angular.io/docs',
},
{
version: 'v5',
url: 'https://v5.angular.io/docs',
},
{
version: 'v4',
url: 'https://v4.angular.io/docs',
},
{
version: 'v2',
url: 'https://v2.angular.io/docs',
},
],
};

View file

@ -62,9 +62,9 @@
<nav
class="adev-nav-primary docs-scroll-hide"
[class.adev-nav-primary--open]="isMobileNavigationOpened()"
[class.adev-nav-primary--rc]="currentDocsVersionMode === 'rc'"
[class.adev-nav-primary--next]="currentDocsVersionMode === 'next'"
[class.adev-nav-primary--deprecated]="currentDocsVersionMode === 'deprecated'"
[class.adev-nav-primary--rc]="currentDocsVersionMode() === 'rc'"
[class.adev-nav-primary--next]="currentDocsVersionMode() === 'next'"
[class.adev-nav-primary--deprecated]="currentDocsVersionMode() === 'deprecated'"
>
<button
type="button"
@ -83,58 +83,65 @@
>
<a aria-label="Angular homepage" routerLink="/">
<!-- Logo Symbol -->
@if(!isUwu) {
<svg class="angular-logo" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="32">
<g clip-path="url(#a)">
<path
fill="url(#b)"
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
<path
fill="url(#c)"
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
</g>
<defs>
<linearGradient
id="b"
x1="49.009"
x2="225.829"
y1="213.75"
y2="129.722"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#E40035" />
<stop offset=".24" stop-color="#F60A48" />
<stop offset=".352" stop-color="#F20755" />
<stop offset=".494" stop-color="#DC087D" />
<stop offset=".745" stop-color="#9717E7" />
<stop offset="1" stop-color="#6C00F5" />
</linearGradient>
<linearGradient
id="c"
x1="41.025"
x2="156.741"
y1="28.344"
y2="160.344"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF31D9" />
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0" />
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h223v236H0z" />
</clipPath>
</defs>
</svg>
} @else {
<img
src="assets/images/uwu.png"
style="width: auto; margin: 0"
class="uwu-logo"
alt="Angular logo"
height="34"/>
}
@if (!isUwu) {
<svg
class="angular-logo"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 223 236"
width="32"
>
<g clip-path="url(#a)">
<path
fill="url(#b)"
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
<path
fill="url(#c)"
d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
</g>
<defs>
<linearGradient
id="b"
x1="49.009"
x2="225.829"
y1="213.75"
y2="129.722"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#E40035" />
<stop offset=".24" stop-color="#F60A48" />
<stop offset=".352" stop-color="#F20755" />
<stop offset=".494" stop-color="#DC087D" />
<stop offset=".745" stop-color="#9717E7" />
<stop offset="1" stop-color="#6C00F5" />
</linearGradient>
<linearGradient
id="c"
x1="41.025"
x2="156.741"
y1="28.344"
y2="160.344"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF31D9" />
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0" />
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h223v236H0z" />
</clipPath>
</defs>
</svg>
} @else {
<img
src="assets/images/uwu.png"
style="width: auto; margin: 0"
class="uwu-logo"
alt="Angular logo"
height="34"
/>
}
</a>
<!-- Version picker for v18+ -->
@ -150,7 +157,7 @@
(cdkMenuClosed)="closeMenu()"
(click)="openVersionMenu($event)"
>
{{ currentDocsVersion()?.displayName }}
{{ currentDocsVersion().displayName }}
<svg
xmlns="http://www.w3.org/2000/svg"
height="15"
@ -165,16 +172,16 @@
<ng-template #docsVersionMiniMenu>
<ul class="adev-mini-menu adev-version-picker" cdkMenu>
@for (item of versions(); track item) {
<li>
<a
type="button"
cdkMenuItem
[href]="item.url"
[attr.aria-label]="item.displayName"
>
<span>{{ item.displayName }}</span>
</a>
</li>
<li>
<a
type="button"
cdkMenuItem
[href]="item.url"
[attr.aria-label]="item.displayName"
>
<span>{{ item.displayName }}</span>
</a>
</li>
}
</ul>
</ng-template>
@ -363,8 +370,9 @@
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.725-1.498 2.697-4.29 4.532-5.668C13.855.955 16 .186 16 2.632c0 .489-.28 4.105-.444 4.692-.572 2.04-2.653 2.561-4.504 2.246 3.236.551 4.06 2.375 2.281 4.2-3.376 3.464-4.852-.87-5.23-1.98-.07-.204-.103-.3-.103-.218 0-.081-.033.014-.102.218-.379 1.11-1.855 5.444-5.231 1.98-1.778-1.825-.955-3.65 2.28-4.2-1.85.315-3.932-.205-4.503-2.246C.28 6.737 0 3.12 0 2.632 0 .186 2.145.955 3.468 1.948Z"></path>
<path
d="M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.725-1.498 2.697-4.29 4.532-5.668C13.855.955 16 .186 16 2.632c0 .489-.28 4.105-.444 4.692-.572 2.04-2.653 2.561-4.504 2.246 3.236.551 4.06 2.375 2.281 4.2-3.376 3.464-4.852-.87-5.23-1.98-.07-.204-.103-.3-.103-.218 0-.081-.033.014-.102.218-.379 1.11-1.855 5.444-5.231 1.98-1.778-1.825-.955-3.65 2.28-4.2-1.85.315-3.932-.205-4.503-2.246C.28 6.737 0 3.12 0 2.632 0 .186 2.145.955 3.468 1.948Z"
></path>
</svg>
</a>
</li>
@ -403,21 +411,27 @@
</a>
</li>
<li>
<a [href]="DISCORD" cdkMenuItem title="Angular Discord" target="_blank" rel="noopener">
<a
[href]="DISCORD"
cdkMenuItem
title="Angular Discord"
target="_blank"
rel="noopener"
>
<!-- Discord Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36"
width="20"
height="20"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,110.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
/>
</svg>
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36"
width="20"
height="20"
fill="none"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,110.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
/>
</svg>
</a>
</li>
</ul>
@ -434,13 +448,17 @@
(cdkMenuOpened)="openMenu('theme-picker')"
>
<docs-icon role="presentation">
@switch (theme()) { @case ('light') {
{{ 'light_mode' }}
} @case ('dark') {
{{ 'dark_mode' }}
} @case ('auto') {
{{ 'routine' }}
} }
@switch (theme()) {
@case ('light') {
{{ 'light_mode' }}
}
@case ('dark') {
{{ 'dark_mode' }}
}
@case ('auto') {
{{ 'routine' }}
}
}
</docs-icon>
</button>
@ -487,15 +505,16 @@
<!-- Tablet: Second horizontal nav layer which opens the secondary nav -->
@if (activeRouteItem() === DOCS_ROUTE || activeRouteItem() === REFERENCE_ROUTE) {
<div class="adev-secondary-tablet-bar">
<button type="button" (click)="openMobileNav($event)">
<docs-icon class="docs-icon_high-contrast">menu</docs-icon>
@if (activeRouteItem() === DOCS_ROUTE) {
<span>Docs</span>
} @if (activeRouteItem() === REFERENCE_ROUTE) {
<span>API</span>
}
</button>
</div>
<div class="adev-secondary-tablet-bar">
<button type="button" (click)="openMobileNav($event)">
<docs-icon class="docs-icon_high-contrast">menu</docs-icon>
@if (activeRouteItem() === DOCS_ROUTE) {
<span>Docs</span>
}
@if (activeRouteItem() === REFERENCE_ROUTE) {
<span>API</span>
}
</button>
</div>
}
</div>

View file

@ -94,7 +94,7 @@ export class Navigation implements OnInit {
openedMenu: MenuType | null = null;
currentDocsVersion = this.versionManager.currentDocsVersion;
currentDocsVersionMode = this.versionManager.currentDocsVersion()?.version;
currentDocsVersionMode = this.versionManager.currentDocsVersionMode;
// Set the values of the search label and title only on the client, because the label is user-agent specific.
searchLabel = this.isBrowser

View file

@ -1,11 +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.dev/license
*/
import {InjectionToken} from '@angular/core';
export const CURRENT_MAJOR_VERSION = new InjectionToken<number>('CURRENT_MAJOR_VERSION');

View file

@ -6,54 +6,100 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {TestBed} from '@angular/core/testing';
import {Injectable, VERSION, computed, inject} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {httpResource} from '@angular/common/http';
import {INITIAL_ADEV_DOCS_VERSION, VersionManager} from './version-manager.service';
import {WINDOW} from '@angular/docs';
import {CURRENT_MAJOR_VERSION} from '../providers/current-version';
import versionJson from '../../../assets/others/versions.json';
describe('VersionManager', () => {
const fakeWindow = {location: {hostname: 'angular.dev'}};
const fakeCurrentMajorVersion = 19;
export interface Version {
displayName: string;
url: string;
}
let service: VersionManager;
export type VersionMode = 'stable' | 'deprecated' | 'rc' | 'next' | number;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{
provide: WINDOW,
useValue: fakeWindow,
},
{
provide: CURRENT_MAJOR_VERSION,
useValue: fakeCurrentMajorVersion,
},
],
});
service = TestBed.inject(VersionManager);
export const INITIAL_ADEV_DOCS_VERSION = 18;
export const VERSION_PLACEHOLDER = '{{version}}';
export const MODE_PLACEHOLDER = '{{prefix}}';
type VersionJson = {version: string; url: string};
/**
* This service will rely on 2 sources of data for the list of versions.
*
* To have an up-to-date list of versions, it will fetch a json from the deployed website.
* As fallback it will use a local json file that is bundled with the app.
*/
@Injectable({
providedIn: 'root',
})
export class VersionManager {
private document = inject(DOCUMENT);
readonly currentDocsVersionMode = computed<VersionMode>(() => {
const hostname = this.document.location.hostname;
if (hostname.startsWith('v')) return 'deprecated';
if (hostname.startsWith('rc')) return 'rc';
if (hostname.startsWith('next')) return 'next';
return 'stable';
});
it('should be created', () => {
expect(service).toBeTruthy();
private localVersions = (versionJson as VersionJson[]).map((v) => {
return {
displayName: v.version,
url: v.url,
};
});
it('should contain correct number of Angular Docs versions', () => {
// Note: From v2 to v18 (inclusive), there were no v3
const expectedAioDocsVersionsCount = 16;
// Last stable version and next
const expectedRecentDocsVersionCount = 2;
const expectedPreviousAdevVersionsCount = fakeCurrentMajorVersion - INITIAL_ADEV_DOCS_VERSION;
expect(service['getAioVersions']().length).toBe(expectedAioDocsVersionsCount);
expect(service['getRecentVersions']().length).toBe(expectedRecentDocsVersionCount);
expect(service['getAdevVersions']().length).toBe(expectedPreviousAdevVersionsCount);
expect(service.versions().length).toBe(
expectedAioDocsVersionsCount +
expectedRecentDocsVersionCount +
expectedPreviousAdevVersionsCount,
);
// This handle the fallback if the resource fails.
readonly versions = computed(() => {
return this.remoteVersions.hasValue() ? this.remoteVersions.value() : this.localVersions;
});
});
// Yes this will trigger a cors error on localhost
// but this is fine as we'll fallback to the local versions.json
// which is the most up-to-date anyway.
remoteVersions = httpResource(
() => ({
url: 'https://angular.dev/assets/others/versions.json',
transferCache: false,
cache: 'no-cache',
}),
{
parse: (json: unknown) => {
if (!Array.isArray(json)) {
throw new Error('Invalid version data');
}
return json.map((v: unknown) => {
if (
v === undefined ||
v === null ||
typeof v !== 'object' ||
!('version' in v) ||
!('url' in v) ||
typeof v.version !== 'string' ||
typeof v.url !== 'string'
) {
throw new Error('Invalid version data');
}
return {
displayName: v.version,
url: v.url,
};
});
},
},
);
readonly currentDocsVersion = computed(() => {
// In devmode the version is 0, so we'll target next (which is first on the list)
if (VERSION.major === '0') {
return this.versions()[0];
}
return this.versions().find((v) => v.displayName.includes(VERSION.major)) ?? this.versions()[0];
});
}

View file

@ -6,14 +6,14 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {Injectable, computed, inject, signal} from '@angular/core';
import {VERSIONS_CONFIG} from '../constants/versions';
import {WINDOW} from '@angular/docs';
import {CURRENT_MAJOR_VERSION} from '../providers/current-version';
import {Injectable, VERSION, computed, inject} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {httpResource} from '@angular/common/http';
import versionJson from '../../../assets/others/versions.json';
export interface Version {
displayName: string;
version: VersionMode;
url: string;
}
@ -23,107 +23,83 @@ export const INITIAL_ADEV_DOCS_VERSION = 18;
export const VERSION_PLACEHOLDER = '{{version}}';
export const MODE_PLACEHOLDER = '{{prefix}}';
type VersionJson = {version: string; url: string};
/**
* This service will rely on 2 sources of data for the list of versions.
*
* To have an up-to-date list of versions, it will fetch a json from the deployed website.
* As fallback it will use a local json file that is bundled with the app.
*/
@Injectable({
providedIn: 'root',
})
export class VersionManager {
private readonly currentMajorVersion = inject(CURRENT_MAJOR_VERSION);
private readonly window = inject(WINDOW);
private document = inject(DOCUMENT) as Document;
// Note: We can assume that if the URL starts with v{{version}}, it is documentation for previous versions of Angular.
// Based on URL we can indicate as well if it's rc or next Docs version.
private get currentVersionMode(): VersionMode {
const hostname = this.window.location.hostname;
readonly currentDocsVersionMode = computed<VersionMode>(() => {
const hostname = this.document.location.hostname;
if (hostname.startsWith('v')) return 'deprecated';
if (hostname.startsWith('rc')) return 'rc';
if (hostname.startsWith('next')) return 'next';
return 'stable';
}
versions = signal<Version[]>([
...this.getRecentVersions(),
...this.getAdevVersions(),
...this.getAioVersions(),
]);
currentDocsVersion = computed(() => {
return this.versions().find(
(version) => version.version.toString() === this.currentVersionMode,
);
});
// List of Angular Docs versions which includes current version, next and rc.
private getRecentVersions(): Version[] {
return [
{
url: this.getAdevDocsUrl('next'),
displayName: `next`,
version: 'next',
},
// Note: 'rc' should not be visible for now
// {
// url: this.getAdevDocsUrl('rc'),
// displayName: `rc`,
// version: 'rc',
// },
{
url: 'https://angular.dev/',
displayName: this.getVersion(this.currentMajorVersion),
version: this.currentVersionMode,
},
];
}
// List of Angular Docs versions hosted on angular.dev domain.
private getAdevVersions(): Version[] {
const adevVersions: Version[] = [];
for (
let version = this.currentMajorVersion - 1;
version >= INITIAL_ADEV_DOCS_VERSION;
version--
) {
adevVersions.push({
url: this.getAdevDocsUrl(version),
displayName: this.getVersion(version),
version: 'deprecated',
});
}
return adevVersions;
}
// List of Angular Docs versions hosted on angular.io domain.
private getAioVersions(): Version[] {
return VERSIONS_CONFIG.aioVersions.map((item) =>
this.mapToVersion(item as Pick<Version, 'url' | 'version'>),
);
}
private mapToVersion(value: Pick<Version, 'url' | 'version'>): Version {
private localVersions = (versionJson as VersionJson[]).map((v) => {
return {
...value,
displayName: this.getVersion(value.version),
displayName: v.version,
url: v.url,
};
}
});
private getVersion(versionMode: VersionMode): string {
if (versionMode === 'stable' || versionMode === 'deprecated') {
return `v${this.currentMajorVersion}`;
// This handle the fallback if the resource fails.
readonly versions = computed(() => {
return this.remoteVersions.hasValue() ? this.remoteVersions.value() : this.localVersions;
});
// Yes this will trigger a cors error on localhost
// but this is fine as we'll fallback to the local versions.json
// which is the most up-to-date anyway.
remoteVersions = httpResource(
() => ({
url: 'https://angular.dev/assets/others/versions.json',
transferCache: false,
cache: 'no-cache',
}),
{
parse: (json: unknown) => {
if (!Array.isArray(json)) {
throw new Error('Invalid version data');
}
return json.map((v: unknown) => {
if (
v === undefined ||
v === null ||
typeof v !== 'object' ||
!('version' in v) ||
!('url' in v) ||
typeof v.version !== 'string' ||
typeof v.url !== 'string'
) {
throw new Error('Invalid version data');
}
return {
displayName: v.version,
url: v.url,
};
});
},
},
);
readonly currentDocsVersion = computed(() => {
// In devmode the version is 0, so we'll target next (which is first on the list)
if (VERSION.major === '0') {
return this.versions()[0];
}
if (Number.isInteger(versionMode)) {
return `v${versionMode}`;
}
return versionMode.toString();
}
private getAdevDocsUrl(version: VersionMode): string {
const docsUrlPrefix = isNaN(Number(version)) ? `` : 'v';
return VERSIONS_CONFIG.aDevVersionsLinkPattern
.replace(MODE_PLACEHOLDER, docsUrlPrefix)
.replace(
VERSION_PLACEHOLDER,
`${version.toString() === 'stable' ? '' : `${version.toString()}.`}`,
);
}
return this.versions().find((v) => v.displayName.includes(VERSION.major)) ?? this.versions()[0];
});
}

View file

@ -0,0 +1,16 @@
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
exports_files(
glob(["*"]),
)
copy_to_bin(
name = "others",
srcs = glob(
include = ["**/*"],
exclude = ["BUILD.bazel"],
),
visibility = [
"//visibility:public",
],
)

View file

@ -0,0 +1,78 @@
[
{
"version": "next",
"url": "https://next.angular.dev/overview"
},
{
"version": "v20",
"url": "https://v20.angular.dev/overview"
},
{
"version": "v19",
"url": "https://v19.angular.dev/overview"
},
{
"version": "v18",
"url": "https://v18.angular.dev/overview"
},
{
"version": "v17",
"url": "https://v17.angular.io/docs"
},
{
"version": "v16",
"url": "https://v16.angular.io/docs"
},
{
"version": "v15",
"url": "https://v15.angular.io/docs"
},
{
"version": "v14",
"url": "https://v14.angular.io/docs"
},
{
"version": "v13",
"url": "https://v13.angular.io/docs"
},
{
"version": "v12",
"url": "https://v12.angular.io/docs"
},
{
"version": "v11",
"url": "https://v11.angular.io/docs"
},
{
"version": "v10",
"url": "https://v10.angular.io/docs"
},
{
"version": "v9",
"url": "https://v9.angular.io/docs"
},
{
"version": "v8",
"url": "https://v8.angular.io/docs"
},
{
"version": "v7",
"url": "https://v7.angular.io/docs"
},
{
"version": "v6",
"url": "https://v6.angular.io/docs"
},
{
"version": "v5",
"url": "https://v5.angular.io/docs"
},
{
"version": "v4",
"url": "https://v4.angular.io/docs"
},
{
"version": "v2",
"url": "https://v2.angular.io/docs"
}
]