Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@angular/schematics): ensure app-shell schematic consistently uses withAppShell #29886

Merged
merged 1 commit into from
Mar 19, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 2 additions & 138 deletions packages/schematics/angular/app-shell/index.ts
Original file line number Diff line number Diff line change
@@ -6,34 +6,23 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {
Rule,
SchematicsException,
Tree,
chain,
noop,
schematic,
} from '@angular-devkit/schematics';
import { Rule, SchematicsException, Tree, chain, schematic } from '@angular-devkit/schematics';
import { dirname, join } from 'node:path/posix';
import ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import {
addSymbolToNgModuleMetadata,
findNode,
findNodes,
getDecoratorMetadata,
getSourceNodes,
insertImport,
isImported,
} from '../utility/ast-utils';
import { applyToUpdateRecorder } from '../utility/change';
import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils';
import { isUsingApplicationBuilder, targetBuildNotFoundError } from '../utility/project-targets';
import { targetBuildNotFoundError } from '../utility/project-targets';
import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util';
import { getWorkspace } from '../utility/workspace';
import { Schema as AppShellOptions } from './schema';

const APP_SHELL_ROUTE = 'shell';

function getSourceFile(host: Tree, path: string): ts.SourceFile {
const content = host.readText(path);
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
@@ -156,126 +145,6 @@ function getMetadataProperty(metadata: ts.Node, propertyName: string): ts.Proper
return property;
}

function addServerRoutes(options: AppShellOptions): Rule {
return async (host: Tree) => {
// The workspace gets updated so this needs to be reloaded
const workspace = await getWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
throw new SchematicsException(`Invalid project name (${options.project})`);
}

const modulePath = getServerModulePath(host, project.sourceRoot || 'src', 'main.server.ts');
if (modulePath === null) {
throw new SchematicsException('Server module not found.');
}

let moduleSource = getSourceFile(host, modulePath);
if (!isImported(moduleSource, 'Routes', '@angular/router')) {
const recorder = host.beginUpdate(modulePath);
const routesChange = insertImport(moduleSource, modulePath, 'Routes', '@angular/router');
if (routesChange) {
applyToUpdateRecorder(recorder, [routesChange]);
}

const imports = getSourceNodes(moduleSource)
.filter((node) => node.kind === ts.SyntaxKind.ImportDeclaration)
.sort((a, b) => a.getStart() - b.getStart());
const insertPosition = imports[imports.length - 1].getEnd();
const routeText = `\n\nconst routes: Routes = [ { path: '${APP_SHELL_ROUTE}', component: AppShell }];`;
recorder.insertRight(insertPosition, routeText);
host.commitUpdate(recorder);
}

moduleSource = getSourceFile(host, modulePath);
if (!isImported(moduleSource, 'RouterModule', '@angular/router')) {
const recorder = host.beginUpdate(modulePath);
const routerModuleChange = insertImport(
moduleSource,
modulePath,
'RouterModule',
'@angular/router',
);

if (routerModuleChange) {
applyToUpdateRecorder(recorder, [routerModuleChange]);
}

const metadataChange = addSymbolToNgModuleMetadata(
moduleSource,
modulePath,
'imports',
'RouterModule.forRoot(routes)',
);
if (metadataChange) {
applyToUpdateRecorder(recorder, metadataChange);
}
host.commitUpdate(recorder);
}
};
}

function addStandaloneServerRoute(options: AppShellOptions): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
const project = workspace.projects.get(options.project);
if (!project) {
throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`);
}

const configFilePath = join(project.sourceRoot ?? 'src', 'app/app.config.server.ts');
if (!host.exists(configFilePath)) {
throw new SchematicsException(`Cannot find "${configFilePath}".`);
}

const recorder = host.beginUpdate(configFilePath);
let configSourceFile = getSourceFile(host, configFilePath);
if (!isImported(configSourceFile, 'ROUTES', '@angular/router')) {
const routesChange = insertImport(
configSourceFile,
configFilePath,
'ROUTES',
'@angular/router',
);

if (routesChange) {
applyToUpdateRecorder(recorder, [routesChange]);
}
}

configSourceFile = getSourceFile(host, configFilePath);
const providersLiteral = findNodes(configSourceFile, ts.isPropertyAssignment).find(
(n) => ts.isArrayLiteralExpression(n.initializer) && n.name.getText() === 'providers',
)?.initializer as ts.ArrayLiteralExpression | undefined;
if (!providersLiteral) {
throw new SchematicsException(
`Cannot find the "providers" configuration in "${configFilePath}".`,
);
}

// Add route to providers literal.
recorder.remove(providersLiteral.getStart(), providersLiteral.getWidth());
const updatedProvidersString = [
...providersLiteral.elements.map((element) => ' ' + element.getText()),
` {
provide: ROUTES,
multi: true,
useValue: [{
path: '${APP_SHELL_ROUTE}',
component: AppShell
}]
}\n `,
];

recorder.insertRight(providersLiteral.getStart(), `[\n${updatedProvidersString.join(',\n')}]`);

applyToUpdateRecorder(recorder, [
insertImport(configSourceFile, configFilePath, 'AppShell', './app-shell/app-shell'),
]);
host.commitUpdate(recorder);
};
}

function addServerRoutingConfig(options: AppShellOptions, isStandalone: boolean): Rule {
return async (host: Tree) => {
const workspace = await getWorkspace(host);
@@ -335,11 +204,6 @@ export default function (options: AppShellOptions): Rule {
return chain([
validateProject(browserEntryPoint),
schematic('server', options),
...(isUsingApplicationBuilder(project)
? [noop()]
: isStandalone
? [addStandaloneServerRoute(options)]
: [addServerRoutes(options)]),
addServerRoutingConfig(options, isStandalone),
schematic('component', {
name: 'app-shell',
2 changes: 0 additions & 2 deletions packages/schematics/angular/server/index.ts
Original file line number Diff line number Diff line change
@@ -14,10 +14,8 @@ import {
apply,
applyTemplates,
chain,
filter,
mergeWith,
move,
noop,
strings,
url,
} from '@angular-devkit/schematics';