Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 14 additions & 6 deletions packages/core/src/platform/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,24 @@ import {Injector} from '../di';
import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory';
import {stringify} from '../util/stringify';

export interface ModuleBootstrapConfig<M> {
export interface BootstrapConfig {
platformInjector: Injector;
}

export interface ModuleBootstrapConfig<M> extends BootstrapConfig {
moduleRef: InternalNgModuleRef<M>;
allPlatformModules: NgModuleRef<unknown>[];
}

export interface ApplicationBootstrapConfig {
export interface ApplicationBootstrapConfig extends BootstrapConfig {
r3Injector: R3Injector;
platformInjector: Injector;
rootComponent: Type<unknown> | undefined;
}

function isApplicationBootstrapConfig(
config: ApplicationBootstrapConfig | ModuleBootstrapConfig<unknown>,
): config is ApplicationBootstrapConfig {
return !!(config as ApplicationBootstrapConfig).platformInjector;
return !(config as ModuleBootstrapConfig<unknown>).moduleRef;
}

export function bootstrap<M>(
Expand Down Expand Up @@ -91,9 +94,9 @@ export function bootstrap<M>(
});
});

// If the whole platform is destroyed, invoke the `destroy` method
// for all bootstrapped applications as well.
if (isApplicationBootstrapConfig(config)) {
// If the whole platform is destroyed, invoke the `destroy` method
// for all bootstrapped applications as well.
const destroyListener = () => envInjector.destroy();
const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS);
onPlatformDestroyListeners.add(destroyListener);
Expand All @@ -103,9 +106,14 @@ export function bootstrap<M>(
onPlatformDestroyListeners.delete(destroyListener);
});
} else {
const destroyListener = () => config.moduleRef.destroy();
const onPlatformDestroyListeners = config.platformInjector.get(PLATFORM_DESTROY_LISTENERS);
onPlatformDestroyListeners.add(destroyListener);

config.moduleRef.onDestroy(() => {
remove(config.allPlatformModules, config.moduleRef);
onErrorSubscription.unsubscribe();
onPlatformDestroyListeners.delete(destroyListener);
});
}

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/platform/platform_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ export class PlatformRef {
allAppProviders,
);

return bootstrap({moduleRef, allPlatformModules: this._modules});
return bootstrap({
moduleRef,
allPlatformModules: this._modules,
platformInjector: this.injector,
});
}

/**
Expand Down
49 changes: 29 additions & 20 deletions packages/platform-server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,21 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef)
}

appendServerContextInfo(applicationRef);
const output = platformState.renderToString();

// Destroy the application in a macrotask, this allows pending promises to be settled and errors
// to be surfaced to the users.
await new Promise<void>((resolve) => {
return platformState.renderToString();
Comment thread
alan-agius4 marked this conversation as resolved.
Outdated
}

/**
* Destroy the application in a macrotask, this allows pending promises to be settled and errors
* to be surfaced to the users.
*/
function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
platformRef.destroy();
resolve();
}, 0);
});

return output;
}

