diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts index 7a4b92c311d3..211ab0a798c0 100644 --- a/packages/angular/build/src/builders/application/execute-post-bundle.ts +++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts @@ -29,7 +29,7 @@ import { } from '../../utils/server-rendering/models'; import { prerenderPages } from '../../utils/server-rendering/prerender'; import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker'; -import { INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options'; +import { INDEX_HTML_CSR, INDEX_HTML_SERVER, NormalizedApplicationBuildOptions } from './options'; import { OutputMode } from './schema'; /** @@ -154,7 +154,15 @@ export async function executePostBundleSteps( // Update the index contents with the app shell under these conditions: // - Replace 'index.html' with the app shell only if it hasn't been prerendered yet. // - Always replace 'index.csr.html' with the app shell. - const filePath = appShellRoute && !indexHasBeenPrerendered ? indexHtmlOptions.output : path; + let filePath = path; + if (appShellRoute && !indexHasBeenPrerendered) { + if (outputMode !== OutputMode.Server && indexHtmlOptions.output === INDEX_HTML_CSR) { + filePath = 'index.html'; + } else { + filePath = indexHtmlOptions.output; + } + } + additionalHtmlOutputFiles.set( filePath, createOutputFile(filePath, content, BuildOutputFileType.Browser), diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index 2b65928ac5ec..6c143ca9b528 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -213,7 +213,7 @@ async function renderPages( outputFiles: outputFilesForWorker, assetFiles: assetFilesForWorker, outputMode, - hasSsrEntry: !!outputFilesForWorker['/server.mjs'], + hasSsrEntry: !!outputFilesForWorker['server.mjs'], } as RenderWorkerData, execArgv: workerExecArgv, }); @@ -319,7 +319,7 @@ async function getAllRoutes( outputFiles: outputFilesForWorker, assetFiles: assetFilesForWorker, outputMode, - hasSsrEntry: !!outputFilesForWorker['/server.mjs'], + hasSsrEntry: !!outputFilesForWorker['server.mjs'], } as RoutesExtractorWorkerData, execArgv: workerExecArgv, }); 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/server/files/application-builder/ngmodule-src/app/app.module.server.ts.template b/packages/schematics/angular/server/files/application-builder/ngmodule-src/app/app.module.server.ts.template new file mode 100644 index 000000000000..1249d761d045 --- /dev/null +++ b/packages/schematics/angular/server/files/application-builder/ngmodule-src/app/app.module.server.ts.template @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +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], + providers: [provideServerRoutesConfig(serverRoutes)], + bootstrap: [AppComponent], +}) +export class AppServerModule {} diff --git a/packages/schematics/angular/server/files/application-builder/ngmodule-src/app/app.routes.server.ts.template b/packages/schematics/angular/server/files/application-builder/ngmodule-src/app/app.routes.server.ts.template new file mode 100644 index 000000000000..ffd37b1f233c --- /dev/null +++ b/packages/schematics/angular/server/files/application-builder/ngmodule-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/src/main.server.ts.template b/packages/schematics/angular/server/files/application-builder/ngmodule-src/main.server.ts.template similarity index 100% rename from packages/schematics/angular/server/files/src/main.server.ts.template rename to packages/schematics/angular/server/files/application-builder/ngmodule-src/main.server.ts.template diff --git a/packages/schematics/angular/server/files/application-builder/standalone-src/app/app.config.server.ts.template b/packages/schematics/angular/server/files/application-builder/standalone-src/app/app.config.server.ts.template new file mode 100644 index 000000000000..1b7f65019a98 --- /dev/null +++ b/packages/schematics/angular/server/files/application-builder/standalone-src/app/app.config.server.ts.template @@ -0,0 +1,14 @@ +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(), + provideServerRoutesConfig(serverRoutes) + ] +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/packages/schematics/angular/server/files/application-builder/standalone-src/app/app.routes.server.ts.template b/packages/schematics/angular/server/files/application-builder/standalone-src/app/app.routes.server.ts.template new file mode 100644 index 000000000000..ffd37b1f233c --- /dev/null +++ b/packages/schematics/angular/server/files/application-builder/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/files/standalone-src/main.server.ts.template b/packages/schematics/angular/server/files/application-builder/standalone-src/main.server.ts.template similarity index 100% rename from packages/schematics/angular/server/files/standalone-src/main.server.ts.template rename to packages/schematics/angular/server/files/application-builder/standalone-src/main.server.ts.template diff --git a/packages/schematics/angular/server/files/src/app/app.module.server.ts.template b/packages/schematics/angular/server/files/server-builder/ngmodule-src/app/app.module.server.ts.template similarity index 100% rename from packages/schematics/angular/server/files/src/app/app.module.server.ts.template rename to packages/schematics/angular/server/files/server-builder/ngmodule-src/app/app.module.server.ts.template diff --git a/packages/schematics/angular/server/files/server-builder/ngmodule-src/main.server.ts.template b/packages/schematics/angular/server/files/server-builder/ngmodule-src/main.server.ts.template new file mode 100644 index 000000000000..dfb6fdb3f1f0 --- /dev/null +++ b/packages/schematics/angular/server/files/server-builder/ngmodule-src/main.server.ts.template @@ -0,0 +1 @@ +export { AppServerModule as default } from './app/app.module.server'; diff --git a/packages/schematics/angular/server/files/root/tsconfig.server.json.template b/packages/schematics/angular/server/files/server-builder/root/tsconfig.server.json.template similarity index 100% rename from packages/schematics/angular/server/files/root/tsconfig.server.json.template rename to packages/schematics/angular/server/files/server-builder/root/tsconfig.server.json.template diff --git a/packages/schematics/angular/server/files/standalone-src/app/app.config.server.ts.template b/packages/schematics/angular/server/files/server-builder/standalone-src/app/app.config.server.ts.template similarity index 100% rename from packages/schematics/angular/server/files/standalone-src/app/app.config.server.ts.template rename to packages/schematics/angular/server/files/server-builder/standalone-src/app/app.config.server.ts.template diff --git a/packages/schematics/angular/server/files/server-builder/standalone-src/main.server.ts.template b/packages/schematics/angular/server/files/server-builder/standalone-src/main.server.ts.template new file mode 100644 index 000000000000..4b9d4d1545c1 --- /dev/null +++ b/packages/schematics/angular/server/files/server-builder/standalone-src/main.server.ts.template @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; diff --git a/packages/schematics/angular/server/index.ts b/packages/schematics/angular/server/index.ts index 418fec81070c..af1d03872dfd 100644 --- a/packages/schematics/angular/server/index.ts +++ b/packages/schematics/angular/server/index.ts @@ -16,6 +16,7 @@ import { chain, mergeWith, move, + renameTemplateFiles, strings, url, } from '@angular-devkit/schematics'; @@ -101,10 +102,8 @@ function updateConfigFileApplicationBuilder(options: ServerOptions): Rule { } const buildTarget = project.targets.get('build'); - if (buildTarget?.builder !== Builders.Application) { - throw new SchematicsException( - `This schematic requires "${Builders.Application}" to be used as a build builder.`, - ); + if (!buildTarget) { + return; } buildTarget.options ??= {}; @@ -112,6 +111,8 @@ function updateConfigFileApplicationBuilder(options: ServerOptions): Rule { project.sourceRoot ?? posix.join(project.root, 'src'), serverMainEntryName, ); + + buildTarget.options['outputMode'] = 'static'; }); } @@ -169,7 +170,10 @@ export default function (options: ServerOptions): Rule { throw targetBuildNotFoundError(); } - const isUsingApplicationBuilder = clientBuildTarget.builder === Builders.Application; + const isUsingApplicationBuilder = + clientBuildTarget.builder === Builders.Application || + clientBuildTarget.builder === Builders.BuildApplication; + if ( clientProject.targets.has('server') || (isUsingApplicationBuilder && clientBuildTarget.options?.server !== undefined) @@ -181,13 +185,17 @@ export default function (options: ServerOptions): Rule { const clientBuildOptions = clientBuildTarget.options as Record; const browserEntryPoint = await getMainFilePath(host, options.project); const isStandalone = isStandaloneApp(host, browserEntryPoint); + const sourceRoot = clientProject.sourceRoot ?? join(normalize(clientProject.root), 'src'); + + let filesUrl = `./files/${isUsingApplicationBuilder ? 'application-builder/' : 'server-builder/'}`; + filesUrl += isStandalone ? 'standalone-src' : 'ngmodule-src'; - const templateSource = apply(url(isStandalone ? './files/standalone-src' : './files/src'), [ + const templateSource = apply(url(filesUrl), [ applyTemplates({ ...strings, ...options, }), - move(join(normalize(clientProject.root), 'src')), + move(sourceRoot), ]); const clientTsConfig = normalize(clientBuildOptions.tsConfig); @@ -203,7 +211,7 @@ export default function (options: ServerOptions): Rule { ] : [ mergeWith( - apply(url('./files/root'), [ + apply(url('./files/server-builder/root'), [ applyTemplates({ ...strings, ...options, diff --git a/packages/schematics/angular/ssr/files/application-builder/server.ts.template b/packages/schematics/angular/ssr/files/application-builder/server.ts.template index b8f7e04fb7f2..010804ead0f6 100644 --- a/packages/schematics/angular/ssr/files/application-builder/server.ts.template +++ b/packages/schematics/angular/ssr/files/application-builder/server.ts.template @@ -1,57 +1,49 @@ -import { APP_BASE_HREF } from '@angular/common'; -import { CommonEngine } from '@angular/ssr/node'; +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node'; import express from 'express'; +import { dirname, resolve } from 'node:path'; 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 serverDistFolder = dirname(fileURLToPath(import.meta.url)); +const browserDistFolder = resolve(serverDistFolder, '../<%= browserDistDirectory %>'); - const commonEngine = new CommonEngine(); +const app = express(); +const angularApp = new AngularNodeAppEngine(); - server.set('view engine', 'html'); - server.set('views', browserDistFolder); +// Example Express Rest API endpoints +// app.get('/api/**', (req, res) => { }); - // Example Express Rest API endpoints - // server.get('/api/**', (req, res) => { }); - // Serve static files from /<%= browserDistDirectory %> - server.get('**', express.static(browserDistFolder, { +// Serve static files from /<%= browserDistDirectory %> +app.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 { + 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; - - // Start up the Node server - const server = app(); - server.listen(port, () => { + app.listen(port, () => { console.log(`Node Express server listening on http://localhost:${port}`); }); } -run(); +export default createNodeRequestHandler(app); diff --git a/packages/schematics/angular/ssr/files/server-builder/server.ts.template b/packages/schematics/angular/ssr/files/server-builder/server.ts.template index 8cfcc0e4638b..de14624dce26 100644 --- a/packages/schematics/angular/ssr/files/server-builder/server.ts.template +++ b/packages/schematics/angular/ssr/files/server-builder/server.ts.template @@ -5,7 +5,7 @@ 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'; +import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './main.server'; // The Express app is exported so that it can be used by serverless Functions. export function app(): express.Express { diff --git a/packages/schematics/angular/ssr/index.ts b/packages/schematics/angular/ssr/index.ts index 19569c7775db..6adb700e7b56 100644 --- a/packages/schematics/angular/ssr/index.ts +++ b/packages/schematics/angular/ssr/index.ts @@ -156,17 +156,16 @@ function updateApplicationBuilderTsConfigRule(options: SSROptions): Rule { 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]); - } + 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,15 +201,18 @@ 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 { +function updateWebpackBuilderWorkspaceConfigRule( + projectSourceRoot: string, + options: SSROptions, +): Rule { return updateWorkspace((workspace) => { const projectName = options.project; const project = workspace.projects.get(projectName); @@ -220,7 +222,7 @@ function updateWebpackBuilderWorkspaceConfigRule(options: SSROptions): Rule { // 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'); + (serverTarget.options ??= {}).main = posix.join(projectSourceRoot, 'server.ts'); const serveSSRTarget = project.targets.get(SERVE_SSR_TARGET_NAME); if (serveSSRTarget) { @@ -287,7 +289,7 @@ function updateWebpackBuilderServerTsConfigRule(options: SSROptions): Rule { const tsConfig = new JSONFile(host, tsConfigPath); const filesAstNode = tsConfig.get(['files']); - const serverFilePath = 'server.ts'; + const serverFilePath = 'src/server.ts'; if (Array.isArray(filesAstNode) && !filesAstNode.some(({ text }) => text === serverFilePath)) { tsConfig.modify(['files'], [...filesAstNode, serverFilePath]); } @@ -320,7 +322,11 @@ function addDependencies({ skipInstall }: SSROptions, isUsingApplicationBuilder: return chain(rules); } -function addServerFile(options: ServerOptions, isStandalone: boolean): Rule { +function addServerFile( + projectSourceRoot: string, + options: ServerOptions, + isStandalone: boolean, +): Rule { return async (host) => { const projectName = options.project; const workspace = await readWorkspace(host); @@ -344,7 +350,7 @@ function addServerFile(options: ServerOptions, isStandalone: boolean): Rule { browserDistDirectory, isStandalone, }), - move(project.root), + move(projectSourceRoot), ], ), ); @@ -364,6 +370,8 @@ export default function (options: SSROptions): Rule { const isUsingApplicationBuilder = usingApplicationBuilder(clientProject); + const sourceRoot = clientProject.sourceRoot ?? posix.join(clientProject.root, 'src'); + return chain([ schematic('server', { ...options, @@ -371,14 +379,14 @@ export default function (options: SSROptions): Rule { }), ...(isUsingApplicationBuilder ? [ - updateApplicationBuilderWorkspaceConfigRule(clientProject.root, options, context), + updateApplicationBuilderWorkspaceConfigRule(sourceRoot, options, context), updateApplicationBuilderTsConfigRule(options), ] : [ updateWebpackBuilderServerTsConfigRule(options), - updateWebpackBuilderWorkspaceConfigRule(options), + updateWebpackBuilderWorkspaceConfigRule(sourceRoot, options), ]), - addServerFile(options, isStandalone), + addServerFile(sourceRoot, options, isStandalone), addScriptsRule(options, isUsingApplicationBuilder), addDependencies(options, isUsingApplicationBuilder), ]); diff --git a/packages/schematics/angular/ssr/index_spec.ts b/packages/schematics/angular/ssr/index_spec.ts index b88b767d1d79..63c11e772c2f 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')`); }); @@ -239,20 +200,20 @@ describe('SSR Schematic', () => { files: string[]; }; - expect(files).toEqual(['src/main.server.ts', 'server.ts']); + expect(files).toEqual(['src/main.server.ts', 'src/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'); + const content = tree.readContent('/projects/test-app/src/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'); + const content = tree.readContent('/projects/test-app/src/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..e30c4417fa01 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/commands/serve/ssr-http-requests-assets.js", "tests/build/prerender/http-requests-assets.js", "tests/build/prerender/error-with-sourcemaps.js", + "tests/build/server-rendering/server-routes-*", "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/build/prerender/discover-routes-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/prerender/discover-routes-ngmodule.ts index 074a567c6ef7..7aa700dbcdd0 100644 --- a/tests/legacy-cli/e2e/tests/build/prerender/discover-routes-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/prerender/discover-routes-ngmodule.ts @@ -118,18 +118,11 @@ export default async function () { return; } - await ng('build', projectName, '--configuration=production', '--prerender', '--no-ssr'); + await ng('build', projectName, '--configuration=production'); await runExpects(); // Test also JIT mode. - await ng( - 'build', - projectName, - '--configuration=development', - '--prerender', - '--no-ssr', - '--no-aot', - ); + await ng('build', projectName, '--configuration=development', '--no-aot'); await runExpects(); diff --git a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts similarity index 88% rename from tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts index a1958399d490..35b4e29f7418 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-csp-nonce.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts @@ -1,13 +1,12 @@ -import { getGlobalVariable } from '../../utils/env'; -import { rimraf, writeMultipleFiles } from '../../utils/fs'; -import { findFreePort } from '../../utils/network'; -import { installWorkspacePackages } from '../../utils/packages'; -import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; -import { updateJsonFile, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { findFreePort } from '../../../utils/network'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../../utils/process'; +import { updateJsonFile, updateServerFileForWebpack, 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'); @@ -16,11 +15,13 @@ export default async function () { await installWorkspacePackages(); if (!useWebpackBuilder) { - // Disable prerendering await updateJsonFile('angular.json', (json) => { const build = json['projects']['test-project']['architect']['build']; + build.options.outputMode = undefined; build.configurations.production.prerender = false; }); + + await updateServerFileForWebpack('src/server.ts'); } await writeMultipleFiles({ @@ -46,8 +47,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 +99,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 +115,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); diff --git a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts similarity index 79% rename from tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts index 42c8d735d528..67b003fab334 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts @@ -1,9 +1,15 @@ -import { getGlobalVariable } from '../../utils/env'; -import { rimraf, writeMultipleFiles } from '../../utils/fs'; -import { findFreePort } from '../../utils/network'; -import { installWorkspacePackages } from '../../utils/packages'; -import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; -import { updateJsonFile, useCIChrome, useCIDefaults, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { findFreePort } from '../../../utils/network'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../../utils/process'; +import { + updateJsonFile, + updateServerFileForWebpack, + useCIChrome, + useCIDefaults, + useSha, +} from '../../../utils/project'; export default async function () { // forcibly remove in case another test doesn't clean itself up @@ -53,7 +59,10 @@ export default async function () { await updateJsonFile('angular.json', (json) => { const build = json['projects']['test-project-two']['architect']['build']; build.configurations.production.prerender = false; + build.options.outputMode = undefined; }); + + await updateServerFileForWebpack('projects/test-project-two/src/server.ts'); } await writeMultipleFiles({ @@ -69,8 +78,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,24 +128,20 @@ 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(); + }); + }); `, }); diff --git a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts similarity index 86% rename from tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts index 0922e8800a8e..a4f601c934b4 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/express-engine-standalone.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts @@ -1,14 +1,13 @@ -import { getGlobalVariable } from '../../utils/env'; -import { rimraf, writeMultipleFiles } from '../../utils/fs'; -import { findFreePort } from '../../utils/network'; -import { installWorkspacePackages } from '../../utils/packages'; -import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; -import { updateJsonFile, useSha } from '../../utils/project'; +import { getGlobalVariable } from '../../../utils/env'; +import { rimraf, writeMultipleFiles } from '../../../utils/fs'; +import { findFreePort } from '../../../utils/network'; +import { installWorkspacePackages } from '../../../utils/packages'; +import { execAndWaitForOutputToMatch, ng } from '../../../utils/process'; +import { updateJsonFile, updateServerFileForWebpack, useSha } from '../../../utils/project'; export default async function () { // 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'); const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; @@ -16,8 +15,10 @@ export default async function () { // Disable prerendering await updateJsonFile('angular.json', (json) => { const build = json['projects']['test-project']['architect']['build']; - build.configurations.production.prerender = false; + build.options.outputMode = undefined; }); + + await updateServerFileForWebpack('src/server.ts'); } await useSha(); @@ -34,8 +35,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 +85,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,9 +97,7 @@ export default async function () { // Make sure there were no client side errors. await verifyNoBrowserErrors(); - });` - : '') + - ` + }); }); `, }); 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/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts similarity index 63% rename from tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n-base-href.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts index e06e50b53444..361fb0a96e60 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n-base-href.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts @@ -1,19 +1,27 @@ 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 { langTranslations, setupI18nConfig } from '../i18n/setup'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +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 () { - if (process.version.startsWith('v18')) { - // This is not supported in Node.js version 18 as global web crypto module is not available. - return; - } + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); // 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 +55,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 +100,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/build/server-rendering/server-routes-output-mode-server-i18n.ts similarity index 67% rename from tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts index 98c041bd65ba..a855f300b98e 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server-i18n.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts @@ -1,19 +1,27 @@ 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 { langTranslations, setupI18nConfig } from '../i18n/setup'; +import { expectFileToMatch, writeFile } from '../../../utils/fs'; +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 () { - if (process.version.startsWith('v18')) { - // This is not supported in Node.js version 18 as global web crypto module is not available. - return; - } + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); // 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 +55,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 +112,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/build/server-rendering/server-routes-output-mode-server.ts similarity index 80% rename from tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts index bf6b36fa1c49..e32516129803 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-server.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts @@ -1,18 +1,24 @@ 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 { expectFileToMatch, writeFile } from '../../../utils/fs'; +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 () { - if (process.version.startsWith('v18')) { - // This is not supported in Node.js version 18 as global web crypto module is not available. - return; - } + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); - // 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 +71,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 +188,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/build/server-rendering/server-routes-output-mode-static-http-calls.ts similarity index 80% rename from tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static-http-calls.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts index 027a9ed9abb0..5f0623b921a3 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static-http-calls.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static-http-calls.ts @@ -1,11 +1,21 @@ -import { match } from 'node:assert'; -import { readFile, writeMultipleFiles } from '../../utils/fs'; -import { noSilentNg, silentNg } from '../../utils/process'; -import { setupProjectWithSSRAppEngine } from './setup'; +import assert, { match } from 'node:assert'; +import { readFile, writeMultipleFiles } from '../../../utils/fs'; +import { ng, noSilentNg, silentNg } from '../../../utils/process'; +import { getGlobalVariable } from '../../../utils/env'; +import { installWorkspacePackages, uninstallPackage } from '../../../utils/packages'; +import { useSha } from '../../../utils/project'; 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(); await writeMultipleFiles({ // Add asset @@ -64,17 +74,7 @@ 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': ` + 'src/server.ts': ` import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, createNodeRequestHandler } from '@angular/ssr/node'; import express from 'express'; import { fileURLToPath } from 'node:url'; @@ -86,7 +86,7 @@ export default async function () { const browserDistFolder = resolve(serverDistFolder, '../browser'); const angularNodeAppEngine = new AngularNodeAppEngine(); - server.use('/api', (req, res) => res.json({ dataFromAPI: true })); + server.get('/api', (req, res) => res.json({ dataFromAPI: true })); server.get('**', express.static(browserDistFolder, { maxAge: '1y', @@ -98,11 +98,11 @@ export default async function () { .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, () => { @@ -116,7 +116,6 @@ export default async function () { await silentNg('generate', 'component', 'home'); - // Fix the error await noSilentNg('build', '--output-mode=static'); const contents = await readFile('dist/test-project/browser/home/index.html'); diff --git a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts similarity index 76% rename from tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts rename to tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts index 8b8f77869aa0..74ded1d3981e 100644 --- a/tests/legacy-cli/e2e/tests/server-rendering/server-routes-output-mode-static.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-static.ts @@ -1,14 +1,29 @@ 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 +65,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/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/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/); diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts index fce9a2ec02cb..24316ed954c5 100644 --- a/tests/legacy-cli/e2e/utils/project.ts +++ b/tests/legacy-cli/e2e/utils/project.ts @@ -204,3 +204,63 @@ export function getNgCLIVersion(): SemVer { export function isPrereleaseCli(): boolean { return (prerelease(getNgCLIVersion())?.length ?? 0) > 0; } + +export function updateServerFileForWebpack(filepath: string): Promise { + return writeFile( + filepath, + ` + 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 './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(); + `, + ); +}