docs(docs-infra): Add update guide to adev (#55638)

PR Close #55638
This commit is contained in:
Matthieu Riegler
2024-05-17 01:21:05 +02:00
committed by Dylan Hunn
parent e3eb5fea47
commit 19d782b469
9 changed files with 2791 additions and 5 deletions
+2
View File
@@ -17,6 +17,7 @@ export enum PagePrefix {
PLAYGROUND = 'playground',
REFERENCE = 'reference',
TUTORIALS = 'tutorials',
UPDATE = 'update-guide',
}
// The enum with the default pages for each main tab
@@ -25,4 +26,5 @@ export enum DefaultPage {
REFERENCE = 'api',
TUTORIALS = 'tutorials',
PLAYGROUND = 'playground',
UPDATE = 'update-guide',
}
@@ -202,7 +202,8 @@ export class Navigation implements OnInit {
this.activeRouteItem.set(PagePrefix.DOCS);
} else if (
urlAfterRedirects.startsWith(PagePrefix.REFERENCE) ||
urlAfterRedirects.startsWith(PagePrefix.API)
urlAfterRedirects.startsWith(PagePrefix.API) ||
urlAfterRedirects.startsWith(PagePrefix.UPDATE)
) {
this.activeRouteItem.set(PagePrefix.REFERENCE);
} else if (urlAfterRedirects === PagePrefix.PLAYGROUND) {
@@ -65,12 +65,12 @@ export class SecondaryNavigation implements OnInit {
private readonly router = inject(Router);
private readonly window = inject(WINDOW);
isSecondaryNavVisible = this.navigationState.isMobileNavVisible;
primaryActiveRouteItem = this.navigationState.primaryActiveRouteItem;
maxVisibleLevelsOnSecondaryNav = computed(() =>
readonly isSecondaryNavVisible = this.navigationState.isMobileNavVisible;
readonly primaryActiveRouteItem = this.navigationState.primaryActiveRouteItem;
readonly maxVisibleLevelsOnSecondaryNav = computed(() =>
this.primaryActiveRouteItem() === PagePrefix.REFERENCE ? 1 : 2,
);
navigationItemsSlides = this.navigationState.expandedItems;
readonly navigationItemsSlides = this.navigationState.expandedItems;
navigationItems: NavigationItem[] | undefined;
translateX = computed(() => {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
<div class="page docs-viewer">
<h1 tabindex="-1">Update Guide</h1>
<div class="wizard">
<div>
<h2>Select the options that match your update</h2>
<h3>Angular versions</h3>
<div class="adev-version-selector">
<span class="adev-template-select">
From v.
<button [cdkMenuTriggerFor]="templatesMenu">
<span>{{ from.name }}</span>
<docs-icon>expand_more</docs-icon>
</button>
<ng-template #templatesMenu>
<ul class="adev-template-dropdown" cdkMenu>
@for (version of versions; track $index) {
<li>
<button cdkMenuItem type="button" (click)="from=version; showUpdatePath()">
<span>{{ version.name }}</span>
</button>
</li>
}
</ul>
</ng-template>
</span>
<span>
<span class="adev-template-select">
To v.
<button [cdkMenuTriggerFor]="templatesMenu">
<span>{{ to.name }}</span>
<docs-icon>expand_more</docs-icon>
</button>
<ng-template #templatesMenu>
<ul class="adev-template-dropdown" cdkMenu>
@for (version of versions; track $index) {
<li>
<button cdkMenuItem type="button" (click)="to=version; showUpdatePath()">
<span>{{ version.name }}</span>
</button>
</li>
}
</ul>
</ng-template>
</span>
</span>
</div>
@if (from.number >= futureVersion || to.number >= futureVersion) {
<div class="docs-alert docs-alert-critical">
<p>
<strong>Warning:</strong>
Plans for releases after the current major release are not finalized and may change.
These recommendations are based on scheduled deprecations.
</p>
</div>
}
@if (from.number > to.number) {
<div class="docs-alert docs-alert-critical">
<p>
<strong>Warning:</strong>
We do not support downgrading versions of Angular.
</p>
</div>
}
@if ((to.number - from.number > 150) && from.number > 240) {
<div class="docs-alert docs-alert-critical">
<p>
<strong>Warning:</strong>
Be sure to follow the guide below to migrate your application to the new version. You
can't run
<code>ng update</code>
to update Angular applications more than one major version at a time.
</p>
</div>
}
<h3>Application complexity</h3>
<mat-button-toggle-group
(change)="level = $event.value;"
[value]="level"
style="margin-bottom:16px;"
>
<mat-button-toggle [value]="1">Basic</mat-button-toggle>
<mat-button-toggle [value]="2">Medium</mat-button-toggle>
<mat-button-toggle [value]="3">Advanced</mat-button-toggle>
</mat-button-toggle-group>
@if (level === 1) {
<p>Shows information for all Angular developers.</p>
} @else if (level === 2) {
<p>Shows information that's of interest to more advanced Angular developers.</p>
} @else if (level === 3) {
<p>Shows all the information we have about this update.</p>
}
<h3>Other dependencies</h3>
@for (option of optionList; track $index) {
<p>
<mat-checkbox
(change)="options[option.id] = $event.checked; showUpdatePath()"
[checked]="options[option.id]"
>I use {{option.name}} {{option.description}}</mat-checkbox
>
</p>
}
@if (from.number < 600) {
<h4>Package Manager</h4>
<mat-button-toggle-group
(change)="packageManager = $event.value; showUpdatePath()"
[value]="packageManager"
>
<mat-button-toggle value="npm install">npm</mat-button-toggle>
<mat-button-toggle value="yarn add">yarn</mat-button-toggle>
</mat-button-toggle-group>
}
<button
type="button"
(click)="showUpdatePath()"
class="docs-primary-btn show-button"
[attr.text]="'Show me how to update!'"
>
Show me how to update!
</button>
</div>
</div>
<hr />
<!-- RECOMMENDATIONS SECTION -->
@if (
beforeRecommendations.length > 0 || duringRecommendations.length > 0 || afterRecommendations.length > 0
) {
<div class="recommendations">
<h2>{{title}}</h2>
<h3>Before you update</h3>
@for (r of beforeRecommendations; track $index) {
<div class="adev-recommentation-item">
<mat-checkbox></mat-checkbox>
<div [innerHTML]="r.renderedStep"></div>
</div>
}
@if (beforeRecommendations.length <= 0) {
<div>
<em>You don't need to do anything before moving between these versions.</em>
</div>
}
<h3>Update to the new version</h3>
@if (duringRecommendations.length > 0) {
<div>
<em>Review these changes and perform the actions to update your application.</em>
</div>
}
@for (r of duringRecommendations; track $index) {
<div class="adev-recommentation-item">
<mat-checkbox></mat-checkbox>
<div [innerHTML]="r.renderedStep"></div>
</div>
}
@if (duringRecommendations.length <= 0) {
<div>
<em>There aren't any recommendations for moving between these versions.</em>
</div>
}
<h3>After you update</h3>
@for (r of afterRecommendations; track $index) {
<div class="adev-recommentation-item">
<mat-checkbox></mat-checkbox>
<div [innerHTML]="r.renderedStep"></div>
</div>
}
@if (afterRecommendations.length <= 0) {
<div>
<em>You don't need to do anything after moving between these versions.</em>
</div>
}
</div>
}
</div>
@@ -0,0 +1,100 @@
.page {
padding: var(--layout-padding);
max-width: var(--page-width);
container: update-guide-page / inline-size;
}
h3,
h4 {
margin-block-start: 2em;
}
.wizard {
padding-inline: 1rem;
.show-button {
display: block;
margin-block-start: 2rem;
}
}
.adev-version-selector {
display: flex;
gap: 1rem;
}
.adev-template-dropdown {
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
padding: 0;
transform: translateY(-0.7rem);
max-height: 200px;
overflow-y: auto;
li {
list-style: none;
width: 198px;
box-sizing: border-box;
button {
background: var(--page-background);
font-size: 0.875rem;
width: 100%;
text-align: left;
padding-block: 0.5rem;
color: var(--quaternary-contrast);
transition:
color 0.3s ease,
background 0.3s ease;
font-weight: 400;
&:hover {
background: var(--senary-contrast);
color: var(--primary-contrast);
}
}
}
}
.adev-template-select {
margin-block-end: 0.5rem;
// cdk select button
button {
font-size: 0.875rem;
border: 1px solid var(--senary-contrast);
border-radius: 0.25rem;
width: 200px;
display: inline-flex;
justify-content: space-between;
align-items: center;
padding-block: 0.5rem;
font-weight: 400;
transition: border 0.3s ease;
span {
color: var(--primary-contrast);
transition: color 0.3s ease;
margin-inline-start: 0.1rem;
}
docs-icon {
font-size: 1.3rem;
color: var(--quaternary-contrast);
transition: color 0.3s ease;
}
}
}
.adev-recommentation-item {
display: flex;
align-items: center;
> div {
margin-inline-start: 2rem;
}
// Code blocks are generable from the markdown, we need to opt-out of the scoping
::ng-deep code {
cursor: pointer;
}
}
@@ -0,0 +1,293 @@
import {Component, HostListener, inject} from '@angular/core';
import {Step, RECOMMENDATIONS} from './recommendations';
import {Clipboard} from '@angular/cdk/clipboard';
import {CdkMenuModule} from '@angular/cdk/menu';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatInputModule} from '@angular/material/input';
import {MatCardModule} from '@angular/material/card';
import {MatGridListModule} from '@angular/material/grid-list';
import {MatButtonToggleModule} from '@angular/material/button-toggle';
import {IconComponent} from '@angular/docs';
import {ActivatedRoute, Router} from '@angular/router';
import {marked} from 'marked';
interface Option {
id: keyof Step;
name: string;
description: string;
}
@Component({
selector: 'adev-update-guide',
templateUrl: './update.component.html',
styleUrl: './update.component.scss',
imports: [
MatCheckboxModule,
MatInputModule,
MatCardModule,
MatGridListModule,
MatButtonToggleModule,
CdkMenuModule,
IconComponent,
],
standalone: true,
})
export default class AppComponent {
protected title = '';
protected level = 1;
protected options: Record<string, boolean> = {
ngUpgrade: false,
material: false,
windows: isWindows(),
};
protected readonly optionList: Option[] = [
{id: 'ngUpgrade', name: 'ngUpgrade', description: 'to combine AngularJS & Angular'},
{id: 'material', name: 'Angular Material', description: ''},
{id: 'windows', name: 'Windows', description: ''},
];
protected packageManager: 'npm install' | 'yarn add' = 'npm install';
protected beforeRecommendations: Step[] = [];
protected duringRecommendations: Step[] = [];
protected afterRecommendations: Step[] = [];
protected readonly versions = [
{name: '18.0', number: 1800},
{name: '17.0', number: 1700},
{name: '16.0', number: 1600},
{name: '15.0', number: 1500},
{name: '14.0', number: 1400},
{name: '13.0', number: 1300},
{name: '12.0', number: 1200},
{name: '11.0', number: 1100},
{name: '10.2', number: 1020},
{name: '10.1', number: 1010},
{name: '10.0', number: 1000},
{name: '9.1', number: 910},
{name: '9.0', number: 900},
{name: '8.2', number: 820},
{name: '8.1', number: 810},
{name: '8.0', number: 800},
{name: '7.2', number: 720},
{name: '7.1', number: 710},
{name: '7.0', number: 700},
{name: '6.1', number: 610},
{name: '6.0', number: 600},
{name: '5.2', number: 520},
{name: '5.1', number: 510},
{name: '5.0', number: 500},
{name: '4.4', number: 440},
{name: '4.3', number: 430},
{name: '4.2', number: 420},
{name: '4.1', number: 410},
{name: '4.0', number: 400},
{name: '2.4', number: 204},
{name: '2.3', number: 203},
{name: '2.2', number: 202},
{name: '2.1', number: 201},
{name: '2.0', number: 200},
];
protected from = this.versions.find((version) => version.name === '16.0')!;
protected to = this.versions.find((version) => version.name === '17.0')!;
protected futureVersion = 1800;
protected readonly steps: Step[] = RECOMMENDATIONS;
private readonly clipboard = inject(Clipboard);
private readonly router = inject(Router);
private readonly activatedRoute = inject(ActivatedRoute);
constructor() {
const queryMap = this.activatedRoute.snapshot.queryParamMap;
// Detect settings in URL
this.level = parseInt(queryMap.get('l')!, 10) || this.level;
// Detect versions of from and to
const versions = queryMap.get('v');
if (versions) {
const [from, to] = versions.split('-');
this.from = this.versions.find((version) => version.name === from)!;
this.to = this.versions.find((version) => version.name === to)!;
this.showUpdatePath();
}
}
@HostListener('click', ['$event.target'])
copyCode({tagName, textContent}: Element) {
if (tagName === 'CODE') {
// TODO: add a toast notification
this.clipboard.copy(textContent!);
}
}
async showUpdatePath() {
this.beforeRecommendations = [];
this.duringRecommendations = [];
this.afterRecommendations = [];
// Refuse to generate recommendations for downgrades
if (this.to.number < this.from.number) {
alert('We do not support downgrading versions of Angular.');
return;
}
const labelTitle = 'Guide to update your Angular application';
const labelBasic = 'basic applications';
const labelMedium = 'medium applications';
const labelAdvanced = 'advanced applications';
this.title = `${labelTitle} v${this.from.name} -> v${this.to.name}
for
${this.level < 2 ? labelBasic : this.level < 3 ? labelMedium : labelAdvanced}`;
// Find applicable steps and organize them into before, during, and after upgrade
for (const step of this.steps) {
if (step.level <= this.level && step.necessaryAsOf > this.from.number) {
// Check Options
// Only show steps that don't have a required option
// Or when the user has a matching option selected
let skip = false;
for (const option of this.optionList) {
// Skip steps which require an option not set by the user.
if (step[option.id] && !this.options[option.id]) {
skip = true;
}
// Skip steps which require **not** using an option which **is** set
// by the user.
if (step[option.id] === false && this.options[option.id]) {
skip = true;
}
}
if (skip) {
continue;
}
// Render and replace variables
step.renderedStep = await marked(this.replaceVariables(step.action));
// If you could do it before now, but didn't have to finish it before now
if (step.possibleIn <= this.from.number && step.necessaryAsOf >= this.from.number) {
this.beforeRecommendations.push(step);
// If you couldn't do it before now, and you must do it now
} else if (step.possibleIn > this.from.number && step.necessaryAsOf <= this.to.number) {
this.duringRecommendations.push(step);
} else if (step.possibleIn <= this.to.number) {
this.afterRecommendations.push(step);
}
}
}
// Update the URL so users can link to this transition
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: {v: `${this.from.name}-${this.to.name}`, l: this.level},
queryParamsHandling: 'merge',
});
// Tell everyone how to upgrade for v6 or earlier
this.renderPreV6Instructions();
}
private getAdditionalDependencies(version: number): string {
if (version < 500) {
return `typescript@'>=2.1.0 <2.4.0'`;
} else if (version < 600) {
return `typescript@2.4.2 rxjs@^5.5.2`;
} else {
return `typescript@2.7.x rxjs@^6.0.0`;
}
}
private getAngularVersion(version: number): string {
if (version < 400) {
return `'^2.0.0'`;
} else {
const major = Math.floor(version / 100);
const minor = Math.floor((version - major * 100) / 10);
return `^${major}.${minor}.0`;
}
}
private async renderPreV6Instructions(): Promise<void> {
let upgradeStep: Step;
const isWindows = /win/i.test(navigator.platform);
const additionalDeps = this.getAdditionalDependencies(this.to.number);
const angularVersion = this.getAngularVersion(this.to.number);
const angularPackages = [
'animations',
'common',
'compiler',
'compiler-cli',
'core',
'forms',
'http',
'platform-browser',
'platform-browser-dynamic',
'platform-server',
'router',
];
// Provide npm/yarn instructions for versions before 6
if (this.to.number < 600) {
const actionMessage = `Update all of your dependencies to the latest Angular and the right version of TypeScript.`;
if (isWindows) {
const packages =
angularPackages
.map((packageName) => `@angular/${packageName}@${angularVersion}`)
.join(' ') +
' ' +
additionalDeps;
upgradeStep = {
step: 'General Update',
action: `${actionMessage}
If you are using Windows, you can use:
\`${this.packageManager} ${packages}\``,
} as Step;
} else {
const packages = `@angular/{${angularPackages.join(',')}}@${angularVersion} ${additionalDeps}`;
upgradeStep = {
step: 'General update',
action: `${actionMessage}
If you are using Linux/Mac, you can use:
\`${this.packageManager} ${packages}\``,
} as Step;
}
// Npm installs typescript wrong in v5, let's manually specify
// https://github.com/npm/npm/issues/16813
if (this.packageManager === 'npm install' && this.to.number === 500) {
upgradeStep.action += `
\`npm install typescript@2.4.2 --save-exact\``;
}
upgradeStep.renderedStep = await marked(upgradeStep.action);
this.duringRecommendations.push(upgradeStep);
}
}
private replaceVariables(action: string): string {
let newAction = action;
newAction = newAction.replace(
'${packageManagerGlobalInstall}',
this.packageManager === 'npm install' ? 'npm install -g' : 'yarn global add',
);
newAction = newAction.replace('${packageManagerInstall}', this.packageManager);
return newAction;
}
}
/** Whether or not the user is running on a Windows OS. */
function isWindows(): boolean {
const platform = navigator.platform.toLowerCase();
return platform.includes('windows') || platform.includes('win32');
}
+8
View File
@@ -41,6 +41,12 @@ const referencePageRoutes = mapNavigationItemsToRoutes(
},
);
const updateGuidePageRoute: Route = {
path: referenceNavigationItems.find((r) => r.path === DefaultPage.UPDATE)!.path,
loadComponent: () => import('./features/update/update.component'),
data: commonReferenceRouteData,
};
const cliReferencePageRoutes = mapNavigationItemsToRoutes(
referenceNavigationItems.filter((r) => r.path?.startsWith(`${PagePrefix.CLI}/`)),
{
@@ -61,6 +67,7 @@ const docsReferencePageRoutes = mapNavigationItemsToRoutes(
referenceNavigationItems.filter(
(r) =>
r.path !== DefaultPage.REFERENCE &&
r.path !== DefaultPage.UPDATE &&
!r.path?.startsWith(`${PagePrefix.API}/`) &&
!r.path?.startsWith(`${PagePrefix.CLI}/`),
),
@@ -143,6 +150,7 @@ export const routes: Route[] = [
...SUB_NAVIGATION_ROUTES,
...API_REFERENCE_ROUTES,
...FOOTER_ROUTES,
updateGuidePageRoute,
],
},
// Error page
+4
View File
@@ -1370,6 +1370,10 @@ const REFERENCE_SUB_NAVIGATION_DATA: NavigationItem[] = [
path: 'reference/versions',
contentPath: 'reference/versions',
},
{
label: 'Update guide',
path: 'update-guide',
},
{
label: 'Configurations',
children: [