feat(@angular/ssr): move CommonEngine API to /node entry-point

Refactored the `CommonEngine` API import path to remove Node.js dependencies from the `@angular/ssr` main entry-point.

BREAKING CHANGE:

The `CommonEngine` API now needs to be imported from `@angular/ssr/node`.

**Before**
```ts
import { CommonEngine } from '@angular/ssr';
```

**After**
```ts
import { CommonEngine } from '@angular/ssr/node';
```
This commit is contained in:
Alan Agius 2024-08-26 17:05:38 +00:00 committed by Alan Agius
parent ac6935db26
commit 4b09887a9c
21 changed files with 257 additions and 45 deletions

View File

@ -4,37 +4,6 @@
```ts
import { ApplicationRef } from '@angular/core';
import { StaticProvider } from '@angular/core';
import { Type } from '@angular/core';
// @public
export class CommonEngine {
constructor(options?: CommonEngineOptions | undefined);
render(opts: CommonEngineRenderOptions): Promise<string>;
}
// @public (undocumented)
export interface CommonEngineOptions {
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
enablePerformanceProfiler?: boolean;
providers?: StaticProvider[];
}
// @public (undocumented)
export interface CommonEngineRenderOptions {
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
// (undocumented)
document?: string;
// (undocumented)
documentFilePath?: string;
inlineCriticalCss?: boolean;
providers?: StaticProvider[];
publicPath?: string;
// (undocumented)
url?: string;
}
// (No @packageDocumentation comment for this package)
```

View File

@ -0,0 +1,40 @@
## API Report File for "@angular/ssr_node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApplicationRef } from '@angular/core';
import { StaticProvider } from '@angular/core';
import { Type } from '@angular/core';
// @public
export class CommonEngine {
constructor(options?: CommonEngineOptions | undefined);
render(opts: CommonEngineRenderOptions): Promise<string>;
}
// @public (undocumented)
export interface CommonEngineOptions {
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
enablePerformanceProfiler?: boolean;
providers?: StaticProvider[];
}
// @public (undocumented)
export interface CommonEngineRenderOptions {
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
// (undocumented)
document?: string;
// (undocumented)
documentFilePath?: string;
inlineCriticalCss?: boolean;
providers?: StaticProvider[];
publicPath?: string;
// (undocumented)
url?: string;
}
// (No @packageDocumentation comment for this package)
```

View File

@ -44,6 +44,7 @@ ng_package(
tags = ["release-package"],
deps = [
":ssr",
"//packages/angular/ssr/node",
],
)

View File

@ -0,0 +1,20 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "node",
srcs = glob(
[
"*.ts",
"src/**/*.ts",
],
),
module_name = "@angular/ssr/node",
deps = [
"//packages/angular/ssr",
"@npm//@angular/core",
"@npm//@angular/platform-server",
"@npm//@types/node",
],
)

View File

@ -0,0 +1,9 @@
/**
* @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
*/
export * from './public_api';

View File

@ -0,0 +1,13 @@
/**
* @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
*/
export {
CommonEngine,
type CommonEngineRenderOptions,
type CommonEngineOptions,
} from './src/common-engine/common-engine';

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.dev/license
*/
import { ɵInlineCriticalCssProcessor as InlineCriticalCssProcessor } from '@angular/ssr';
import { readFile } from 'node:fs/promises';
import { InlineCriticalCssProcessor } from '../utils/inline-critical-css';
export class CommonEngineInlineCriticalCssProcessor {
private readonly resourceCache = new Map<string, string>();

View File

@ -16,3 +16,5 @@ export {
setAngularAppManifest as ɵsetAngularAppManifest,
setAngularAppEngineManifest as ɵsetAngularAppEngineManifest,
} from './src/manifest';
export { InlineCriticalCssProcessor as ɵInlineCriticalCssProcessor } from './src/utils/inline-critical-css';

View File

@ -6,10 +6,4 @@
* found in the LICENSE file at https://angular.dev/license
*/
export {
CommonEngine,
type CommonEngineRenderOptions,
type CommonEngineOptions,
} from './src/common-engine/common-engine';
export * from './private_export';

View File

@ -365,7 +365,7 @@ LARGE_SPECS = {
"@npm//browser-sync",
"@npm//express",
"@npm//undici",
"//packages/angular/ssr",
"//packages/angular/ssr/node",
],
},
}

View File

@ -25,7 +25,7 @@ describe('Serve SSR Builder', () => {
'src/main.server.ts': `
import 'zone.js/node';
import { CommonEngine } from '@angular/ssr';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { resolve, join } from 'node:path';
import { AppServerModule } from './app/app.module.server';

View File