/**
Expand Down Expand Up @@ -264,9 +267,13 @@ export async function renderModule<T>(
): Promise<string> {
const {document, url, extraProviders: platformProviders} = options;
const platformRef = createServerPlatform({document, url, platformProviders});
const moduleRef = await platformRef.bootstrapModule(moduleType);
const applicationRef = moduleRef.injector.get(ApplicationRef);
return _render(platformRef, applicationRef);
try {
const moduleRef = await platformRef.bootstrapModule(moduleType);
const applicationRef = moduleRef.injector.get(ApplicationRef);
return await _render(platformRef, applicationRef);
} finally {
await asyncDestroyPlatform(platformRef);
}
}

/**
Expand Down Expand Up @@ -299,15 +306,17 @@ export async function renderApplication<T>(

startMeasuring(renderAppLabel);
const platformRef = createServerPlatform(options);

startMeasuring(bootstrapLabel);
const applicationRef = await bootstrap();
stopMeasuring(bootstrapLabel);

startMeasuring(_renderLabel);
const rendered = await _render(platformRef, applicationRef);
stopMeasuring(_renderLabel);

stopMeasuring(renderAppLabel);
return rendered;
try {
startMeasuring(bootstrapLabel);
const applicationRef = await bootstrap();
stopMeasuring(bootstrapLabel);

startMeasuring(_renderLabel);
const rendered = await _render(platformRef, applicationRef);
stopMeasuring(_renderLabel);
return rendered;
} finally {
await asyncDestroyPlatform(platformRef);
stopMeasuring(renderAppLabel);
}
}
150 changes: 150 additions & 0 deletions packages/platform-server/test/integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import {
ViewEncapsulation,
ɵPendingTasks as PendingTasks,
ɵwhenStable as whenStable,
APP_INITIALIZER,
inject,
getPlatform,
} from '@angular/core';
import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils';
import {TestBed} from '@angular/core/testing';
Expand Down Expand Up @@ -1076,6 +1079,153 @@ class HiddenModule {}
);
},
);

it(
`should call onOnDestroy of a service after a successful render` +
`(standalone: ${isStandalone})`,
async () => {
let wasServiceNgOnDestroyCalled = false;

@Injectable({providedIn: 'root'})
class DestroyableService {
ngOnDestroy() {
wasServiceNgOnDestroyCalled = true;
}
}

const SuccessfulAppInitializerProviders = [
{
provide: APP_INITIALIZER,
useFactory: () => {
inject(DestroyableService);
return () => Promise.resolve(); // Success in APP_INITIALIZER
},
multi: true,
},
];

@NgModule({
providers: SuccessfulAppInitializerProviders,
imports: [MyServerAppModule, ServerModule],
bootstrap: [MyServerApp],
})
class ServerSuccessfulAppInitializerModule {}

const ServerSuccessfulAppInitializerAppStandalone = getStandaloneBootstrapFn(
createMyServerApp(true),
SuccessfulAppInitializerProviders,
);

const options = {document: doc};
const bootstrap = isStandalone
? renderApplication(ServerSuccessfulAppInitializerAppStandalone, options)
: renderModule(ServerSuccessfulAppInitializerModule, options);
await bootstrap;

expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
expect(wasServiceNgOnDestroyCalled)
.withContext('DestroyableService.ngOnDestroy() should be called')
.toBeTrue();
},
);

it(
`should call onOnDestroy of a service after some APP_INITIALIZER fails ` +
`(standalone: ${isStandalone})`,
async () => {
let wasServiceNgOnDestroyCalled = false;

@Injectable({providedIn: 'root'})
class DestroyableService {
ngOnDestroy() {
wasServiceNgOnDestroyCalled = true;
}
}

const FailingAppInitializerProviders = [
{
provide: APP_INITIALIZER,
useFactory: () => {
inject(DestroyableService);
return () => Promise.reject('Error in APP_INITIALIZER');
},
multi: true,
},
];

@NgModule({
providers: FailingAppInitializerProviders,
imports: [MyServerAppModule, ServerModule],
bootstrap: [MyServerApp],
})
class ServerFailingAppInitializerModule {}

const ServerFailingAppInitializerAppStandalone = getStandaloneBootstrapFn(
createMyServerApp(true),
FailingAppInitializerProviders,
);

const options = {document: doc};
const bootstrap = isStandalone
? renderApplication(ServerFailingAppInitializerAppStandalone, options)
: renderModule(ServerFailingAppInitializerModule, options);
await expectAsync(bootstrap).toBeRejectedWith('Error in APP_INITIALIZER');

expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
expect(wasServiceNgOnDestroyCalled)
.withContext('DestroyableService.ngOnDestroy() should be called')
.toBeTrue();
},
);

it(
`should call onOnDestroy of a service after an error happens in a root component's constructor ` +
`(standalone: ${isStandalone})`,
async () => {
let wasServiceNgOnDestroyCalled = false;

@Injectable({providedIn: 'root'})
class DestroyableService {
ngOnDestroy() {
wasServiceNgOnDestroyCalled = true;
}
}

@Component({
standalone: isStandalone,
selector: 'app',
template: `Works!`,
})
class MyServerFailingConstructorApp {
constructor() {
inject(DestroyableService);
throw 'Error in constructor of the root component';
}
}

@NgModule({
declarations: [MyServerFailingConstructorApp],
imports: [MyServerAppModule, ServerModule],
bootstrap: [MyServerFailingConstructorApp],
})
class MyServerFailingConstructorAppModule {}

const MyServerFailingConstructorAppStandalone = getStandaloneBootstrapFn(
MyServerFailingConstructorApp,
);
const options = {document: doc};
const bootstrap = isStandalone
? renderApplication(MyServerFailingConstructorAppStandalone, options)
: renderModule(MyServerFailingConstructorAppModule, options);
await expectAsync(bootstrap).toBeRejectedWith(
'Error in constructor of the root component',
);
expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
expect(wasServiceNgOnDestroyCalled)
.withContext('DestroyableService.ngOnDestroy() should be called')
.toBeTrue();
},
);
});
});

Expand Down