Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Migrate existing apps with application builder to use index.server.html in server file #19848

Merged
merged 42 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
158766c
chore: Nx migration (all packages and their migration)
pawelfras Dec 12, 2024
f95bd2c
chore: Change Version Range Specifier For Typescript
pawelfras Dec 12, 2024
54ca560
fix: Build issues
pawelfras Dec 13, 2024
51ae9c4
chore: Update '@angular-builders/custom-webpack' to '19.0.0-beta.0'
pawelfras Dec 13, 2024
71152c8
fix: Issues In Unit Tests
pawelfras Dec 4, 2024
f77c7eb
chore: Update dependencies
pawelfras Dec 16, 2024
1126c06
fix: Fix issues with schematics and related unit tests
pawelfras Dec 17, 2024
e3e935d
chore: Fix linter issues
pawelfras Dec 17, 2024
4048f17
chore: Fix prettier issues
pawelfras Dec 17, 2024
34574a8
chore: Update NgRx to v19.0.0-beta.0
pawelfras Dec 17, 2024
b1bc98c
chore: Upgrade `@typescript-eslint` packages to 8.18.1
pawelfras Dec 17, 2024
6d1bf09
chore: Update package-lock file
pawelfras Dec 17, 2024
ccf37f2
chore: Upgrade NgRx to stable version 19.0.0
pawelfras Dec 18, 2024
c1fa528
chore: Update dependencies and package-lock file
pawelfras Dec 18, 2024
3984894
fix: Jest tests issues
pawelfras Dec 18, 2024
85526ca
Trigger Build
pawelfras Dec 19, 2024
a004277
Trigger Build
pawelfras Dec 19, 2024
8da6be2
chore: Initial migration docs
pawelfras Dec 24, 2024
ef4ede7
refactor: Adjust installation schematics for SSR to use `server/index…
pawelfras Dec 30, 2024
89b2d08
docs: Update migration docs
pawelfras Dec 31, 2024
1ec74e0
chore Upgrade '@angular-builders/custom-webpack' to version 19.0.0
pawelfras Jan 7, 2025
3fb0b3a
Add license header
github-actions[bot] Jan 7, 2025
587ce7b
Revert "Add license header"
pawelfras Jan 7, 2025
c304b34
Add license header
github-actions[bot] Jan 7, 2025
c59ccf8
Revert "Add license header"
pawelfras Jan 8, 2025
b5500cb
Merge branch 'develop-next-major' into epic/upgrade-to-angular-19
pawelfras Jan 8, 2025
9fe60e6
refactor: add util to work with application builder and handle scenar…
pawelfras Jan 9, 2025
768edc1
Merge branch 'epic/upgrade-to-angular-19' into feat/use-index-server-…
pawelfras Jan 9, 2025
72a5973
Add license header
github-actions[bot] Jan 9, 2025
12980b5
fix sonar issues
pawelfras Jan 20, 2025
1a504c1
chore: fix sonar issues and add JSDOCs
pawelfras Jan 20, 2025
ffa0564
Merge branch 'develop' into feat/use-index-server-file
pawelfras Jan 23, 2025
d7d60c9
chore: revert changes in migration.md
pawelfras Jan 23, 2025
120b7f2
Merge branch 'develop' into feat/use-index-server-file
pawelfras Jan 24, 2025
6c16b8b
chore: revert changes in package-lock.json
pawelfras Jan 24, 2025
b33f694
chore: update migration version and related parts to 2211.35.0
pawelfras Jan 24, 2025
68435b4
Merge branch 'develop' into feat/use-index-server-file
pawelfras Jan 24, 2025
be9b4fe
Merge branch 'develop' into feat/use-index-server-file
Platonn Jan 27, 2025
ba97e49
fix: prevent removing new-lines from original server.ts file (#19933)
Platonn Jan 28, 2025
b1207c9
refactor after review
pawelfras Jan 28, 2025
6f28a3d
fix sonanr issue
pawelfras Jan 28, 2025
c4cbd53
Merge branch 'develop' into feat/use-index-server-file
pawelfras Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions projects/schematics/src/migrations/2211_35/ssr/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { checkIfSSRIsUsedWithApplicationBuilder } from '../../../shared/utils/package-utils';
import { updateServerFile } from './update-ssr/update-server-files';

export function migrate(): Rule {
return (tree: Tree, _context: SchematicContext) => {
return checkIfSSRIsUsedWithApplicationBuilder(tree)
? updateServerFile()
: noop();
};
}
147 changes: 147 additions & 0 deletions projects/schematics/src/migrations/2211_35/ssr/ssr_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import { join } from 'path';
import * as shared from '../../../shared/utils/package-utils';

jest.mock('../../../shared/utils/package-utils', () => ({
...jest.requireActual('../../../shared/utils/package-utils'),
checkIfSSRIsUsedWithApplicationBuilder: jest.fn(),
}));

const collectionPath = join(__dirname, '../../migrations.json');
const MIGRATION_SCRIPT_NAME = '01-migration-v2211_35-ssr';

describe('Update SSR Migration', () => {
let tree: Tree;
let runner: SchematicTestRunner;

const workspaceContent = {
version: 1,
projects: {
app: {
root: '',
architect: {
build: {
builder: '@angular-devkit/build-angular:application',
},
},
},
},
};

const serverFileContent = `
import { APP_BASE_HREF } from '@angular/common';
import {
NgExpressEngineDecorator,
defaultExpressErrorHandlers,
ngExpressEngine as engine,
} from '@spartacus/setup/ssr';
import express from 'express';
import { readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import AppServerModule from './main.server';

const ngExpressEngine = NgExpressEngineDecorator.get(engine, {
ssrFeatureToggles: {
avoidCachingErrors: true,
},
});

// 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(browserDistFolder, 'index.html');
const indexHtmlContent = readFileSync(indexHtml, 'utf-8');
}
`;

beforeEach(() => {
tree = Tree.empty();
runner = new SchematicTestRunner('migrations', collectionPath);
jest.resetAllMocks();
tree.create('/angular.json', JSON.stringify(workspaceContent));
});

it.each(['/server.ts', '/src/server.ts'])(
'should update %s when using application builder and SSR is used',
async (filePath) => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(true);
tree.create(filePath, serverFileContent);

const newTree = await runner.runSchematic(
MIGRATION_SCRIPT_NAME,
{},
tree
);
const content = newTree.readText(filePath);

expect(content).toContain('export function app()');
expect(content).toContain("join(serverDistFolder, 'index.server.html')");
expect(content).not.toContain("join(browserDistFolder, 'index.html')");
expect(
shared.checkIfSSRIsUsedWithApplicationBuilder
).toHaveBeenCalledWith(tree);
}
);

it('should not update when SSR is not used', async () => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(false);
tree.create('/server.ts', serverFileContent);

const newTree = await runner.runSchematic(MIGRATION_SCRIPT_NAME, {}, tree);
const content = newTree.readText('/server.ts');

expect(content).toContain("join(browserDistFolder, 'index.html')");
expect(content).not.toContain(
"join(serverDistFolder, 'index.server.html')"
);
expect(shared.checkIfSSRIsUsedWithApplicationBuilder).toHaveBeenCalledWith(
tree
);
});

it('should handle missing server.ts file', async () => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(true);

const newTree = await runner.runSchematic(MIGRATION_SCRIPT_NAME, {}, tree);

expect(newTree.exists('/server.ts')).toBe(false);
expect(newTree.exists('/src/server.ts')).toBe(false);
expect(shared.checkIfSSRIsUsedWithApplicationBuilder).toHaveBeenCalledWith(
tree
);
});

it('should preserve other join statements when SSR is used', async () => {
(
shared.checkIfSSRIsUsedWithApplicationBuilder as jest.Mock
).mockReturnValue(true);

const contentWithMultipleJoins = `
const otherFile = join(process.cwd(), 'other.html');
const indexHtml = join(browserDistFolder, "index.html");
const anotherFile = join(process.cwd(), 'another.html');
`;

tree.create('/server.ts', contentWithMultipleJoins);

const newTree = await runner.runSchematic(MIGRATION_SCRIPT_NAME, {}, tree);
const content = newTree.readText('/server.ts');

expect(content).toContain("join(process.cwd(), 'other.html')");
expect(content).toContain('join(serverDistFolder, "index.server.html")');
expect(content).toContain("join(process.cwd(), 'another.html')");
expect(shared.checkIfSSRIsUsedWithApplicationBuilder).toHaveBeenCalledWith(
tree
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { normalize } from '@angular-devkit/core';
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';

/**
* Finds the server.ts file in the project.
* Checks both root and src directory locations.
*/
function findServerFile(tree: Tree): string | null {
const possiblePaths = ['./server.ts', './src/server.ts'];

for (const path of possiblePaths) {
if (tree.exists(normalize(path))) {
return path;
}
}
return null;
}

/**
* Determines if a string literal uses single or double quotes.
* @param node - The string literal node to check
* @returns true for single quotes, false for double quotes
*/
function getQuotePreference(node: ts.StringLiteral): boolean {
return node.getText().startsWith("'");
}

interface JoinCallResult {
isMatch: boolean;
quotePreference?: boolean;
}

/**
* Checks if a node is a join() call with 'index.html' as the last argument.
* Also determines the quote style used in the original code.
*/
function isJoinCallWithIndexHtml(node: ts.Node): JoinCallResult {
if (!ts.isCallExpression(node)) {
return { isMatch: false };
}

if (!ts.isIdentifier(node.expression) || node.expression.text !== 'join') {
return { isMatch: false };
}

const lastArg = node.arguments[node.arguments.length - 1];
if (!ts.isStringLiteral(lastArg)) {
return { isMatch: false };
}

if (lastArg.text === 'index.html') {
return {
isMatch: true,
quotePreference: getQuotePreference(lastArg),
};
}

return { isMatch: false };
}

/**
* Visits the node and tries to find recursively the indexHtml const.
* When found, it updates the indexHtml const to use serverDistFolder and index.server.html.
*/
function visitNodeToUpdateIndexHtmlConst(
node: ts.Node,
content: string
): string {
let updatedContent = content;

if (ts.isVariableStatement(node)) {
const declarations = node.declarationList.declarations;
if (declarations.length === 1) {
const declaration = declarations[0];
if (
ts.isIdentifier(declaration.name) &&
declaration.name.text === 'indexHtml' &&
declaration.initializer
) {
const { isMatch, quotePreference } = isJoinCallWithIndexHtml(
declaration.initializer
);
if (isMatch) {
const originalText = node.getText();
const quote = quotePreference ? "'" : '"';
const newText = `const indexHtml = join(serverDistFolder, ${quote}index.server.html${quote});`;
updatedContent = updatedContent.replace(originalText, newText);
}
}
}
}

// Recursively visit all children
ts.forEachChild(node, (childNode) => {
updatedContent = visitNodeToUpdateIndexHtmlConst(childNode, updatedContent);
});

return updatedContent;
}

/**
* Creates a rule that updates the server.ts file to use index.server.html
* instead of index.html when using the application builder.
*/
export function updateServerFile(): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info('🔍 Checking if server.ts needs to be updated...');
const serverFilePath = findServerFile(tree);
if (!serverFilePath) {
context.logger.warn('🔍 Could not find server.ts file - skipping update');
return tree;
}

context.logger.info(
`🔄 Updating ${serverFilePath} to use index.server.html`
);

const fileContentBuffer = tree.read(serverFilePath);
if (!fileContentBuffer) {
context.logger.warn(
`⚠️ Could not read ${serverFilePath} - skipping update`
);
return tree;
}

const content = fileContentBuffer.toString('utf-8');
const sourceFile = ts.createSourceFile(
'server.ts',
content,
ts.ScriptTarget.Latest,
true
);

const updatedContent = visitNodeToUpdateIndexHtmlConst(sourceFile, content);

if (updatedContent !== content) {
tree.overwrite(serverFilePath, updatedContent);
context.logger.info(`✅ Successfully updated ${serverFilePath}`);
}
return tree;
};
}
5 changes: 5 additions & 0 deletions projects/schematics/src/migrations/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@
"version": "2211.19.0",
"factory": "./2211_19/angular-json-styles/angular-json-styles#migrate",
"description": "Update the angular.json with the style preprocessor options"
},
"01-migration-v2211_35-ssr": {
"version": "2211.35.0",
"factory": "./2211_35/ssr/ssr#migrate",
"description": "Updates server.ts file to use dist/server/index.server.ts"
}
}
}
Loading
Loading