@ -25,7 +25,7 @@ describe('Serve SSR Builder', () => {
'src/main.server.ts': `
import 'zone.js/node';
import { CommonEngine } from '@angular/ssr';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { resolve, join } from 'node:path';
import { AppServerModule } from './app/app.module.server';

View File

@ -24,7 +24,7 @@ describe('Serve SSR Builder', () => {
'src/main.server.ts': `
import 'zone.js/node';
import { CommonEngine } from '@angular/ssr';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { resolve, join } from 'node:path';
import { AppServerModule } from './app/app.module.server';

View File

@ -11,6 +11,11 @@
"version": "19.0.0",
"factory": "./update-workspace-config/migration",
"description": "Update the workspace configuration by replacing deprecated options in 'angular.json' for compatibility with the latest Angular CLI changes."
},
"update-ssr-imports": {
"version": "19.0.0",
"factory": "./update-ssr-imports/migration",
"description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected."
}
}
}

View File

@ -0,0 +1,92 @@
/**
* @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 { DirEntry, Rule, UpdateRecorder } from '@angular-devkit/schematics';
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
function* visit(directory: DirEntry): IterableIterator<ts.SourceFile> {
for (const path of directory.subfiles) {
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
const entry = directory.file(path);
if (entry) {
const content = entry.content;
if (content.includes('CommonEngine') && !content.includes('@angular/ssr/node')) {
const source = ts.createSourceFile(
entry.path,
content.toString().replace(/^\uFEFF/, ''),
ts.ScriptTarget.Latest,
true,
);
yield source;
}
}
}
}
for (const path of directory.subdirs) {
if (path === 'node_modules' || path.startsWith('.')) {
continue;
}
yield* visit(directory.dir(path));
}
}
/**
* Schematics rule that identifies and updates import declarations in TypeScript files.
* Specifically, it modifies imports of '@angular/ssr' by appending '/node' if the
* `CommonEngine` is used from the old entry point.
*
*/
export default function (): Rule {
return (tree) => {
for (const sourceFile of visit(tree.root)) {
let recorder: UpdateRecorder | undefined;
const allImportDeclarations = sourceFile.statements.filter((n) => ts.isImportDeclaration(n));
if (allImportDeclarations.length === 0) {
continue;
}
const ssrImports = allImportDeclarations.filter(
(n) => ts.isStringLiteral(n.moduleSpecifier) && n.moduleSpecifier.text === '@angular/ssr',
);
for (const ssrImport of ssrImports) {
const ssrNamedBinding = getNamedImports(ssrImport);
if (ssrNamedBinding) {
const isUsingOldEntryPoint = ssrNamedBinding.elements.some((e) =>
e.name.text.startsWith('CommonEngine'),
);
if (!isUsingOldEntryPoint) {
continue;
}
recorder ??= tree.beginUpdate(sourceFile.fileName);
recorder.insertRight(ssrImport.moduleSpecifier.getEnd() - 1, '/node');
}
}
if (recorder) {
tree.commitUpdate(recorder);
}
}
};
}
function getNamedImports(
importDeclaration: ts.ImportDeclaration | undefined,
): ts.NamedImports | undefined {
const namedBindings = importDeclaration?.importClause?.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
return namedBindings;
}
return undefined;
}

View File

@ -0,0 +1,67 @@
/**
* @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 { tags } from '@angular-devkit/core';
import { EmptyTree } from '@angular-devkit/schematics';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
describe('CommonEngine migration', () => {
const schematicRunner = new SchematicTestRunner(
'migrations',
require.resolve('../migration-collection.json'),
);
let tree: UnitTestTree;
beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
});
function runMigration(): Promise<UnitTestTree> {
return schematicRunner.runSchematic('update-ssr-imports', {}, tree);
}
it(`should replace 'CommonEngine*' imports from '@angular/ssr' to '@angular/ssr/node'`, async () => {
tree.create(
'/index.ts',
tags.stripIndents`
import { CommonEngine } from '@angular/ssr';
import type { CommonEngineOptions, CommonEngineRenderOptions } from '@angular/ssr';
`,
);
const newTree = await runMigration();
expect(newTree.readContent('/index.ts')).toBe(tags.stripIndents`
import { CommonEngine } from '@angular/ssr/node';
import type { CommonEngineOptions, CommonEngineRenderOptions } from '@angular/ssr/node';
`);
});
it(`should not replace 'CommonEngine*' imports from '@angular/ssr/node'`, async () => {
const input = tags.stripIndents`
import { CommonEngine } from '@angular/ssr/node';
import type { CommonEngineOptions, CommonEngineRenderOptions } from '@angular/ssr/node';
`;
tree.create('/index.ts', input);
const newTree = await runMigration();
expect(newTree.readContent('/index.ts')).toBe(input);
});
it(`should not replace 'CommonEngine*' imports from other package`, async () => {
const input = tags.stripIndents`
import { CommonEngine } from 'unknown';
import type { CommonEngineOptions, CommonEngineRenderOptions } from 'unknown';
`;
tree.create('/index.ts', input);
const newTree = await runMigration();
expect(newTree.readContent('/index.ts')).toBe(input);
});
});

View File

@ -1,5 +1,5 @@
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import { CommonEngine } from '@angular/ssr/node';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';

View File

@ -1,7 +1,7 @@
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';

View File

@ -1,7 +1,7 @@
import 'zone.js/node';
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';