/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular_devkit/build_angular/src/builders/prerender/index.ts
300 строк
9 KB
Alan Agius
refactor(@angular-devkit/build-angular): deprecate Webpack builders
06 май 2026, 19:48
06 май 2026, 19:48
b7940db
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { augmentAppWithServiceWorker } from '@angular/build/private'; import { BuilderContext, BuilderOutput, createBuilder, targetFromTargetString, } from '@angular-devkit/architect'; import * as fs from 'node:fs'; import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; import ora from 'ora'; import Piscina from 'piscina'; import { normalizeOptimization } from '../../utils'; import { maxWorkers } from '../../utils/environment-options'; import { assertIsError } from '../../utils/error'; import { getIndexOutputFile } from '../../utils/webpack-browser-config'; import { BrowserBuilderOutput } from '../browser'; import { Schema as BrowserBuilderOptions } from '../browser/schema'; import { ServerBuilderOutput } from '../server'; import type { RenderOptions, RenderResult } from './render-worker'; import { RoutesExtractorWorkerData } from './routes-extractor-worker'; import { Schema } from './schema'; type PrerenderBuilderOptions = Schema; type PrerenderBuilderOutput = BuilderOutput; class RoutesSet extends Set<string> { override add(value: string): this { return super.add(value.charAt(0) === '/' ? value.slice(1) : value); } } async function getRoutes( indexFile: string, outputPath: string, serverBundlePath: string, options: PrerenderBuilderOptions, workspaceRoot: string, ): Promise<string[]> { const { routes: extraRoutes = [], routesFile, discoverRoutes } = options; const routes = new RoutesSet(extraRoutes); if (routesFile) { const routesFromFile = (await readFile(path.join(workspaceRoot, routesFile), 'utf8')).split( /\r?\n/, ); for (const route of routesFromFile) { routes.add(route); } } let zonePackage: string | undefined; try { zonePackage = require.resolve('zone.js/node', { paths: [workspaceRoot] }); } catch {} if (discoverRoutes) { const renderWorker = new Piscina({ filename: require.resolve('./routes-extractor-worker'), maxThreads: 1, workerData: { indexFile, outputPath, serverBundlePath, zonePackage, } as RoutesExtractorWorkerData, recordTiming: false, }); const extractedRoutes: string[] = await renderWorker .run({}) .finally(() => void renderWorker.destroy()); for (const route of extractedRoutes) { routes.add(route); } } if (routes.size === 0) { throw new Error('Could not find any routes to prerender.'); } return [...routes]; } /** * Schedules the server and browser builds and returns their results if both builds are successful. */ async function _scheduleBuilds( options: PrerenderBuilderOptions, context: BuilderContext, ): Promise< BuilderOutput & { serverResult?: ServerBuilderOutput; browserResult?: BrowserBuilderOutput; } > { const browserTarget = targetFromTargetString(options.browserTarget); const serverTarget = targetFromTargetString(options.serverTarget); const browserTargetRun = await context.scheduleTarget(browserTarget, { watch: false, serviceWorker: false, // todo: handle service worker augmentation }); if (browserTargetRun.info.builderName === '@angular-devkit/build-angular:application') { return { success: false, error: '"@angular-devkit/build-angular:application" has built-in prerendering capabilities. ' + 'The "prerender" option should be used instead.', }; } const serverTargetRun = await context.scheduleTarget(serverTarget, { watch: false, }); try { const [browserResult, serverResult] = await Promise.all([ browserTargetRun.result as unknown as BrowserBuilderOutput, serverTargetRun.result as unknown as ServerBuilderOutput, ]); const success = browserResult.success && serverResult.success && browserResult.baseOutputPath !== undefined; const error = browserResult.error || (serverResult.error as string); return { success, error, browserResult, serverResult }; } catch (e) { assertIsError(e); return { success: false, error: e.message }; } finally { await Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]); } } /** * Renders each route and writes them to * <route>/index.html for each output path in the browser result. */ async function _renderUniversal( options: PrerenderBuilderOptions, context: BuilderContext, browserResult: BrowserBuilderOutput, serverResult: ServerBuilderOutput, browserOptions: BrowserBuilderOptions, ): Promise<PrerenderBuilderOutput> { const projectName = context.target && context.target.project; if (!projectName) { throw new Error('The builder requires a target.'); } const projectMetadata = await context.getProjectMetadata(projectName); const projectRoot = path.join( context.workspaceRoot, (projectMetadata.root as string | undefined) ?? '', ); // Users can specify a different base html file e.g. "src/home.html" const indexFile = getIndexOutputFile(browserOptions.index); const { styles: normalizedStylesOptimization } = normalizeOptimization( browserOptions.optimization, ); let zonePackage: string | undefined; try { zonePackage = require.resolve('zone.js/node', { paths: [context.workspaceRoot] }); } catch {} const { baseOutputPath = '' } = serverResult; const worker = new Piscina({ filename: path.join(__dirname, 'render-worker.js'), maxThreads: maxWorkers, workerData: { zonePackage }, recordTiming: false, }); let routes: string[] | undefined; try { // We need to render the routes for each locale from the browser output. for (const { path: outputPath } of browserResult.outputs) { const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath); const serverBundlePath = path.join(baseOutputPath, localeDirectory, 'main.js'); if (!fs.existsSync(serverBundlePath)) { throw new Error(`Could not find the main bundle: ${serverBundlePath}`); } routes ??= await getRoutes( indexFile, outputPath, serverBundlePath, options, context.workspaceRoot, ); const spinner = ora(`Prerendering ${routes.length} route(s) to ${outputPath}...`).start(); try { const results = (await Promise.all( routes.map((route) => { const options: RenderOptions = { indexFile, deployUrl: browserOptions.deployUrl || '', inlineCriticalCss: !!normalizedStylesOptimization.inlineCritical, minifyCss: !!normalizedStylesOptimization.minify, outputPath, route: route[0] === '/' ? route : '/' + route, serverBundlePath, }; return worker.run(options); }), )) as RenderResult[]; let numErrors = 0; for (const { errors, warnings } of results) { spinner.stop(); errors?.forEach((e) => context.logger.error(e)); warnings?.forEach((e) => context.logger.warn(e)); spinner.start(); numErrors += errors?.length ?? 0; } if (numErrors > 0) { throw Error(`Rendering failed with ${numErrors} worker errors.`); } } catch (error) { spinner.fail(`Prerendering routes to ${outputPath} failed.`); assertIsError(error); return { success: false, error: error.message }; } spinner.succeed(`Prerendering routes to ${outputPath} complete.`); if (browserOptions.serviceWorker) { spinner.start('Generating service worker...'); try { await augmentAppWithServiceWorker( projectRoot, context.workspaceRoot, outputPath, browserOptions.baseHref || '/', browserOptions.ngswConfigPath, ); } catch (error) { spinner.fail('Service worker generation failed.'); assertIsError(error); return { success: false, error: error.message }; } spinner.succeed('Service worker generation complete.'); } } } finally { void worker.destroy(); } return browserResult; } /** * Builds the browser and server, then renders each route in options.routes * and writes them to prerender/<route>/index.html for each output path in * the browser result. */ export async function execute( options: PrerenderBuilderOptions, context: BuilderContext, ): Promise<PrerenderBuilderOutput> { context.logger.warn( 'The "@angular-devkit/build-angular:prerender" builder is deprecated as part of Angular\'s Webpack support deprecation. ' + 'Use "@angular/build:application" instead. For more information, see https://angular.dev/tools/cli/build-system-migration.', ); const browserTarget = targetFromTargetString(options.browserTarget); const browserOptions = (await context.getTargetOptions( browserTarget, )) as unknown as BrowserBuilderOptions; const result = await _scheduleBuilds(options, context); const { success, error, browserResult, serverResult } = result; if (!success || !browserResult || !serverResult) { return { success, error } as BuilderOutput; } return _renderUniversal(options, context, browserResult, serverResult, browserOptions); } export default createBuilder(execute);