138 lines
4.4 KiB
TypeScript

/**
* @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);
}
};
}