feat(@schematics/angular): add support to add service worker to standalone application

This commit adds support to generate a service worker in a standalone application.
This commit is contained in:
Alan Agius 2023-04-04 14:25:18 +00:00 committed by angular-robot[bot]
parent 584b51907c
commit c2d2da41b1
3 changed files with 112 additions and 15 deletions

View File

@ -163,6 +163,7 @@ export function addModuleImportToStandaloneBootstrap(
* @param functionName Name of the function that should be called. * @param functionName Name of the function that should be called.
* @param importPath Path from which to import the function. * @param importPath Path from which to import the function.
* @param args Arguments to use when calling the function. * @param args Arguments to use when calling the function.
* @returns The file path that the provider was added to.
*/ */
export function addFunctionalProvidersToStandaloneBootstrap( export function addFunctionalProvidersToStandaloneBootstrap(
tree: Tree, tree: Tree,
@ -170,7 +171,7 @@ export function addFunctionalProvidersToStandaloneBootstrap(
functionName: string, functionName: string,
importPath: string, importPath: string,
args: ts.Expression[] = [], args: ts.Expression[] = [],
) { ): string {
const sourceFile = createSourceFile(tree, filePath); const sourceFile = createSourceFile(tree, filePath);
const bootstrapCall = findBootstrapApplicationCall(sourceFile); const bootstrapCall = findBootstrapApplicationCall(sourceFile);
const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => { const addImports = (file: ts.SourceFile, recorder: UpdateRecorder) => {
@ -198,7 +199,7 @@ export function addFunctionalProvidersToStandaloneBootstrap(
addImports(sourceFile, recorder); addImports(sourceFile, recorder);
tree.commitUpdate(recorder); tree.commitUpdate(recorder);
return; return filePath;
} }
// If the config is a `mergeApplicationProviders` call, add another config to it. // If the config is a `mergeApplicationProviders` call, add another config to it.
@ -208,7 +209,7 @@ export function addFunctionalProvidersToStandaloneBootstrap(
addImports(sourceFile, recorder); addImports(sourceFile, recorder);
tree.commitUpdate(recorder); tree.commitUpdate(recorder);
return; return filePath;
} }
// Otherwise attempt to merge into the current config. // Otherwise attempt to merge into the current config.
@ -235,6 +236,8 @@ export function addFunctionalProvidersToStandaloneBootstrap(
} }
tree.commitUpdate(recorder); tree.commitUpdate(recorder);
return configFilePath;
} }
/** Finds the call to `bootstrapApplication` within a file. */ /** Finds the call to `bootstrapApplication` within a file. */

View File

@ -20,12 +20,13 @@ import {
url, url,
} from '@angular-devkit/schematics'; } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { addFunctionalProvidersToStandaloneBootstrap } from '../private/standalone';
import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { readWorkspace, writeWorkspace } from '../utility'; import { readWorkspace, writeWorkspace } from '../utility';
import { addSymbolToNgModuleMetadata, insertImport } from '../utility/ast-utils'; import { addSymbolToNgModuleMetadata, insertImport } from '../utility/ast-utils';
import { applyToUpdateRecorder } from '../utility/change'; import { applyToUpdateRecorder } from '../utility/change';
import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies'; import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies';
import { getAppModulePath } from '../utility/ng-ast-utils'; import { getAppModulePath, isStandaloneApp } from '../utility/ng-ast-utils';
import { relativePathToWorkspaceRoot } from '../utility/paths'; import { relativePathToWorkspaceRoot } from '../utility/paths';
import { targetBuildNotFoundError } from '../utility/project-targets'; import { targetBuildNotFoundError } from '../utility/project-targets';
import { BrowserBuilderOptions } from '../utility/workspace-models'; import { BrowserBuilderOptions } from '../utility/workspace-models';
@ -85,6 +86,44 @@ function updateAppModule(mainPath: string): Rule {
}; };
} }
function addProvideServiceWorker(mainPath: string): Rule {
return (host: Tree) => {
const updatedFilePath = addFunctionalProvidersToStandaloneBootstrap(
host,
mainPath,
'provideServiceWorker',
'@angular/service-worker',
[
ts.factory.createStringLiteral('ngsw-worker.js', true),
ts.factory.createObjectLiteralExpression(
[
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier('enabled'),
ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.ExclamationToken,
ts.factory.createCallExpression(
ts.factory.createIdentifier('isDevMode'),
undefined,
[],
),
),
),
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier('registrationStrategy'),
ts.factory.createStringLiteral('registerWhenStable:30000', true),
),
],
true,
),
],
);
addImport(host, updatedFilePath, 'isDevMode', '@angular/core');
return host;
};
}
function getTsSourceFile(host: Tree, path: string): ts.SourceFile { function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
const content = host.readText(path); const content = host.readText(path);
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
@ -116,23 +155,25 @@ export default function (options: ServiceWorkerOptions): Rule {
resourcesOutputPath = normalize(`/${resourcesOutputPath}`); resourcesOutputPath = normalize(`/${resourcesOutputPath}`);
} }
const templateSource = apply(url('./files'), [ context.addTask(new NodePackageInstallTask());
await writeWorkspace(host, workspace);
const { main } = buildOptions;
return chain([
mergeWith(
apply(url('./files'), [
applyTemplates({ applyTemplates({
...options, ...options,
resourcesOutputPath, resourcesOutputPath,
relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(project.root), relativePathToWorkspaceRoot: relativePathToWorkspaceRoot(project.root),
}), }),
move(project.root), move(project.root),
]); ]),
),
context.addTask(new NodePackageInstallTask());
await writeWorkspace(host, workspace);
return chain([
mergeWith(templateSource),
addDependencies(), addDependencies(),
updateAppModule(buildOptions.main), isStandaloneApp(host, main) ? addProvideServiceWorker(main) : updateAppModule(main),
]); ]);
}; };
} }

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import { tags } from '@angular-devkit/core';
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Schema as ApplicationOptions } from '../application/schema'; import { Schema as ApplicationOptions } from '../application/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema'; import { Schema as WorkspaceOptions } from '../workspace/schema';
@ -164,4 +165,56 @@ describe('Service Worker Schematic', () => {
const { projects } = JSON.parse(tree.readContent('/angular.json')); const { projects } = JSON.parse(tree.readContent('/angular.json'));
expect(projects.foo.architect.build.options.ngswConfigPath).toBe('ngsw-config.json'); expect(projects.foo.architect.build.options.ngswConfigPath).toBe('ngsw-config.json');
}); });
describe('standalone', () => {
const name = 'buz';
const standaloneAppOptions: ApplicationOptions = {
...appOptions,
name,
standalone: true,
};
const standaloneSWOptions: ServiceWorkerOptions = {
...defaultOptions,
project: name,
};
beforeEach(async () => {
appTree = await schematicRunner.runSchematic('application', standaloneAppOptions, appTree);
});
it(`should add the 'provideServiceWorker' to providers`, async () => {
const tree = await schematicRunner.runSchematic(
'service-worker',
standaloneSWOptions,
appTree,
);
const content = tree.readContent('/projects/buz/src/app/app.config.ts');
expect(tags.oneLine`${content}`).toContain(tags.oneLine`
providers: [provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000'
})]
`);
});
it(`should import 'isDevMode' from '@angular/core'`, async () => {
const tree = await schematicRunner.runSchematic(
'service-worker',
standaloneSWOptions,
appTree,
);
const content = tree.readContent('/projects/buz/src/app/app.config.ts');
expect(content).toContain(`import { ApplicationConfig, isDevMode } from '@angular/core';`);
});
it(`should import 'provideServiceWorker' from '@angular/service-worker'`, async () => {
const tree = await schematicRunner.runSchematic(
'service-worker',
standaloneSWOptions,
appTree,
);
const content = tree.readContent('/projects/buz/src/app/app.config.ts');
expect(content).toContain(`import { provideServiceWorker } from '@angular/service-worker';`);
});
});
}); });