diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index 5d4878f6fe81..280a97966f62 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -141,74 +141,32 @@ 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 updateWorkspace((workspace) => { + const project = workspace.projects.get(options.project); + if (!project) { + return; + } - return; - } + const buildTarget = project.targets.get('build'); + if ( + buildTarget?.builder !== Builders.Application && + buildTarget?.builder !== Builders.BuildApplication + ) { + throw new SchematicsException( + `App-shell schematic requires the project to use "${Builders.Application}" or "${Builders.BuildApplication}" as the build builder.`, + ); + } - // 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}`, - }; - } + // Application builder configuration. + const prodConfig = buildTarget.configurations?.production; + if (!prodConfig) { + throw new SchematicsException( + `A "production" configuration is not defined for the "build" builder.`, + ); + } - project.targets.add({ - name: 'app-shell', - builder: Builders.AppShell, - defaultConfiguration: configurations['production'] ? 'production' : undefined, - options: { - route: APP_SHELL_ROUTE, - }, - configurations, - }); - }); - }; + prodConfig.appShell = true; + }); } function addRouterModule(mainPath: string): Rule { diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index f617b29f4112..1bca4f87da3b 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -240,44 +240,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/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/index.ts b/packages/schematics/angular/server/index.ts index 418fec81070c..55182cc85ee8 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); @@ -169,11 +109,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 +135,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/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/application-builder/server.ts.template b/packages/schematics/angular/ssr/files/server.ts.template similarity index 100% rename from packages/schematics/angular/ssr/files/application-builder/server.ts.template rename to packages/schematics/angular/ssr/files/server.ts.template diff --git a/packages/schematics/angular/ssr/index.ts b/packages/schematics/angular/ssr/index.ts index 19569c7775db..39a126f6dac6 100644 --- a/packages/schematics/angular/ssr/index.ts +++ b/packages/schematics/angular/ssr/index.ts @@ -34,40 +34,15 @@ 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 +89,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,20 +97,9 @@ 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)); }; @@ -210,116 +174,6 @@ function updateApplicationBuilderWorkspaceConfigRule( }); } -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 { return async (host) => { const projectName = options.project; @@ -328,25 +182,30 @@ 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, + isStandalone, + }), + move(project.root), + ]), ); }; } @@ -362,33 +221,25 @@ export default function (options: SSROptions): Rule { throw targetBuildNotFoundError(); } - const isUsingApplicationBuilder = usingApplicationBuilder(clientProject); + const install = options.skipInstall ? InstallBehavior.None : InstallBehavior.Auto; return chain([ schematic('server', { ...options, skipInstall: true, }), - ...(isUsingApplicationBuilder - ? [ - updateApplicationBuilderWorkspaceConfigRule(clientProject.root, options, context), - updateApplicationBuilderTsConfigRule(options), - ] - : [ - updateWebpackBuilderServerTsConfigRule(options), - updateWebpackBuilderWorkspaceConfigRule(options), - ]), + updateApplicationBuilderWorkspaceConfigRule(clientProject.root, options, context), + updateApplicationBuilderTsConfigRule(options), addServerFile(options, isStandalone), - addScriptsRule(options, isUsingApplicationBuilder), - addDependencies(options, isUsingApplicationBuilder), + 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..1ec00bcf7a3c 100644 --- a/packages/schematics/angular/ssr/index_spec.ts +++ b/packages/schematics/angular/ssr/index_spec.ts @@ -190,70 +190,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/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..53d2020bf7cb 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,13 +12,10 @@ 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.configurations.production.prerender = false; + }); await writeMultipleFiles({ 'src/app/app.component.css': `div { color: #000 }`, @@ -46,8 +40,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 +92,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 +108,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 +129,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 +143,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..5da86d783615 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,13 +26,11 @@ 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.configurations.production.prerender = false; + }); await writeMultipleFiles({ 'projects/test-project-two/src/app/app.component.css': `div { color: #000 }`, @@ -69,8 +45,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 +95,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 +128,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..61cea4db3a0e 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,9 +1,8 @@ -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 { useSha } from '../../utils/project'; export default async function () { // forcibly remove in case another test doesn't clean itself up @@ -11,15 +10,6 @@ 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(); @@ -34,8 +24,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 +74,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 +86,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 +107,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=');