diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 7cf261c47295..c0aa38fb3db6 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -323,7 +323,10 @@ export async function normalizeOptions( * For instance, accessing `foo.com/` would lead to `foo.com/index.html` being served instead of hitting the server. */ const indexBaseName = path.basename(options.index); - indexOutput = ssrOptions && indexBaseName === 'index.html' ? INDEX_HTML_CSR : indexBaseName; + indexOutput = + ssrOptions && options.outputMode !== OutputMode.Static && indexBaseName === 'index.html' + ? INDEX_HTML_CSR + : indexBaseName; } else { indexOutput = options.index.output || 'index.html'; } diff --git a/packages/angular/ssr/schematics/ng-add/index_spec.ts b/packages/angular/ssr/schematics/ng-add/index_spec.ts index f254dd78cd64..b93a509200b1 100644 --- a/packages/angular/ssr/schematics/ng-add/index_spec.ts +++ b/packages/angular/ssr/schematics/ng-add/index_spec.ts @@ -52,7 +52,7 @@ describe('@angular/ssr ng-add schematic', () => { }); it('works', async () => { - const filePath = '/projects/test-app/server.ts'; + const filePath = '/projects/test-app/src/server.ts'; expect(appTree.exists(filePath)).toBeFalse(); const tree = await schematicRunner.runSchematic('ng-add', defaultOptions, appTree); diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts b/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts index f62208738507..78c718eee28f 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts @@ -51,6 +51,7 @@ export async function extractMessages( buildOptions.budgets = undefined; buildOptions.index = false; buildOptions.serviceWorker = false; + buildOptions.server = undefined; buildOptions.ssr = false; buildOptions.appShell = false; buildOptions.prerender = false; diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index 5d4878f6fe81..f8dc1f399840 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -29,8 +29,7 @@ import { import { applyToUpdateRecorder } from '../utility/change'; import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils'; import { findBootstrapApplicationCall, getMainFilePath } from '../utility/standalone/util'; -import { getWorkspace, updateWorkspace } from '../utility/workspace'; -import { Builders } from '../utility/workspace-models'; +import { getWorkspace } from '../utility/workspace'; import { Schema as AppShellOptions } from './schema'; const APP_SHELL_ROUTE = 'shell'; @@ -140,77 +139,6 @@ function validateProject(mainPath: string): Rule { }; } -function addAppShellConfigToWorkspace(options: AppShellOptions): Rule { - return (host, context) => { - return updateWorkspace((workspace) => { - const project = workspace.projects.get(options.project); - if (!project) { - return; - } - - const buildTarget = project.targets.get('build'); - if (buildTarget?.builder === Builders.Application) { - // Application builder configuration. - const prodConfig = buildTarget.configurations?.production; - if (!prodConfig) { - throw new SchematicsException( - `A "production" configuration is not defined for the "build" builder.`, - ); - } - - prodConfig.appShell = true; - - return; - } - - // Webpack based builders configuration. - // Validation of targets is handled already in the main function. - // Duplicate keys means that we have configurations in both server and build builders. - const serverConfigKeys = project.targets.get('server')?.configurations ?? {}; - const buildConfigKeys = project.targets.get('build')?.configurations ?? {}; - - const configurationNames = Object.keys({ - ...serverConfigKeys, - ...buildConfigKeys, - }); - - const configurations: Record = {}; - for (const key of configurationNames) { - if (!serverConfigKeys[key]) { - context.logger.warn( - `Skipped adding "${key}" configuration to "app-shell" target as it's missing from "server" target.`, - ); - - continue; - } - - if (!buildConfigKeys[key]) { - context.logger.warn( - `Skipped adding "${key}" configuration to "app-shell" target as it's missing from "build" target.`, - ); - - continue; - } - - configurations[key] = { - browserTarget: `${options.project}:build:${key}`, - serverTarget: `${options.project}:server:${key}`, - }; - } - - project.targets.add({ - name: 'app-shell', - builder: Builders.AppShell, - defaultConfiguration: configurations['production'] ? 'production' : undefined, - options: { - route: APP_SHELL_ROUTE, - }, - configurations, - }); - }); - }; -} - function addRouterModule(mainPath: string): Rule { return (host: Tree) => { const modulePath = getAppModulePath(host, mainPath); @@ -313,6 +241,7 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule { 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( @@ -322,10 +251,8 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule { '@angular/router', ); - const recorder = host.beginUpdate(configFilePath); if (routesChange) { applyToUpdateRecorder(recorder, [routesChange]); - host.commitUpdate(recorder); } } @@ -340,45 +267,20 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule { } // Add route to providers literal. - const newProvidersLiteral = ts.factory.updateArrayLiteralExpression(providersLiteral, [ - ...providersLiteral.elements, - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment('provide', ts.factory.createIdentifier('ROUTES')), - ts.factory.createPropertyAssignment('multi', ts.factory.createIdentifier('true')), - ts.factory.createPropertyAssignment( - 'useValue', - ts.factory.createArrayLiteralExpression( - [ - ts.factory.createObjectLiteralExpression( - [ - ts.factory.createPropertyAssignment( - 'path', - ts.factory.createIdentifier(`'${APP_SHELL_ROUTE}'`), - ), - ts.factory.createPropertyAssignment( - 'component', - ts.factory.createIdentifier('AppShellComponent'), - ), - ], - true, - ), - ], - true, - ), - ), - ], - true, - ), - ]); - - const recorder = host.beginUpdate(configFilePath); recorder.remove(providersLiteral.getStart(), providersLiteral.getWidth()); - const printer = ts.createPrinter(); - recorder.insertRight( - providersLiteral.getStart(), - printer.printNode(ts.EmitHint.Unspecified, newProvidersLiteral, configSourceFile), - ); + const updatedProvidersString = [ + ...providersLiteral.elements.map((element) => ' ' + element.getText()), + ` { + provide: ROUTES, + multi: true, + useValue: [{ + path: '${APP_SHELL_ROUTE}', + component: AppShellComponent + }] + }\n `, + ]; + + recorder.insertRight(providersLiteral.getStart(), `[\n${updatedProvidersString.join(',\n')}]`); // Add AppShellComponent import const appShellImportChange = insertImport( @@ -393,6 +295,52 @@ function addStandaloneServerRoute(options: AppShellOptions): Rule { }; } +function addServerRoutingConfig(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.routes.server.ts'); + if (!host.exists(configFilePath)) { + throw new SchematicsException(`Cannot find "${configFilePath}".`); + } + + const sourceFile = getSourceFile(host, configFilePath); + const nodes = getSourceNodes(sourceFile); + + // Find the serverRoutes variable declaration + const serverRoutesNode = nodes.find( + (node) => + ts.isVariableDeclaration(node) && + node.initializer && + ts.isArrayLiteralExpression(node.initializer) && + node.type && + ts.isArrayTypeNode(node.type) && + node.type.getText().includes('ServerRoute'), + ) as ts.VariableDeclaration | undefined; + + if (!serverRoutesNode) { + throw new SchematicsException( + `Cannot find the "ServerRoute" configuration in "${configFilePath}".`, + ); + } + const recorder = host.beginUpdate(configFilePath); + const arrayLiteral = serverRoutesNode.initializer as ts.ArrayLiteralExpression; + const firstElementPosition = + arrayLiteral.elements[0]?.getStart() ?? arrayLiteral.getStart() + 1; + const newRouteString = `{ + path: '${APP_SHELL_ROUTE}', + renderMode: RenderMode.AppShell + },\n`; + recorder.insertLeft(firstElementPosition, newRouteString); + + host.commitUpdate(recorder); + }; +} + export default function (options: AppShellOptions): Rule { return async (tree) => { const browserEntryPoint = await getMainFilePath(tree, options.project); @@ -401,9 +349,9 @@ export default function (options: AppShellOptions): Rule { return chain([ validateProject(browserEntryPoint), schematic('server', options), - addAppShellConfigToWorkspace(options), isStandalone ? noop() : addRouterModule(browserEntryPoint), isStandalone ? addStandaloneServerRoute(options) : addServerRoutes(options), + addServerRoutingConfig(options), schematic('component', { name: 'app-shell', module: 'app.module.server.ts', diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index f617b29f4112..0c1f9a546d9d 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -9,7 +9,6 @@ import { tags } from '@angular-devkit/core'; import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { Schema as ApplicationOptions } from '../application/schema'; -import { Builders } from '../utility/workspace-models'; import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as AppShellOptions } from './schema'; @@ -51,15 +50,6 @@ describe('App Shell Schematic', () => { ); }); - it('should add app shell configuration', async () => { - const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree); - const filePath = '/angular.json'; - const content = tree.readContent(filePath); - const workspace = JSON.parse(content); - const target = workspace.projects.bar.architect['build']; - expect(target.configurations.production.appShell).toBeTrue(); - }); - it('should ensure the client app has a router-outlet', async () => { appTree = await schematicRunner.runSchematic('workspace', workspaceOptions); appTree = await schematicRunner.runSchematic( @@ -168,7 +158,7 @@ describe('App Shell Schematic', () => { expect(content).toMatch( /const routes: Routes = \[ { path: 'shell', component: AppShellComponent }\];/, ); - expect(content).toMatch(/ServerModule,\r?\n\s*RouterModule\.forRoot\(routes\),/); + expect(content).toContain(`ServerModule, RouterModule.forRoot(routes)]`); }); it('should create the shell component', async () => { @@ -205,9 +195,23 @@ describe('App Shell Schematic', () => { const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree); expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true); const content = tree.readContent('/projects/bar/src/app/app.config.server.ts'); + expect(content).toMatch(/app-shell\.component/); }); + it('should update the server routing configuration', async () => { + const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree); + const content = tree.readContent('/projects/bar/src/app/app.routes.server.ts'); + expect(tags.oneLine`${content}`).toContain(tags.oneLine`{ + path: 'shell', + renderMode: RenderMode.AppShell + }, + { + path: '**', + renderMode: RenderMode.Prerender + }`); + }); + it('should define a server route', async () => { const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree); const filePath = '/projects/bar/src/app/app.config.server.ts'; @@ -215,12 +219,10 @@ describe('App Shell Schematic', () => { expect(tags.oneLine`${content}`).toContain(tags.oneLine`{ provide: ROUTES, multi: true, - useValue: [ - { - path: 'shell', - component: AppShellComponent - } - ] + useValue: [{ + path: 'shell', + component: AppShellComponent + }] }`); }); @@ -240,44 +242,4 @@ describe('App Shell Schematic', () => { ); }); }); - - describe('Legacy browser builder', () => { - function convertBuilderToLegacyBrowser(): void { - const config = JSON.parse(appTree.readContent('/angular.json')); - const build = config.projects.bar.architect.build; - - build.builder = Builders.Browser; - build.options = { - ...build.options, - main: build.options.browser, - browser: undefined, - }; - - build.configurations.development = { - ...build.configurations.development, - vendorChunk: true, - namedChunks: true, - buildOptimizer: false, - }; - - appTree.overwrite('/angular.json', JSON.stringify(config, undefined, 2)); - } - - beforeEach(async () => { - appTree = await schematicRunner.runSchematic('application', appOptions, appTree); - convertBuilderToLegacyBrowser(); - }); - - it('should add app shell configuration', async () => { - const tree = await schematicRunner.runSchematic('app-shell', defaultOptions, appTree); - const filePath = '/angular.json'; - const content = tree.readContent(filePath); - const workspace = JSON.parse(content); - const target = workspace.projects.bar.architect['app-shell']; - expect(target.configurations.development.browserTarget).toEqual('bar:build:development'); - expect(target.configurations.development.serverTarget).toEqual('bar:server:development'); - expect(target.configurations.production.browserTarget).toEqual('bar:build:production'); - expect(target.configurations.production.serverTarget).toEqual('bar:server:production'); - }); - }); }); diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index 37ceeb9946ae..0bbfc60f5c43 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -191,7 +191,7 @@ describe('Application Schematic', () => { it(`should create an application with SSR features when 'ssr=true'`, async () => { const options = { ...defaultOptions, ssr: true }; - const filePath = '/projects/foo/server.ts'; + const filePath = '/projects/foo/src/server.ts'; expect(workspaceTree.exists(filePath)).toBeFalse(); const tree = await schematicRunner.runSchematic('application', options, workspaceTree); expect(tree.exists(filePath)).toBeTrue(); @@ -200,7 +200,7 @@ describe('Application Schematic', () => { it(`should not create an application with SSR features when 'ssr=false'`, async () => { const options = { ...defaultOptions, ssr: false }; const tree = await schematicRunner.runSchematic('application', options, workspaceTree); - expect(tree.exists('/projects/foo/server.ts')).toBeFalse(); + expect(tree.exists('/projects/foo/src/server.ts')).toBeFalse(); }); describe(`update package.json`, () => { diff --git a/packages/schematics/angular/environments/index_spec.ts b/packages/schematics/angular/environments/index_spec.ts index 3db69c5d866c..97936e263b38 100644 --- a/packages/schematics/angular/environments/index_spec.ts +++ b/packages/schematics/angular/environments/index_spec.ts @@ -153,43 +153,4 @@ describe('Environments Schematic', () => { }), ); }); - - it('should update the angular.json file replacements option for server configurations', async () => { - convertBuilderToLegacyBrowser(); - - await schematicRunner.runSchematic( - 'server', - { project: 'foo', skipInstall: true }, - applicationTree, - ); - - const tree = await runEnvironmentsSchematic(); - const workspace = JSON.parse(tree.readContent('/angular.json')); - - const developmentConfiguration = - workspace.projects.foo.architect.build.configurations.development; - expect(developmentConfiguration).toEqual( - jasmine.objectContaining({ - fileReplacements: [ - { - replace: 'projects/foo/src/environments/environment.ts', - with: 'projects/foo/src/environments/environment.development.ts', - }, - ], - }), - ); - - const serverDevelopmentConfiguration = - workspace.projects.foo.architect.server.configurations.development; - expect(serverDevelopmentConfiguration).toEqual( - jasmine.objectContaining({ - fileReplacements: [ - { - replace: 'projects/foo/src/environments/environment.ts', - with: 'projects/foo/src/environments/environment.development.ts', - }, - ], - }), - ); - }); }); diff --git a/packages/schematics/angular/server/files/root/tsconfig.server.json.template b/packages/schematics/angular/server/files/root/tsconfig.server.json.template deleted file mode 100644 index 392d45706775..000000000000 --- a/packages/schematics/angular/server/files/root/tsconfig.server.json.template +++ /dev/null @@ -1,15 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "extends": "./<%= tsConfigExtends %>", - "compilerOptions": { - "outDir": "<%= relativePathToWorkspaceRoot %>/out-tsc/server", - "types": [ - "node"<% if (hasLocalizePackage) { %>, - "@angular/localize"<% } %> - ] - }, - "files": [ - "src/main.server.ts" - ] -} diff --git a/packages/schematics/angular/server/files/src/app/app.module.server.ts.template b/packages/schematics/angular/server/files/src/app/app.module.server.ts.template index 795380cd2294..1249d761d045 100644 --- a/packages/schematics/angular/server/files/src/app/app.module.server.ts.template +++ b/packages/schematics/angular/server/files/src/app/app.module.server.ts.template @@ -1,14 +1,13 @@ import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; - -import { AppModule } from './app.module'; +import { provideServerRoutesConfig } from '@angular/ssr'; import { AppComponent } from './app.component'; +import { AppModule } from './app.module'; +import { serverRoutes } from './app.routes.server'; @NgModule({ - imports: [ - AppModule, - ServerModule, - ], + imports: [AppModule, ServerModule], + providers: [provideServerRoutesConfig(serverRoutes)], bootstrap: [AppComponent], }) export class AppServerModule {} diff --git a/packages/schematics/angular/server/files/src/app/app.routes.server.ts.template b/packages/schematics/angular/server/files/src/app/app.routes.server.ts.template new file mode 100644 index 000000000000..ffd37b1f233c --- /dev/null +++ b/packages/schematics/angular/server/files/src/app/app.routes.server.ts.template @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender + } +]; diff --git a/packages/schematics/angular/server/files/standalone-src/app/app.config.server.ts.template b/packages/schematics/angular/server/files/standalone-src/app/app.config.server.ts.template index b4d57c94235f..1b7f65019a98 100644 --- a/packages/schematics/angular/server/files/standalone-src/app/app.config.server.ts.template +++ b/packages/schematics/angular/server/files/standalone-src/app/app.config.server.ts.template @@ -1,10 +1,13 @@ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; +import { provideServerRoutesConfig } from '@angular/ssr'; import { appConfig } from './app.config'; +import { serverRoutes } from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [ - provideServerRendering() + provideServerRendering(), + provideServerRoutesConfig(serverRoutes) ] }; diff --git a/packages/schematics/angular/server/files/standalone-src/app/app.routes.server.ts.template b/packages/schematics/angular/server/files/standalone-src/app/app.routes.server.ts.template new file mode 100644 index 000000000000..ffd37b1f233c --- /dev/null +++ b/packages/schematics/angular/server/files/standalone-src/app/app.routes.server.ts.template @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender + } +]; diff --git a/packages/schematics/angular/server/index.ts b/packages/schematics/angular/server/index.ts index 418fec81070c..5acd9d673871 100644 --- a/packages/schematics/angular/server/index.ts +++ b/packages/schematics/angular/server/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { JsonValue, Path, basename, dirname, join, normalize } from '@angular-devkit/core'; +import { join, normalize } from '@angular-devkit/core'; import { Rule, SchematicsException, @@ -25,7 +25,6 @@ import { getPackageJsonDependency } from '../utility/dependencies'; import { JSONFile } from '../utility/json-file'; import { latestVersions } from '../utility/latest-versions'; import { isStandaloneApp } from '../utility/ng-ast-utils'; -import { relativePathToWorkspaceRoot } from '../utility/paths'; import { targetBuildNotFoundError } from '../utility/project-targets'; import { getMainFilePath } from '../utility/standalone/util'; import { getWorkspace, updateWorkspace } from '../utility/workspace'; @@ -34,65 +33,6 @@ import { Schema as ServerOptions } from './schema'; const serverMainEntryName = 'main.server.ts'; -function updateConfigFileBrowserBuilder(options: ServerOptions, tsConfigDirectory: Path): Rule { - return updateWorkspace((workspace) => { - const clientProject = workspace.projects.get(options.project); - - if (clientProject) { - // In case the browser builder hashes the assets - // we need to add this setting to the server builder - // as otherwise when assets it will be requested twice. - // One for the server which will be unhashed, and other on the client which will be hashed. - const getServerOptions = (options: Record = {}): {} => { - return { - buildOptimizer: options?.buildOptimizer, - outputHashing: options?.outputHashing === 'all' ? 'media' : options?.outputHashing, - fileReplacements: options?.fileReplacements, - optimization: options?.optimization === undefined ? undefined : !!options?.optimization, - sourceMap: options?.sourceMap, - localization: options?.localization, - stylePreprocessorOptions: options?.stylePreprocessorOptions, - resourcesOutputPath: options?.resourcesOutputPath, - deployUrl: options?.deployUrl, - i18nMissingTranslation: options?.i18nMissingTranslation, - preserveSymlinks: options?.preserveSymlinks, - extractLicenses: options?.extractLicenses, - inlineStyleLanguage: options?.inlineStyleLanguage, - vendorChunk: options?.vendorChunk, - }; - }; - - const buildTarget = clientProject.targets.get('build'); - if (buildTarget?.options) { - buildTarget.options.outputPath = `dist/${options.project}/browser`; - } - - const buildConfigurations = buildTarget?.configurations; - const configurations: Record = {}; - if (buildConfigurations) { - for (const [key, options] of Object.entries(buildConfigurations)) { - configurations[key] = getServerOptions(options); - } - } - - const sourceRoot = clientProject.sourceRoot ?? join(normalize(clientProject.root), 'src'); - const serverTsConfig = join(tsConfigDirectory, 'tsconfig.server.json'); - clientProject.targets.add({ - name: 'server', - builder: Builders.Server, - defaultConfiguration: 'production', - options: { - outputPath: `dist/${options.project}/server`, - main: join(normalize(sourceRoot), serverMainEntryName), - tsConfig: serverTsConfig, - ...(buildTarget?.options ? getServerOptions(buildTarget?.options) : {}), - }, - configurations, - }); - } - }); -} - function updateConfigFileApplicationBuilder(options: ServerOptions): Rule { return updateWorkspace((workspace) => { const project = workspace.projects.get(options.project); @@ -108,6 +48,7 @@ function updateConfigFileApplicationBuilder(options: ServerOptions): Rule { } buildTarget.options ??= {}; + buildTarget.options.outputMode = 'static'; buildTarget.options['server'] = posix.join( project.sourceRoot ?? posix.join(project.root, 'src'), serverMainEntryName, @@ -169,11 +110,16 @@ export default function (options: ServerOptions): Rule { throw targetBuildNotFoundError(); } - const isUsingApplicationBuilder = clientBuildTarget.builder === Builders.Application; if ( - clientProject.targets.has('server') || - (isUsingApplicationBuilder && clientBuildTarget.options?.server !== undefined) + clientBuildTarget?.builder !== Builders.Application && + clientBuildTarget?.builder !== Builders.BuildApplication ) { + throw new SchematicsException( + `Ssr schematic requires the project to use "${Builders.Application}" or "${Builders.BuildApplication}" as the build builder.`, + ); + } + + if (clientBuildTarget.options?.server) { // Server has already been added. return; } @@ -190,33 +136,10 @@ export default function (options: ServerOptions): Rule { move(join(normalize(clientProject.root), 'src')), ]); - const clientTsConfig = normalize(clientBuildOptions.tsConfig); - const tsConfigExtends = basename(clientTsConfig); - const tsConfigDirectory = dirname(clientTsConfig); - return chain([ mergeWith(templateSource), - ...(isUsingApplicationBuilder - ? [ - updateConfigFileApplicationBuilder(options), - updateTsConfigFile(clientBuildOptions.tsConfig), - ] - : [ - mergeWith( - apply(url('./files/root'), [ - applyTemplates({ - ...strings, - ...options, - stripTsExtension: (s: string) => s.replace(/\.ts$/, ''), - tsConfigExtends, - hasLocalizePackage: !!getPackageJsonDependency(host, '@angular/localize'), - relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(tsConfigDirectory), - }), - move(tsConfigDirectory), - ]), - ), - updateConfigFileBrowserBuilder(options, tsConfigDirectory), - ]), + updateConfigFileApplicationBuilder(options), + updateTsConfigFile(clientBuildOptions.tsConfig), addDependencies(options.skipInstall), addRootProvider( options.project, diff --git a/packages/schematics/angular/server/index_spec.ts b/packages/schematics/angular/server/index_spec.ts index 130d4bf05cd3..93cad3b8bf49 100644 --- a/packages/schematics/angular/server/index_spec.ts +++ b/packages/schematics/angular/server/index_spec.ts @@ -9,9 +9,6 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { parse as parseJson } from 'jsonc-parser'; import { Schema as ApplicationOptions, Style } from '../application/schema'; -import { CompilerOptions } from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; -import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies'; -import { Builders } from '../utility/workspace-models'; import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as ServerOptions } from './schema'; @@ -141,105 +138,4 @@ describe('Server Schematic', () => { expect(contents).toContain(`provideClientHydration(withEventReplay())`); }); }); - - describe('Legacy browser builder', () => { - function convertBuilderToLegacyBrowser(): void { - const config = JSON.parse(appTree.readContent('/angular.json')); - const build = config.projects.bar.architect.build; - - build.builder = Builders.Browser; - build.options = { - ...build.options, - main: build.options.browser, - browser: undefined, - }; - - build.configurations.development = { - ...build.configurations.development, - vendorChunk: true, - namedChunks: true, - buildOptimizer: false, - }; - - appTree.overwrite('/angular.json', JSON.stringify(config, undefined, 2)); - } - - beforeEach(async () => { - appTree = await schematicRunner.runSchematic('application', appOptions, appTree); - convertBuilderToLegacyBrowser(); - }); - - it(`should not add import to '@angular/localize' as type in 'tsconfig.server.json' when it's not a dependency`, async () => { - const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree); - const { compilerOptions } = tree.readJson('/projects/bar/tsconfig.server.json') as { - compilerOptions: CompilerOptions; - }; - expect(compilerOptions.types).not.toContain('@angular/localize'); - }); - - it(`should add import to '@angular/localize' as type in 'tsconfig.server.json' when it's a dependency`, async () => { - addPackageJsonDependency(appTree, { - name: '@angular/localize', - type: NodeDependencyType.Default, - version: 'latest', - }); - const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree); - const { compilerOptions } = tree.readJson('/projects/bar/tsconfig.server.json') as { - compilerOptions: CompilerOptions; - }; - expect(compilerOptions.types).toContain('@angular/localize'); - }); - - it('should update workspace with a server target', async () => { - const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree); - const filePath = '/angular.json'; - const contents = tree.readContent(filePath); - const config = JSON.parse(contents.toString()); - const targets = config.projects.bar.architect; - expect(targets.server).toBeDefined(); - expect(targets.server.builder).toBeDefined(); - const opts = targets.server.options; - expect(opts.outputPath).toEqual('dist/bar/server'); - expect(opts.main).toEqual('projects/bar/src/main.server.ts'); - expect(opts.tsConfig).toEqual('projects/bar/tsconfig.server.json'); - }); - - it('should update workspace with a build target outputPath', async () => { - const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree); - const filePath = '/angular.json'; - const contents = tree.readContent(filePath); - const config = JSON.parse(contents.toString()); - const targets = config.projects.bar.architect; - expect(targets.build.options.outputPath).toEqual('dist/bar/browser'); - }); - - it(`should work when 'tsconfig.app.json' has comments`, async () => { - const appTsConfigPath = '/projects/bar/tsconfig.app.json'; - const appTsConfigContent = appTree.readContent(appTsConfigPath); - appTree.overwrite(appTsConfigPath, '// comment in json file\n' + appTsConfigContent); - - const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree); - const filePath = '/projects/bar/tsconfig.server.json'; - expect(tree.exists(filePath)).toBeTrue(); - }); - - it('should create a tsconfig file for a generated application', async () => { - const tree = await schematicRunner.runSchematic('server', defaultOptions, appTree); - const filePath = '/projects/bar/tsconfig.server.json'; - expect(tree.exists(filePath)).toBeTrue(); - const contents = parseJson(tree.readContent(filePath).toString()); - expect(contents).toEqual({ - extends: './tsconfig.app.json', - compilerOptions: { - outDir: '../../out-tsc/server', - types: ['node'], - }, - files: ['src/main.server.ts'], - }); - const angularConfig = JSON.parse(tree.readContent('angular.json')); - expect(angularConfig.projects.bar.architect.server.options.tsConfig).toEqual( - 'projects/bar/tsconfig.server.json', - ); - }); - }); }); diff --git a/packages/schematics/angular/ssr/files/application-builder/server.ts.template b/packages/schematics/angular/ssr/files/application-builder/server.ts.template deleted file mode 100644 index b8f7e04fb7f2..000000000000 --- a/packages/schematics/angular/ssr/files/application-builder/server.ts.template +++ /dev/null @@ -1,57 +0,0 @@ -import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '@angular/ssr/node'; -import express from 'express'; -import { fileURLToPath } from 'node:url'; -import { dirname, join, resolve } from 'node:path'; -import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './src/main.server'; - -// The Express app is exported so that it can be used by serverless Functions. -export function app(): express.Express { - const server = express(); - const serverDistFolder = dirname(fileURLToPath(import.meta.url)); - const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>'); - const indexHtml = join(serverDistFolder, 'index.server.html'); - - const commonEngine = new CommonEngine(); - - server.set('view engine', 'html'); - server.set('views', browserDistFolder); - - // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); - // Serve static files from /<%= browserDistDirectory %> - server.get('**', express.static(browserDistFolder, { - maxAge: '1y', - index: 'index.html', - })); - - // All regular routes use the Angular engine - server.get('**', (req, res, next) => { - const { protocol, originalUrl, baseUrl, headers } = req; - - commonEngine - .render({ - <% if (isStandalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule<% } %>, - documentFilePath: indexHtml, - url: `${protocol}://${headers.host}${originalUrl}`, - publicPath: browserDistFolder, - providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], - }) - .then((html) => res.send(html)) - .catch((err) => next(err)); - }); - - return server; -} - -function run(): void { - const port = process.env['PORT'] || 4000; - - // Start up the Node server - const server = app(); - server.listen(port, () => { - console.log(`Node Express server listening on http://localhost:${port}`); - }); -} - -run(); diff --git a/packages/schematics/angular/ssr/files/server-builder/server.ts.template b/packages/schematics/angular/ssr/files/server-builder/server.ts.template deleted file mode 100644 index 8cfcc0e4638b..000000000000 --- a/packages/schematics/angular/ssr/files/server-builder/server.ts.template +++ /dev/null @@ -1,69 +0,0 @@ -import 'zone.js/node'; - -import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '@angular/ssr/node'; -import * as express from 'express'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; -import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './src/main.server'; - -// The Express app is exported so that it can be used by serverless Functions. -export function app(): express.Express { - const server = express(); - const distFolder = join(process.cwd(), '<%= browserDistDirectory %>'); - const indexHtml = existsSync(join(distFolder, 'index.original.html')) - ? join(distFolder, 'index.original.html') - : join(distFolder, 'index.html'); - - const commonEngine = new CommonEngine(); - - server.set('view engine', 'html'); - server.set('views', distFolder); - - // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); - // Serve static files from /browser - server.get('*.*', express.static(distFolder, { - maxAge: '1y' - })); - - // All regular routes use the Angular engine - server.get('*', (req, res, next) => { - const { protocol, originalUrl, baseUrl, headers } = req; - - commonEngine - .render({ - <% if (isStandalone) { %>bootstrap<% } else { %>bootstrap: AppServerModule<% } %>, - documentFilePath: indexHtml, - url: `${protocol}://${headers.host}${originalUrl}`, - publicPath: distFolder, - providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], - }) - .then((html) => res.send(html)) - .catch((err) => next(err)); - }); - - return server; -} - -function run(): void { - const port = process.env['PORT'] || 4000; - - // Start up the Node server - const server = app(); - server.listen(port, () => { - console.log(`Node Express server listening on http://localhost:${port}`); - }); -} - -// Webpack will replace 'require' with '__webpack_require__' -// '__non_webpack_require__' is a proxy to Node 'require' -// The below code is to ensure that the server is run only when not requiring the bundle. -declare const __non_webpack_require__: NodeRequire; -const mainModule = __non_webpack_require__.main; -const moduleFilename = mainModule && mainModule.filename || ''; -if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { - run(); -} - -export default <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %>; diff --git a/packages/schematics/angular/ssr/files/server.ts.template b/packages/schematics/angular/ssr/files/server.ts.template new file mode 100644 index 000000000000..010804ead0f6 --- /dev/null +++ b/packages/schematics/angular/ssr/files/server.ts.template @@ -0,0 +1,49 @@ +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; +import express from 'express'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const serverDistFolder = dirname(fileURLToPath(import.meta.url)); +const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>'); + +const app = express(); +const angularApp = new AngularNodeAppEngine(); + +// Example Express Rest API endpoints +// app.get('/api/**', (req, res) => { }); + +// Serve static files from /<%= browserDistDirectory %> +app.get( + '**', + express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + setHeaders: (res) => { + const headers = angularApp.getPrerenderHeaders(res.req); + for (const [key, value] of headers) { + res.setHeader(key, value); + } + }, + }), +); + +app.get('**', (req, res, next) => { + angularApp + .render(req) + .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) + .catch(next); +}); + +if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || 4000; + app.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +export default createNodeRequestHandler(app); diff --git a/packages/schematics/angular/ssr/index.ts b/packages/schematics/angular/ssr/index.ts index 19569c7775db..1d374ff7dd6d 100644 --- a/packages/schematics/angular/ssr/index.ts +++ b/packages/schematics/angular/ssr/index.ts @@ -31,43 +31,16 @@ import { } from '../utility'; import { JSONFile } from '../utility/json-file'; import { latestVersions } from '../utility/latest-versions'; -import { isStandaloneApp } from '../utility/ng-ast-utils'; import { targetBuildNotFoundError } from '../utility/project-targets'; -import { getMainFilePath } from '../utility/standalone/util'; -import { ProjectDefinition, getWorkspace } from '../utility/workspace'; +import { getWorkspace } from '../utility/workspace'; import { Builders } from '../utility/workspace-models'; import { Schema as SSROptions } from './schema'; -const SERVE_SSR_TARGET_NAME = 'serve-ssr'; -const PRERENDER_TARGET_NAME = 'prerender'; const DEFAULT_BROWSER_DIR = 'browser'; const DEFAULT_MEDIA_DIR = 'media'; const DEFAULT_SERVER_DIR = 'server'; -async function getLegacyOutputPaths( - host: Tree, - projectName: string, - target: 'server' | 'build', -): Promise { - // Generate new output paths - const workspace = await readWorkspace(host); - const project = workspace.projects.get(projectName); - const architectTarget = project?.targets.get(target); - if (!architectTarget?.options) { - throw new SchematicsException(`Cannot find 'options' for ${projectName} ${target} target.`); - } - - const { outputPath } = architectTarget.options; - if (typeof outputPath !== 'string') { - throw new SchematicsException( - `outputPath for ${projectName} ${target} target is not a string.`, - ); - } - - return outputPath; -} - async function getApplicationBuilderOutputPaths( host: Tree, projectName: string, @@ -114,7 +87,7 @@ async function getApplicationBuilderOutputPaths( }; } -function addScriptsRule({ project }: SSROptions, isUsingApplicationBuilder: boolean): Rule { +function addScriptsRule({ project }: SSROptions): Rule { return async (host) => { const pkgPath = '/package.json'; const pkg = host.readJson(pkgPath) as { scripts?: Record } | null; @@ -122,51 +95,26 @@ function addScriptsRule({ project }: SSROptions, isUsingApplicationBuilder: bool throw new SchematicsException('Could not find package.json'); } - if (isUsingApplicationBuilder) { - const { base, server } = await getApplicationBuilderOutputPaths(host, project); - pkg.scripts ??= {}; - pkg.scripts[`serve:ssr:${project}`] = `node ${posix.join(base, server)}/server.mjs`; - } else { - const serverDist = await getLegacyOutputPaths(host, project, 'server'); - pkg.scripts = { - ...pkg.scripts, - 'dev:ssr': `ng run ${project}:${SERVE_SSR_TARGET_NAME}`, - 'serve:ssr': `node ${serverDist}/main.js`, - 'build:ssr': `ng build && ng run ${project}:server`, - 'prerender': `ng run ${project}:${PRERENDER_TARGET_NAME}`, - }; - } + const { base, server } = await getApplicationBuilderOutputPaths(host, project); + pkg.scripts ??= {}; + pkg.scripts[`serve:ssr:${project}`] = `node ${posix.join(base, server)}/server.mjs`; host.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); }; } -function updateApplicationBuilderTsConfigRule(options: SSROptions): Rule { - return async (host) => { - const workspace = await readWorkspace(host); - const project = workspace.projects.get(options.project); - const buildTarget = project?.targets.get('build'); - if (!buildTarget || !buildTarget.options) { - return; - } - - const tsConfigPath = buildTarget.options.tsConfig; - if (!tsConfigPath || typeof tsConfigPath !== 'string') { - // No tsconfig path - return; - } - - const tsConfig = new JSONFile(host, tsConfigPath); - const filesAstNode = tsConfig.get(['files']); - const serverFilePath = 'server.ts'; - if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) { - tsConfig.modify(['files'], [...filesAstNode, serverFilePath]); - } +function updateTsConfigFile(tsConfigPath: string): Rule { + return (host: Tree) => { + const json = new JSONFile(host, tsConfigPath); + const filesPath = ['files']; + const files = new Set((json.get(filesPath) as string[] | undefined) ?? []); + files.add('src/server.ts'); + json.modify(filesPath, [...files]); }; } function updateApplicationBuilderWorkspaceConfigRule( - projectRoot: string, + projectSourceRoot: string, options: SSROptions, { logger }: SchematicContext, ): Rule { @@ -202,125 +150,15 @@ function updateApplicationBuilderWorkspaceConfigRule( buildTarget.options = { ...buildTarget.options, outputPath, - prerender: true, + outputMode: 'server', ssr: { - entry: join(normalize(projectRoot), 'server.ts'), + entry: join(normalize(projectSourceRoot), 'server.ts'), }, }; }); } -function updateWebpackBuilderWorkspaceConfigRule(options: SSROptions): Rule { - return updateWorkspace((workspace) => { - const projectName = options.project; - const project = workspace.projects.get(projectName); - if (!project) { - return; - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const serverTarget = project.targets.get('server')!; - (serverTarget.options ??= {}).main = join(normalize(project.root), 'server.ts'); - - const serveSSRTarget = project.targets.get(SERVE_SSR_TARGET_NAME); - if (serveSSRTarget) { - return; - } - - project.targets.add({ - name: SERVE_SSR_TARGET_NAME, - builder: '@angular-devkit/build-angular:ssr-dev-server', - defaultConfiguration: 'development', - options: {}, - configurations: { - development: { - browserTarget: `${projectName}:build:development`, - serverTarget: `${projectName}:server:development`, - }, - production: { - browserTarget: `${projectName}:build:production`, - serverTarget: `${projectName}:server:production`, - }, - }, - }); - - const prerenderTarget = project.targets.get(PRERENDER_TARGET_NAME); - if (prerenderTarget) { - return; - } - - project.targets.add({ - name: PRERENDER_TARGET_NAME, - builder: '@angular-devkit/build-angular:prerender', - defaultConfiguration: 'production', - options: { - routes: ['/'], - }, - configurations: { - production: { - browserTarget: `${projectName}:build:production`, - serverTarget: `${projectName}:server:production`, - }, - development: { - browserTarget: `${projectName}:build:development`, - serverTarget: `${projectName}:server:development`, - }, - }, - }); - }); -} - -function updateWebpackBuilderServerTsConfigRule(options: SSROptions): Rule { - return async (host) => { - const workspace = await readWorkspace(host); - const project = workspace.projects.get(options.project); - const serverTarget = project?.targets.get('server'); - if (!serverTarget || !serverTarget.options) { - return; - } - - const tsConfigPath = serverTarget.options.tsConfig; - if (!tsConfigPath || typeof tsConfigPath !== 'string') { - // No tsconfig path - return; - } - - const tsConfig = new JSONFile(host, tsConfigPath); - const filesAstNode = tsConfig.get(['files']); - const serverFilePath = 'server.ts'; - if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) { - tsConfig.modify(['files'], [...filesAstNode, serverFilePath]); - } - }; -} - -function addDependencies({ skipInstall }: SSROptions, isUsingApplicationBuilder: boolean): Rule { - const install = skipInstall ? InstallBehavior.None : InstallBehavior.Auto; - - const rules: Rule[] = [ - addDependency('express', latestVersions['express'], { - type: DependencyType.Default, - install, - }), - addDependency('@types/express', latestVersions['@types/express'], { - type: DependencyType.Dev, - install, - }), - ]; - - if (!isUsingApplicationBuilder) { - rules.push( - addDependency('browser-sync', latestVersions['browser-sync'], { - type: DependencyType.Dev, - install, - }), - ); - } - - return chain(rules); -} - -function addServerFile(options: ServerOptions, isStandalone: boolean): Rule { +function addServerFile(options: ServerOptions): Rule { return async (host) => { const projectName = options.project; const workspace = await readWorkspace(host); @@ -328,67 +166,68 @@ function addServerFile(options: ServerOptions, isStandalone: boolean): Rule { if (!project) { throw new SchematicsException(`Invalid project name (${projectName})`); } - const isUsingApplicationBuilder = usingApplicationBuilder(project); - const browserDistDirectory = isUsingApplicationBuilder - ? (await getApplicationBuilderOutputPaths(host, projectName)).browser - : await getLegacyOutputPaths(host, projectName, 'build'); + const buildTarget = project.targets.get('build'); + if ( + buildTarget?.builder !== Builders.Application && + buildTarget?.builder !== Builders.BuildApplication + ) { + throw new SchematicsException( + `Ssr schematic requires the project to use "${Builders.Application}" or "${Builders.BuildApplication}" as the build builder.`, + ); + } + + const browserDistDirectory = (await getApplicationBuilderOutputPaths(host, projectName)) + .browser; return mergeWith( - apply( - url(`./files/${isUsingApplicationBuilder ? 'application-builder' : 'server-builder'}`), - [ - applyTemplates({ - ...strings, - ...options, - browserDistDirectory, - isStandalone, - }), - move(project.root), - ], - ), + apply(url('./files/'), [ + applyTemplates({ + ...strings, + ...options, + browserDistDirectory, + }), + move(project.sourceRoot ?? posix.join(project.root, 'src')), + ]), ); }; } export default function (options: SSROptions): Rule { return async (host, context) => { - const browserEntryPoint = await getMainFilePath(host, options.project); - const isStandalone = isStandaloneApp(host, browserEntryPoint); - const workspace = await getWorkspace(host); const clientProject = workspace.projects.get(options.project); if (!clientProject) { throw targetBuildNotFoundError(); } + const install = options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto; - const isUsingApplicationBuilder = usingApplicationBuilder(clientProject); + const tsConfigPath = clientProject?.targets.get('build')?.options?.tsConfig; + if (!tsConfigPath || typeof tsConfigPath !== 'string') { + throw new SchematicsException(`'tsConfig' builder option must be defined as a string.`); + } return chain([ schematic('server', { ...options, skipInstall: true, }), - ...(isUsingApplicationBuilder - ? [ - updateApplicationBuilderWorkspaceConfigRule(clientProject.root, options, context), - updateApplicationBuilderTsConfigRule(options), - ] - : [ - updateWebpackBuilderServerTsConfigRule(options), - updateWebpackBuilderWorkspaceConfigRule(options), - ]), - addServerFile(options, isStandalone), - addScriptsRule(options, isUsingApplicationBuilder), - addDependencies(options, isUsingApplicationBuilder), + updateApplicationBuilderWorkspaceConfigRule( + clientProject.sourceRoot ?? posix.join(clientProject.root, 'src'), + options, + context, + ), + updateTsConfigFile(tsConfigPath), + addServerFile(options), + addScriptsRule(options), + addDependency('express', latestVersions['express'], { + type: DependencyType.Default, + install, + }), + addDependency('@types/express', latestVersions['@types/express'], { + type: DependencyType.Dev, + install, + }), ]); }; } - -function usingApplicationBuilder(project: ProjectDefinition) { - const buildBuilder = project.targets.get('build')?.builder; - const isUsingApplicationBuilder = - buildBuilder === Builders.Application || buildBuilder === Builders.BuildApplication; - - return isUsingApplicationBuilder; -} diff --git a/packages/schematics/angular/ssr/index_spec.ts b/packages/schematics/angular/ssr/index_spec.ts index b88b767d1d79..acdbcbe0fcef 100644 --- a/packages/schematics/angular/ssr/index_spec.ts +++ b/packages/schematics/angular/ssr/index_spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import { tags } from '@angular-devkit/core'; import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { join } from 'node:path'; @@ -77,26 +76,7 @@ describe('SSR Schematic', () => { files: string[]; }; - expect(files).toEqual(['src/main.ts', 'src/main.server.ts', 'server.ts']); - }); - - it(`should import 'AppServerModule' from 'main.server.ts'`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const filePath = '/projects/test-app/server.ts'; - const content = tree.readContent(filePath); - expect(content).toContain(`import AppServerModule from './src/main.server';`); - }); - - it(`should pass 'AppServerModule' in the bootstrap parameter.`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const filePath = '/projects/test-app/server.ts'; - const content = tree.readContent(filePath); - expect(tags.oneLine`${content}`).toContain(tags.oneLine` - .render({ - bootstrap: AppServerModule, - `); + expect(files).toEqual(['src/main.ts', 'src/main.server.ts', 'src/server.ts']); }); }); @@ -118,25 +98,6 @@ describe('SSR Schematic', () => { ); }); - it(`should add default import to 'main.server.ts'`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const filePath = '/projects/test-app/server.ts'; - const content = tree.readContent(filePath); - expect(content).toContain(`import bootstrap from './src/main.server';`); - }); - - it(`should pass 'AppServerModule' in the bootstrap parameter.`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const filePath = '/projects/test-app/server.ts'; - const content = tree.readContent(filePath); - expect(tags.oneLine`${content}`).toContain(tags.oneLine` - .render({ - bootstrap, - `); - }); - it('should add script section in package.json', async () => { const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); const { scripts } = tree.readJson('/package.json') as { scripts: Record }; @@ -161,7 +122,7 @@ describe('SSR Schematic', () => { const { scripts } = tree.readJson('/package.json') as { scripts: Record }; expect(scripts['serve:ssr:test-app']).toBe(`node dist/test-app/node-server/server.mjs`); - const serverFileContent = tree.readContent('/projects/test-app/server.ts'); + const serverFileContent = tree.readContent('/projects/test-app/src/server.ts'); expect(serverFileContent).toContain(`resolve(serverDistFolder, '../public')`); }); @@ -190,70 +151,4 @@ describe('SSR Schematic', () => { }); }); }); - - describe('Legacy browser builder', () => { - function convertBuilderToLegacyBrowser(): void { - const config = JSON.parse(appTree.readContent('/angular.json')); - const build = config.projects['test-app'].architect.build; - - build.builder = '@angular-devkit/build-angular:browser'; - build.options = { - ...build.options, - main: build.options.browser, - browser: undefined, - }; - - build.configurations.development = { - ...build.configurations.development, - vendorChunk: true, - namedChunks: true, - buildOptimizer: false, - }; - - appTree.overwrite('/angular.json', JSON.stringify(config, undefined, 2)); - } - - beforeEach(async () => { - appTree = await schematicRunner.runExternalSchematic( - '@schematics/angular', - 'application', - { - name: 'test-app', - inlineStyle: false, - inlineTemplate: false, - routing: false, - style: 'css', - skipTests: false, - standalone: false, - }, - appTree, - ); - - convertBuilderToLegacyBrowser(); - }); - - it(`should update 'tsconfig.server.json' files with Express main file`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const { files } = tree.readJson('/projects/test-app/tsconfig.server.json') as { - files: string[]; - }; - - expect(files).toEqual(['src/main.server.ts', 'server.ts']); - }); - - it(`should add export to main file in 'server.ts'`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const content = tree.readContent('/projects/test-app/server.ts'); - expect(content).toContain(`export default AppServerModule`); - }); - - it(`should add correct value to 'distFolder'`, async () => { - const tree = await schematicRunner.runSchematic('ssr', defaultOptions, appTree); - - const content = tree.readContent('/projects/test-app/server.ts'); - expect(content).toContain(`const distFolder = join(process.cwd(), 'dist/test-app/browser');`); - }); - }); }); diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl index 42433f67edbc..1c09c59d0cd9 100644 --- a/tests/legacy-cli/e2e.bzl +++ b/tests/legacy-cli/e2e.bzl @@ -42,10 +42,13 @@ ESBUILD_TESTS = [ WEBPACK_IGNORE_TESTS = [ "tests/vite/**", - "tests/server-rendering/server-routes-*", + "tests/build/app-shell/**", + "tests/i18n/ivy-localize-app-shell.js", + "tests/i18n/ivy-localize-app-shell-service-worker.js", + "tests/i18n/ivy-localize-ssr.js", + "tests/server-rendering/**", + "tests/build/prerender/**", "tests/commands/serve/ssr-http-requests-assets.js", - "tests/build/prerender/http-requests-assets.js", - "tests/build/prerender/error-with-sourcemaps.js", "tests/build/wasm-esm.js", ] diff --git a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts index 336b673cf2a3..b1b6cfab499d 100644 --- a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-ngmodule.ts @@ -8,28 +8,6 @@ const snapshots = require('../../../ng-snapshot/package.json'); export default async function () { await ng('generate', 'app', 'test-project-two', '--routing', '--no-standalone', '--skip-install'); - - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; - - // Setup webpack builder if esbuild is not requested on the commandline - await updateJsonFile('angular.json', (json) => { - const build = json['projects']['test-project-two']['architect']['build']; - if (useWebpackBuilder) { - build.builder = '@angular-devkit/build-angular:browser'; - build.options = { - ...build.options, - main: build.options.browser, - browser: undefined, - }; - - build.configurations.development = { - ...build.configurations.development, - vendorChunk: true, - namedChunks: true, - }; - } - }); - await ng('generate', 'app-shell', '--project', 'test-project-two'); const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; @@ -52,13 +30,6 @@ export default async function () { } } - if (useWebpackBuilder) { - await ng('run', 'test-project-two:app-shell:development'); - await expectFileToMatch('dist/test-project-two/browser/index.html', 'app-shell works!'); - await ng('run', 'test-project-two:app-shell'); - await expectFileToMatch('dist/test-project-two/browser/index.html', 'app-shell works!'); - } else { - await ng('build', 'test-project-two'); - await expectFileToMatch('dist/test-project-two/browser/index.html', 'app-shell works!'); - } + await ng('build', 'test-project-two'); + await expectFileToMatch('dist/test-project-two/browser/index.html', 'app-shell works!'); } diff --git a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-schematic.ts b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-schematic.ts index b166e2c7d8d1..bf0b683f05d1 100644 --- a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-schematic.ts +++ b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-schematic.ts @@ -9,8 +9,6 @@ const snapshots = require('../../../ng-snapshot/package.json'); export default async function () { await appendToFile('src/app/app.component.html', ''); await ng('generate', 'app-shell', '--project', 'test-project'); - // Setup webpack builder if esbuild is not requested on the commandline - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; if (isSnapshotBuild) { @@ -31,15 +29,6 @@ export default async function () { await installPackage(pkg); } } - - if (useWebpackBuilder) { - await ng('run', 'test-project:app-shell:development'); - await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/); - - await ng('run', 'test-project:app-shell'); - await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/); - } else { - await ng('build'); - await expectFileToMatch('dist/test-project/browser/index.html', 'app-shell works!'); - } + await ng('build'); + await expectFileToMatch('dist/test-project/browser/index.html', 'app-shell works!'); } diff --git a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-service-worker.ts b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-service-worker.ts index 9577ea3995ae..5136b53bf9f5 100644 --- a/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-service-worker.ts +++ b/tests/legacy-cli/e2e/tests/build/app-shell/app-shell-with-service-worker.ts @@ -7,8 +7,6 @@ import { updateJsonFile } from '../../../utils/project'; const snapshots = require('../../../ng-snapshot/package.json'); export default async function () { - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; - await appendToFile('src/app/app.component.html', ''); await ng('generate', 'service-worker', '--project', 'test-project'); await ng('generate', 'app-shell', '--project', 'test-project'); @@ -51,11 +49,7 @@ export default async function () { `, ); - if (useWebpackBuilder) { - await ng('run', 'test-project:app-shell:production'); - } else { - await ng('build'); - } + await ng('build'); await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/); await ng('e2e', '--configuration=production'); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts index f6ed9fc1c080..5c25e41fa120 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts @@ -8,7 +8,6 @@ import { readNgVersion } from '../../utils/version'; const snapshots = require('../../ng-snapshot/package.json'); export default async function () { - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; await updateJsonFile('package.json', (packageJson) => { @@ -48,11 +47,6 @@ export default async function () { // Enable localization for all locales buildOptions.localize = true; - if (useWebpackBuilder) { - const serverOptions = appArchitect['server'].options; - serverOptions.localize = true; - serverOptions.outputHashing = 'none'; - } // Add locale definitions to the project const i18n: Record = (appProject.i18n = { locales: {} }); @@ -80,11 +74,7 @@ export default async function () { } // Build each locale and verify the SW output. - if (useWebpackBuilder) { - await ng('run', 'test-project:app-shell:development'); - } else { - await ng('build', '--output-hashing=none'); - } + await ng('build', '--output-hashing=none'); for (const { lang } of langTranslations) { await Promise.all([ diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts index dc28d9c0a78a..204261aef0dc 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts @@ -14,7 +14,6 @@ import { readNgVersion } from '../../utils/version'; const snapshots = require('../../ng-snapshot/package.json'); export default async function () { - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; await updateJsonFile('package.json', (packageJson) => { @@ -50,10 +49,6 @@ export default async function () { // Enable localization for all locales buildOptions.localize = true; - if (useWebpackBuilder) { - const serverOptions = appArchitect['server'].options; - serverOptions.localize = true; - } // Add locale definitions to the project const i18n: Record = (appProject.i18n = { locales: {} }); @@ -105,11 +100,8 @@ export default async function () { } // Build each locale and verify the output. - if (useWebpackBuilder) { - await ng('run', 'test-project:app-shell'); - } else { - await ng('build'); - } + await ng('build', '--output-mode=static'); + for (const { lang, translation } of langTranslations) { await expectFileToMatch(`dist/test-project/browser/${lang}/index.html`, translation); } diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-ssr.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-ssr.ts index 1977c2e3eae1..dd0c75ae74fc 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-ssr.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-ssr.ts @@ -4,7 +4,6 @@ import { installWorkspacePackages, uninstallPackage } from '../../utils/packages import { ng } from '../../utils/process'; import { updateJsonFile, useSha } from '../../utils/project'; import { langTranslations, setupI18nConfig } from './setup'; -import { expectFileToMatch } from '../../utils/fs'; export default async function () { const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; diff --git a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts b/tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts index a1958399d490..c5fa22680e88 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts @@ -1,4 +1,3 @@ -import { getGlobalVariable } from '../../utils/env'; import { rimraf, writeMultipleFiles } from '../../utils/fs'; import { findFreePort } from '../../utils/network'; import { installWorkspacePackages } from '../../utils/packages'; @@ -6,8 +5,6 @@ import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; import { updateJsonFile, useSha } from '../../utils/project'; export default async function () { - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; - // forcibly remove in case another test doesn't clean itself up await rimraf('node_modules/@angular/ssr'); await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); @@ -15,15 +12,67 @@ export default async function () { await useSha(); await installWorkspacePackages(); - if (!useWebpackBuilder) { - // Disable prerendering - await updateJsonFile('angular.json', (json) => { - const build = json['projects']['test-project']['architect']['build']; - build.configurations.production.prerender = false; - }); - } + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project']['architect']['build']; + build.options.outputMode = undefined; + build.configurations.production.prerender = false; + }); await writeMultipleFiles({ + 'src/server.ts': ` + import { APP_BASE_HREF } from '@angular/common'; + import { CommonEngine } from '@angular/ssr/node'; + import express from 'express'; + import { fileURLToPath } from 'node:url'; + import { dirname, join, resolve } from 'node:path'; + import bootstrap from './src/main.server'; + + // The Express app is exported so that it can be used by serverless Functions. + export function app(): express.Express { + const server = express(); + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', browserDistFolder); + + server.get('**', express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + })); + + // All regular routes use the Angular engine + server.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; + } + + function run(): void { + const port = process.env['PORT'] || 4000; + const server = app(); + server.listen(port, () => { + console.log(\`Node Express server listening on http://localhost:\${port}\`); + }); + } + + run(); + `, 'src/app/app.component.css': `div { color: #000 }`, 'src/styles.css': `* { color: #000 }`, 'src/main.ts': `import { bootstrapApplication } from '@angular/platform-browser'; @@ -46,8 +95,7 @@ export default async function () { `, - 'e2e/src/app.e2e-spec.ts': - ` + 'e2e/src/app.e2e-spec.ts': ` import { browser, by, element } from 'protractor'; import * as webdriver from 'selenium-webdriver'; @@ -99,10 +147,8 @@ export default async function () { // Make sure there were no client side errors. await verifyNoBrowserErrors(); - });` + - // TODO(alanagius): enable the below tests once critical css inlining for SSR is supported with Vite. - (useWebpackBuilder - ? ` + }); + it('stylesheets should be configured to load asynchronously', async () => { // Load the page without waiting for Angular since it is not bootstrapped automatically. await browser.driver.get(browser.baseUrl); @@ -117,9 +163,8 @@ export default async function () { // Make sure there were no client side errors. await verifyNoBrowserErrors(); - });` - : '') + - ` + }); + it('style tags all have a nonce attribute', async () => { // Load the page without waiting for Angular since it is not bootstrapped automatically. await browser.driver.get(browser.baseUrl); @@ -139,11 +184,9 @@ export default async function () { async function spawnServer(): Promise { const port = await findFreePort(); - const runCommand = useWebpackBuilder ? 'serve:ssr' : 'serve:ssr:test-project'; - await execAndWaitForOutputToMatch( 'npm', - ['run', runCommand], + ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { 'PORT': String(port), @@ -155,11 +198,6 @@ export default async function () { await ng('build'); - if (useWebpackBuilder) { - // Build server code - await ng('run', 'test-project:server'); - } - const port = await spawnServer(); await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); } diff --git a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts index 42c8d735d528..90c3632e0251 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts @@ -1,4 +1,3 @@ -import { getGlobalVariable } from '../../utils/env'; import { rimraf, writeMultipleFiles } from '../../utils/fs'; import { findFreePort } from '../../utils/network'; import { installWorkspacePackages } from '../../utils/packages'; @@ -16,27 +15,6 @@ export default async function () { await useCIChrome('test-project-two', 'projects/test-project-two/e2e/'); await useCIDefaults('test-project-two'); - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; - - if (useWebpackBuilder) { - await updateJsonFile('angular.json', (json) => { - const build = json['projects']['test-project-two']['architect']['build']; - build.builder = '@angular-devkit/build-angular:browser'; - build.options = { - ...build.options, - main: build.options.browser, - browser: undefined, - }; - - build.configurations.development = { - ...build.configurations.development, - vendorChunk: true, - namedChunks: true, - buildOptimizer: false, - }; - }); - } - await ng( 'add', '@angular/ssr', @@ -48,15 +26,68 @@ export default async function () { await useSha(); await installWorkspacePackages(); - if (!useWebpackBuilder) { - // Disable prerendering - await updateJsonFile('angular.json', (json) => { - const build = json['projects']['test-project-two']['architect']['build']; - build.configurations.production.prerender = false; - }); - } + // Disable prerendering + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project-two']['architect']['build']; + build.options.outputMode = undefined; + build.configurations.production.prerender = false; + }); await writeMultipleFiles({ + 'projects/test-project-two/src/server.ts': ` + import { APP_BASE_HREF } from '@angular/common'; + import { CommonEngine } from '@angular/ssr/node'; + import express from 'express'; + import { fileURLToPath } from 'node:url'; + import { dirname, join, resolve } from 'node:path'; + import bootstrap from './src/main.server'; + + // The Express app is exported so that it can be used by serverless Functions. + export function app(): express.Express { + const server = express(); + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', browserDistFolder); + + server.get('**', express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + })); + + // All regular routes use the Angular engine + server.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; + } + + function run(): void { + const port = process.env['PORT'] || 4000; + const server = app(); + server.listen(port, () => { + console.log(\`Node Express server listening on http://localhost:\${port}\`); + }); + } + + run(); + `, 'projects/test-project-two/src/app/app.component.css': `div { color: #000 }`, 'projects/test-project-two/src/styles.css': `* { color: #000 }`, 'projects/test-project-two/src/main.ts': ` @@ -69,8 +100,7 @@ export default async function () { .catch((err) => console.error(err)); }; `, - 'projects/test-project-two/e2e/src/app.e2e-spec.ts': - ` + 'projects/test-project-two/e2e/src/app.e2e-spec.ts': ` import { browser, by, element } from 'protractor'; import * as webdriver from 'selenium-webdriver'; @@ -120,35 +150,28 @@ export default async function () { // Make sure there were no client side errors. await verifyNoBrowserErrors(); - });` + - // TODO(alanagius): enable the below tests once critical css inlining for SSR is supported with Vite. - (useWebpackBuilder - ? ` - it('stylesheets should be configured to load asynchronously', async () => { - // Load the page without waiting for Angular since it is not bootstrapped automatically. - await browser.driver.get(browser.baseUrl); - - // Test the contents from the server. - const styleTag = await browser.driver.findElement(by.css('link[rel="stylesheet"]')); - expect(await styleTag.getAttribute('media')).toMatch('all'); - - // Make sure there were no client side errors. - await verifyNoBrowserErrors(); - });` - : '') + - ` }); + + it('stylesheets should be configured to load asynchronously', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents from the server. + const styleTag = await browser.driver.findElement(by.css('link[rel="stylesheet"]')); + expect(await styleTag.getAttribute('media')).toMatch('all'); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + }); `, }); async function spawnServer(): Promise { const port = await findFreePort(); - - const runCommand = useWebpackBuilder ? 'serve:ssr' : `serve:ssr:test-project-two`; - await execAndWaitForOutputToMatch( 'npm', - ['run', runCommand], + ['run', 'serve:ssr:test-project-two'], /Node Express server listening on/, { 'PORT': String(port), @@ -160,11 +183,6 @@ export default async function () { await ng('build', 'test-project-two'); - if (useWebpackBuilder) { - // Build server code - await ng('run', `test-project-two:server`); - } - const port = await spawnServer(); await ng( 'e2e', diff --git a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts b/tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts index 0922e8800a8e..167236545687 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts @@ -1,4 +1,3 @@ -import { getGlobalVariable } from '../../utils/env'; import { rimraf, writeMultipleFiles } from '../../utils/fs'; import { findFreePort } from '../../utils/network'; import { installWorkspacePackages } from '../../utils/packages'; @@ -11,19 +10,145 @@ export default async function () { await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; - if (!useWebpackBuilder) { - // Disable prerendering - await updateJsonFile('angular.json', (json) => { - const build = json['projects']['test-project']['architect']['build']; - build.configurations.production.prerender = false; - }); - } - await useSha(); await installWorkspacePackages(); + await updateJsonFile('angular.json', (json) => { + const build = json['projects']['test-project']['architect']['build']; + build.options.outputMode = undefined; + }); + await writeMultipleFiles({ + 'src/server.ts': ` + import { APP_BASE_HREF } from '@angular/common'; + import { CommonEngine } from '@angular/ssr/node'; + import express from 'express'; + import { fileURLToPath } from 'node:url'; + import { dirname, join, resolve } from 'node:path'; + import bootstrap from './src/main.server'; + + // The Express app is exported so that it can be used by serverless Functions. + export function app(): express.Express { + const server = express(); + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const browserDistFolder = resolve(serverDistFolder, '../browser'); + const indexHtml = join(serverDistFolder, 'index.server.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', browserDistFolder); + + server.get('**', express.static(browserDistFolder, { + maxAge: '1y', + index: 'index.html', + })); + + // All regular routes use the Angular engine + server.get('**', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, + publicPath: browserDistFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; + } + + function run(): void { + const port = process.env['PORT'] || 4000; + const server = app(); + server.listen(port, () => { + console.log(\`Node Express server listening on http://localhost:\${port}\`); + }); + } + + run(); + `, + 'projects/test-project-two/src/app/app.component.css': `div { color: #000 }`, + 'projects/test-project-two/src/styles.css': `* { color: #000 }`, + 'projects/test-project-two/src/main.ts': ` + import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + import { AppModule } from './app/app.module'; + + (window as any)['doBootstrap'] = () => { + platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); + }; + `, + 'projects/test-project-two/e2e/src/app.e2e-spec.ts': ` + import { browser, by, element } from 'protractor'; + import * as webdriver from 'selenium-webdriver'; + + function verifyNoBrowserErrors() { + return browser + .manage() + .logs() + .get('browser') + .then(function (browserLog: any[]) { + const errors: any[] = []; + browserLog.filter((logEntry) => { + const msg = logEntry.message; + console.log('>> ' + msg); + if (logEntry.level.value >= webdriver.logging.Level.INFO.value) { + errors.push(msg); + } + }); + expect(errors).toEqual([]); + }); + } + + describe('Hello world E2E Tests', () => { + beforeAll(async () => { + await browser.waitForAngularEnabled(false); + }); + + it('should display: Welcome', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + const style = await browser.driver.findElement(by.css('style[ng-app-id="ng"]')); + expect(await style.getText()).not.toBeNull(); + + // Test the contents from the server. + const serverDiv = await browser.driver.findElement(by.css('h1')); + expect(await serverDiv.getText()).toMatch('Hello'); + + // Bootstrap the client side app. + await browser.executeScript('doBootstrap()'); + + // Retest the contents after the client bootstraps. + expect(await element(by.css('h1')).getText()).toMatch('Hello'); + + // Make sure the server styles got replaced by client side ones. + expect(await element(by.css('style[ng-app-id="ng"]')).isPresent()).toBeFalsy(); + expect(await element(by.css('style')).getText()).toMatch(''); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + + it('stylesheets should be configured to load asynchronously', async () => { + // Load the page without waiting for Angular since it is not bootstrapped automatically. + await browser.driver.get(browser.baseUrl); + + // Test the contents from the server. + const styleTag = await browser.driver.findElement(by.css('link[rel="stylesheet"]')); + expect(await styleTag.getAttribute('media')).toMatch('all'); + + // Make sure there were no client side errors. + await verifyNoBrowserErrors(); + }); + });`, 'src/app/app.component.css': `div { color: #000 }`, 'src/styles.css': `* { color: #000 }`, 'src/main.ts': `import { bootstrapApplication } from '@angular/platform-browser'; @@ -34,8 +159,7 @@ export default async function () { bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); }; `, - 'e2e/src/app.e2e-spec.ts': - ` + 'e2e/src/app.e2e-spec.ts': ` import { browser, by, element } from 'protractor'; import * as webdriver from 'selenium-webdriver'; @@ -85,10 +209,8 @@ export default async function () { // Make sure there were no client side errors. await verifyNoBrowserErrors(); - }); ` + - // TODO(alanagius): enable the below tests once critical css inlining for SSR is supported with Vite. - (useWebpackBuilder - ? ` + }); + it('stylesheets should be configured to load asynchronously', async () => { // Load the page without waiting for Angular since it is not bootstrapped automatically. await browser.driver.get(browser.baseUrl); @@ -99,20 +221,17 @@ export default async function () { // Make sure there were no client side errors. await verifyNoBrowserErrors(); - });` - : '') + - ` + }); }); `, }); async function spawnServer(): Promise { const port = await findFreePort(); - const runCommand = useWebpackBuilder ? 'serve:ssr' : 'serve:ssr:test-project'; await execAndWaitForOutputToMatch( 'npm', - ['run', runCommand], + ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { 'PORT': String(port), @@ -123,10 +242,6 @@ export default async function () { } await ng('build'); - if (useWebpackBuilder) { - // Build server code - await ng('run', `test-project:server`); - } const port = await spawnServer(); await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); diff --git a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n-base-href.ts b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n-base-href.ts index e06e50b53444..1e0f3115de52 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n-base-href.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n-base-href.ts @@ -1,11 +1,19 @@ import { join } from 'node:path'; import assert from 'node:assert'; import { expectFileToMatch, writeFile } from '../../utils/fs'; -import { noSilentNg, silentNg } from '../../utils/process'; -import { setupProjectWithSSRAppEngine, spawnServer } from './setup'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../utils/process'; import { langTranslations, setupI18nConfig } from '../i18n/setup'; +import { findFreePort } from '../../utils/network'; +import { getGlobalVariable } from '../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { useSha } from '../../utils/project'; export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + if (process.version.startsWith('v18')) { // This is not supported in Node.js version 18 as global web crypto module is not available. return; @@ -13,7 +21,12 @@ export default async function () { // Setup project await setupI18nConfig(); - await setupProjectWithSSRAppEngine(); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); // Add routes await writeFile( @@ -47,7 +60,7 @@ export default async function () { ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: '', renderMode: RenderMode.Prerender, @@ -92,3 +105,17 @@ export default async function () { ); } } + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n.ts b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n.ts index 98c041bd65ba..9de524a42023 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n.ts @@ -1,11 +1,19 @@ import { join } from 'node:path'; import assert from 'node:assert'; import { expectFileToMatch, writeFile } from '../../utils/fs'; -import { noSilentNg, silentNg } from '../../utils/process'; -import { setupProjectWithSSRAppEngine, spawnServer } from './setup'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../utils/process'; import { langTranslations, setupI18nConfig } from '../i18n/setup'; +import { findFreePort } from '../../utils/network'; +import { getGlobalVariable } from '../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { useSha } from '../../utils/project'; export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + if (process.version.startsWith('v18')) { // This is not supported in Node.js version 18 as global web crypto module is not available. return; @@ -13,7 +21,12 @@ export default async function () { // Setup project await setupI18nConfig(); - await setupProjectWithSSRAppEngine(); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); // Add routes await writeFile( @@ -47,7 +60,7 @@ export default async function () { ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: '', renderMode: RenderMode.Prerender, @@ -104,3 +117,17 @@ export default async function () { } } } + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server.ts b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server.ts index bf6b36fa1c49..75521b324964 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server.ts @@ -2,17 +2,28 @@ import { join } from 'node:path'; import { existsSync } from 'node:fs'; import assert from 'node:assert'; import { expectFileToMatch, writeFile } from '../../utils/fs'; -import { noSilentNg, silentNg } from '../../utils/process'; -import { setupProjectWithSSRAppEngine, spawnServer } from './setup'; +import { execAndWaitForOutputToMatch, ng, noSilentNg, silentNg } from '../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; +import { findFreePort } from '../../utils/network'; export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + if (process.version.startsWith('v18')) { // This is not supported in Node.js version 18 as global web crypto module is not available. return; } - // Setup project - await setupProjectWithSSRAppEngine(); + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); // Add routes await writeFile( @@ -65,7 +76,7 @@ export default async function () { ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: 'ssg/:id', renderMode: RenderMode.Prerender, @@ -182,3 +193,17 @@ export default async function () { } } } + +async function spawnServer(): Promise { + const port = await findFreePort(); + await execAndWaitForOutputToMatch( + 'npm', + ['run', 'serve:ssr:test-project'], + /Node Express server listening on/, + { + 'PORT': String(port), + }, + ); + + return port; +} diff --git a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static-http-calls.ts b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static-http-calls.ts index 027a9ed9abb0..09f6190535d7 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static-http-calls.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static-http-calls.ts @@ -1,11 +1,9 @@ import { match } from 'node:assert'; import { readFile, writeMultipleFiles } from '../../utils/fs'; import { noSilentNg, silentNg } from '../../utils/process'; -import { setupProjectWithSSRAppEngine } from './setup'; export default async function () { // Setup project - await setupProjectWithSSRAppEngine(); await writeMultipleFiles({ // Add asset @@ -64,54 +62,6 @@ export default async function () { ], }; `, - 'src/app/app.routes.server.ts': ` - import { RenderMode, ServerRoute } from '@angular/ssr'; - - export const routes: ServerRoute[] = [ - { - path: '**', - renderMode: RenderMode.Prerender, - }, - ]; - `, - 'server.ts': ` - import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; - import express from 'express'; - import { fileURLToPath } from 'node:url'; - import { dirname, resolve } from 'node:path'; - - export function app(): express.Express { - const server = express(); - const serverDistFolder = dirname(fileURLToPath(import.meta.url)); - const browserDistFolder = resolve(serverDistFolder, '../browser'); - const angularNodeAppEngine = new AngularNodeAppEngine(); - - server.use('/api', (req, res) => res.json({ dataFromAPI: true })); - - server.get('**', express.static(browserDistFolder, { - maxAge: '1y', - index: 'index.html' - })); - - server.get('**', (req, res, next) => { - angularNodeAppEngine.render(req) - .then((response) => response ? writeResponseToNodeResponse(response, res) : next()) - .catch(next); - }); - - return server; - } - - const server = app(); - if (isMainModule(import.meta.url)) { - const port = process.env['PORT'] || 4000; - server.listen(port, () => { - console.log(\`Node Express server listening on http://localhost:\${port}\`); - }); - } - - export default createNodeRequestHandler(server); - `, }); await silentNg('generate', 'component', 'home'); diff --git a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts index 8b8f77869aa0..31e06debc757 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts +++ b/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts @@ -1,14 +1,24 @@ import { join } from 'node:path'; -import { expectFileNotToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; -import { noSilentNg, silentNg } from '../../utils/process'; -import { setupProjectWithSSRAppEngine } from './setup'; import { existsSync } from 'node:fs'; -import { expectToFail } from '../../utils/utils'; import assert from 'node:assert'; +import { expectFileNotToExist, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs'; +import { ng, noSilentNg, silentNg } from '../../utils/process'; +import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; +import { useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../utils/env'; +import { expectToFail } from '../../utils/utils'; export default async function () { - // Setup project - await setupProjectWithSSRAppEngine(); + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + // Forcibly remove in case another test doesn't clean itself up. + await uninstallPackage('@angular/ssr'); + await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); + await useSha(); + await installWorkspacePackages(); // Add routes await writeFile( @@ -50,7 +60,7 @@ export default async function () { ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: 'ssg/:id', renderMode: RenderMode.Prerender, diff --git a/tests/legacy-cli/e2e/tests/server-rendering/setup.ts b/tests/legacy-cli/e2e/tests/server-rendering/setup.ts deleted file mode 100644 index 1dfc3d6a8222..000000000000 --- a/tests/legacy-cli/e2e/tests/server-rendering/setup.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { getGlobalVariable } from '../../utils/env'; -import { writeFile } from '../../utils/fs'; -import { findFreePort } from '../../utils/network'; -import { installWorkspacePackages, uninstallPackage } from '../../utils/packages'; -import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; -import { updateJsonFile, useSha } from '../../utils/project'; -import assert from 'node:assert'; - -export async function spawnServer(): Promise { - const port = await findFreePort(); - await execAndWaitForOutputToMatch( - 'npm', - ['run', 'serve:ssr:test-project'], - /Node Express server listening on/, - { - 'PORT': String(port), - }, - ); - - return port; -} - -export async function setupProjectWithSSRAppEngine(): Promise { - assert( - getGlobalVariable('argv')['esbuild'], - 'This test should not be called in the Webpack suite.', - ); - - // Forcibly remove in case another test doesn't clean itself up. - await uninstallPackage('@angular/ssr'); - await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install'); - - await useSha(); - await installWorkspacePackages(); - - // Add server config - await writeFile( - 'src/app/app.config.server.ts', - ` - import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; - import { provideServerRendering } from '@angular/platform-server'; - import { provideServerRoutesConfig } from '@angular/ssr'; - import { routes } from './app.routes.server'; - import { appConfig } from './app.config'; - - const serverConfig: ApplicationConfig = { - providers: [ - provideServerRoutesConfig(routes), - provideServerRendering() - ] - }; - - export const config = mergeApplicationConfig(appConfig, serverConfig); - `, - ); - - // Update server.ts - await writeFile( - 'server.ts', - ` - import { AngularNodeAppEngine, writeResponseToNodeResponse } from '@angular/ssr/node'; - import express from 'express'; - import { fileURLToPath } from 'node:url'; - import { dirname, resolve } from 'node:path'; - - // The Express app is exported so that it can be used by serverless Functions. - export function app(): express.Express { - const server = express(); - const serverDistFolder = dirname(fileURLToPath(import.meta.url)); - const browserDistFolder = resolve(serverDistFolder, '../browser'); - - const angularNodeAppEngine = new AngularNodeAppEngine(); - - server.set('view engine', 'html'); - server.set('views', browserDistFolder); - - server.get('**', express.static(browserDistFolder, { - maxAge: '1y', - index: 'index.html', - setHeaders: (res, path) => { - const headers = angularNodeAppEngine.getPrerenderHeaders(res.req); - for (const [key, value] of headers) { - res.setHeader(key, value); - } - } - })); - - // All regular routes use the Angular engine - server.get('**', (req, res, next) => { - angularNodeAppEngine - .render(req) - .then((response) => { - if (response) { - return writeResponseToNodeResponse(response, res); - } - - return next(); - }) - .catch((err) => next(err)); - }); - - return server; - } - - function run(): void { - const port = process.env['PORT'] || 4000; - - // Start up the Node server - const server = app(); - server.listen(port, () => { - console.log(\`Node Express server listening on http://localhost:\${port}\`); - }); - } - - run(); -`, - ); - - // Update angular.json - await updateJsonFile('angular.json', (workspaceJson) => { - const appArchitect = workspaceJson.projects['test-project'].architect; - const options = appArchitect.build.options; - - delete options.prerender; - delete options.appShell; - }); -} diff --git a/tests/legacy-cli/e2e/tests/update/update-application-builder.ts b/tests/legacy-cli/e2e/tests/update/update-application-builder.ts index 75f68ec576e7..585d61256be5 100644 --- a/tests/legacy-cli/e2e/tests/update/update-application-builder.ts +++ b/tests/legacy-cli/e2e/tests/update/update-application-builder.ts @@ -16,7 +16,7 @@ export default async function () { await Promise.all([ expectFileNotToExist('tsconfig.server.json'), expectFileToMatch('tsconfig.json', 'esModuleInterop'), - expectFileToMatch('server.ts', 'import.meta.url'), + expectFileToMatch('src/server.ts', 'import.meta.url'), ]); // Verify project now creates bundles diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts index 056b24a4bc6e..f5d185f37896 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts @@ -28,22 +28,6 @@ export default async function () { await writeMultipleFiles({ // Replace the template of app.component.html as it makes it harder to debug 'src/app/app.component.html': '', - 'src/app/app.config.server.ts': ` - import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; - import { provideServerRendering } from '@angular/platform-server'; - import { provideServerRoutesConfig } from '@angular/ssr'; - import { routes } from './app.routes.server'; - import { appConfig } from './app.config'; - - const serverConfig: ApplicationConfig = { - providers: [ - provideServerRoutesConfig(routes), - provideServerRendering() - ] - }; - - export const config = mergeApplicationConfig(appConfig, serverConfig); - `, 'src/app/app.routes.ts': ` import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; @@ -55,11 +39,11 @@ export default async function () { 'src/app/app.routes.server.ts': ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: '**', renderMode: RenderMode.Server } ]; `, - 'server.ts': ` + 'src/server.ts': ` import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; import express from 'express'; import { fileURLToPath } from 'node:url'; @@ -96,7 +80,7 @@ export default async function () { } export default createNodeRequestHandler(server); - `, + `, }); await silentNg('generate', 'component', 'home'); @@ -118,7 +102,7 @@ export default async function () { await validateResponse('/home', /yay home works/); // Modify the API response and validate the change. - await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); await validateResponse('/api/test', /bar/); await validateResponse('/home', /yay home works/); diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts index 850669aca9fc..a4391587f52d 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts @@ -29,22 +29,6 @@ export default async function () { await writeMultipleFiles({ // Replace the template of app.component.html as it makes it harder to debug 'src/app/app.component.html': '', - 'src/app/app.config.server.ts': ` - import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; - import { provideServerRendering } from '@angular/platform-server'; - import { provideServerRoutesConfig } from '@angular/ssr'; - import { routes } from './app.routes.server'; - import { appConfig } from './app.config'; - - const serverConfig: ApplicationConfig = { - providers: [ - provideServerRoutesConfig(routes), - provideServerRendering() - ] - }; - - export const config = mergeApplicationConfig(appConfig, serverConfig); - `, 'src/app/app.routes.ts': ` import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; @@ -56,11 +40,11 @@ export default async function () { 'src/app/app.routes.server.ts': ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: '**', renderMode: RenderMode.Server } ]; `, - 'server.ts': ` + 'src/server.ts': ` import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; import fastify from 'fastify'; @@ -118,7 +102,7 @@ export default async function () { await validateResponse('/home', /yay home works/); // Modify the API response and validate the change. - await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); await validateResponse('/api/test', /bar/); await validateResponse('/home', /yay home works/); diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts index 5dfb2aa14c50..ef9c85a2d56c 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts @@ -29,22 +29,6 @@ export default async function () { await writeMultipleFiles({ // Replace the template of app.component.html as it makes it harder to debug 'src/app/app.component.html': '', - 'src/app/app.config.server.ts': ` - import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; - import { provideServerRendering } from '@angular/platform-server'; - import { provideServerRoutesConfig } from '@angular/ssr'; - import { routes } from './app.routes.server'; - import { appConfig } from './app.config'; - - const serverConfig: ApplicationConfig = { - providers: [ - provideServerRoutesConfig(routes), - provideServerRendering() - ] - }; - - export const config = mergeApplicationConfig(appConfig, serverConfig); - `, 'src/app/app.routes.ts': ` import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; @@ -56,11 +40,11 @@ export default async function () { 'src/app/app.routes.server.ts': ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: '**', renderMode: RenderMode.Server } ]; `, - 'server.ts': ` + 'src/server.ts': ` import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; import { createApp, createRouter, toWebHandler, defineEventHandler, toWebRequest } from 'h3'; @@ -109,7 +93,7 @@ export default async function () { await validateResponse('/home', /yay home works/); // Modify the API response and validate the change. - await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); await validateResponse('/api/test', /bar/); await validateResponse('/home', /yay home works/); diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts b/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts index 6a72367687f5..3ee3d4740d17 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts @@ -29,22 +29,6 @@ export default async function () { await writeMultipleFiles({ // Replace the template of app.component.html as it makes it harder to debug 'src/app/app.component.html': '', - 'src/app/app.config.server.ts': ` - import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; - import { provideServerRendering } from '@angular/platform-server'; - import { provideServerRoutesConfig } from '@angular/ssr'; - import { routes } from './app.routes.server'; - import { appConfig } from './app.config'; - - const serverConfig: ApplicationConfig = { - providers: [ - provideServerRoutesConfig(routes), - provideServerRendering() - ] - }; - - export const config = mergeApplicationConfig(appConfig, serverConfig); - `, 'src/app/app.routes.ts': ` import { Routes } from '@angular/router'; import { HomeComponent } from './home/home.component'; @@ -56,11 +40,11 @@ export default async function () { 'src/app/app.routes.server.ts': ` import { RenderMode, ServerRoute } from '@angular/ssr'; - export const routes: ServerRoute[] = [ + export const serverRoutes: ServerRoute[] = [ { path: '**', renderMode: RenderMode.Server } ]; `, - 'server.ts': ` + 'src/server.ts': ` import { AngularAppEngine, createRequestHandler } from '@angular/ssr'; import { Hono } from 'hono'; @@ -101,7 +85,7 @@ export default async function () { await validateResponse('/home', /yay home works/); // Modify the API response and validate the change. - await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); + await modifyFileAndWaitUntilUpdated('src/server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`); await validateResponse('/api/test', /bar/); await validateResponse('/home', /yay home works/);