/** * @license * Copyright Google Inc. 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.io/license */ import { Rule } from '@angular-devkit/schematics'; import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { findNodes } from '../../utility/ast-utils'; import { allWorkspaceTargets, getWorkspace } from '../../utility/workspace'; import { Builders } from '../../utility/workspace-models'; /** * Update the `main.server.ts` file by adding exports to `renderModule` and `renderModuleFactory` which are * now required for Universal and App-Shell for Ivy and `bundleDependencies`. */ export function updateServerMainFile(): Rule { return async tree => { const workspace = await getWorkspace(tree); for (const [targetName, target] of allWorkspaceTargets(workspace)) { if (targetName !== 'server' || target.builder !== Builders.Server) { continue; } // find the main server file const mainFilePath = target.options?.main; if (!mainFilePath || typeof mainFilePath !== 'string') { continue; } const content = tree.read(mainFilePath); if (!content) { continue; } const source = ts.createSourceFile( mainFilePath, content.toString().replace(/^\uFEFF/, ''), ts.ScriptTarget.Latest, true, ); // find exports in main server file const exportDeclarations = findNodes(source, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration[]; const platformServerExports = exportDeclarations.filter(({ moduleSpecifier }) => ( moduleSpecifier && ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@angular/platform-server' )); let hasRenderModule = false; let hasRenderModuleFactory = false; // find exports of renderModule or renderModuleFactory for (const { exportClause } of platformServerExports) { if (exportClause && ts.isNamedExports(exportClause)) { if (!hasRenderModuleFactory) { hasRenderModuleFactory = exportClause.elements.some(({ name }) => name.text === 'renderModuleFactory'); } if (!hasRenderModule) { hasRenderModule = exportClause.elements.some(({ name }) => name.text === 'renderModule'); } } } if (hasRenderModule && hasRenderModuleFactory) { // We have both required exports continue; } let exportSpecifiers: ts.ExportSpecifier[] = []; let updateExisting = false; // Add missing exports if (platformServerExports.length) { const { exportClause } = platformServerExports[0] as ts.ExportDeclaration; if (!exportClause || ts.isNamespaceExport(exportClause)) { continue; } exportSpecifiers = [...exportClause.elements]; updateExisting = true; } if (!hasRenderModule) { exportSpecifiers.push(ts.createExportSpecifier( undefined, ts.createIdentifier('renderModule'), )); } if (!hasRenderModuleFactory) { exportSpecifiers.push(ts.createExportSpecifier( undefined, ts.createIdentifier('renderModuleFactory'), )); } // Create a TS printer to get the text of the export node const printer = ts.createPrinter(); const moduleSpecifier = ts.createStringLiteral('@angular/platform-server'); // TypeScript will emit the Node with double quotes. // In schematics we usually write code with a single quotes // tslint:disable-next-line: no-any (moduleSpecifier as any).singleQuote = true; const newExportDeclarationText = printer.printNode( ts.EmitHint.Unspecified, ts.createExportDeclaration( undefined, undefined, ts.createNamedExports(exportSpecifiers), moduleSpecifier, ), source, ); const recorder = tree.beginUpdate(mainFilePath); if (updateExisting) { const start = platformServerExports[0].getStart(); const width = platformServerExports[0].getWidth(); recorder .remove(start, width) .insertLeft(start, newExportDeclarationText); } else { recorder.insertLeft(source.getWidth(), '\n' + newExportDeclarationText); } tree.commitUpdate(recorder); } }; }